/* * Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hazelcast.client.cache.impl; import com.hazelcast.cache.CacheStatistics; import com.hazelcast.client.impl.ClientMessageDecoder; import com.hazelcast.client.impl.protocol.ClientMessage; import com.hazelcast.client.impl.protocol.codec.CacheGetAllCodec; import com.hazelcast.client.impl.protocol.codec.CacheGetCodec; import com.hazelcast.client.impl.protocol.codec.CachePutAllCodec; import com.hazelcast.client.impl.protocol.codec.CacheSizeCodec; import com.hazelcast.client.spi.ClientContext; import com.hazelcast.client.spi.ClientPartitionService; import com.hazelcast.client.spi.impl.ClientInvocation; import com.hazelcast.client.spi.impl.ClientInvocationFuture; import com.hazelcast.client.util.ClientDelegatingFuture; import com.hazelcast.config.CacheConfig; import com.hazelcast.core.ExecutionCallback; import com.hazelcast.core.ICompletableFuture; import com.hazelcast.nio.serialization.Data; import com.hazelcast.spi.InternalCompletableFuture; import javax.cache.CacheException; import javax.cache.expiry.ExpiryPolicy; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import static com.hazelcast.cache.impl.CacheProxyUtil.NULL_KEY_IS_NOT_ALLOWED; import static com.hazelcast.cache.impl.CacheProxyUtil.validateNotNull; import static com.hazelcast.util.CollectionUtil.objectToDataCollection; import static com.hazelcast.util.ExceptionUtil.rethrow; import static com.hazelcast.util.ExceptionUtil.rethrowAllowedTypeFirst; import static com.hazelcast.util.MapUtil.createHashMap; import static com.hazelcast.util.Preconditions.checkNotNull; import static java.util.Collections.emptyMap; /** * Hazelcast provides extension functionality to default spec interface {@link javax.cache.Cache}. * {@link com.hazelcast.cache.ICache} is the designated interface. * * AbstractCacheProxyExtension provides implementation of various {@link com.hazelcast.cache.ICache} methods. * * Note: this partial implementation is used by client. * * @param <K> the type of key * @param <V> the type of value */ @SuppressWarnings("checkstyle:npathcomplexity") abstract class AbstractClientCacheProxy<K, V> extends AbstractClientInternalCacheProxy<K, V> { @SuppressWarnings("unchecked") private static final ClientMessageDecoder CACHE_GET_RESPONSE_DECODER = new ClientMessageDecoder() { @Override public <T> T decodeClientMessage(ClientMessage clientMessage) { return (T) CacheGetCodec.decodeResponse(clientMessage).response; } }; AbstractClientCacheProxy(CacheConfig<K, V> cacheConfig, ClientContext context) { super(cacheConfig, context); } protected V getSyncInternal(Data keyData, ExpiryPolicy expiryPolicy) { long startNanos = nowInNanosOrDefault(); try { ClientDelegatingFuture<V> future = getInternal(keyData, expiryPolicy, false); V value = future.get(); if (statisticsEnabled) { statsHandler.onGet(startNanos, value != null); } return value; } catch (Throwable e) { throw rethrowAllowedTypeFirst(e, CacheException.class); } } private ClientDelegatingFuture<V> getInternal(Data keyData, ExpiryPolicy expiryPolicy, boolean deserializeResponse) { Data expiryPolicyData = toData(expiryPolicy); ClientMessage request = CacheGetCodec.encodeRequest(nameWithPrefix, keyData, expiryPolicyData); int partitionId = getContext().getPartitionService().getPartitionId(keyData); ClientInvocation clientInvocation = new ClientInvocation(getClient(), request, partitionId); ClientInvocationFuture future = clientInvocation.invoke(); return newDelegatingFuture(future, CACHE_GET_RESPONSE_DECODER, deserializeResponse); } @Override public ICompletableFuture<V> getAsync(K key) { return getAsync(key, null); } @Override public ICompletableFuture<V> getAsync(K key, ExpiryPolicy expiryPolicy) { long startNanos = nowInNanosOrDefault(); ensureOpen(); validateNotNull(key); Data dataKey = toData(key); ExecutionCallback<V> callback = !statisticsEnabled ? null : statsHandler.<V>newOnGetCallback(startNanos); return getAsyncInternal(dataKey, expiryPolicy, callback); } protected InternalCompletableFuture<V> getAsyncInternal(Data dataKey, ExpiryPolicy expiryPolicy, ExecutionCallback<V> callback) { ClientDelegatingFuture<V> future = getInternal(dataKey, expiryPolicy, true); addCallback(future, callback); return future; } @Override public ICompletableFuture<Void> putAsync(K key, V value) { return putAsync(key, value, null); } @Override public ICompletableFuture<Void> putAsync(K key, V value, ExpiryPolicy expiryPolicy) { return (ICompletableFuture<Void>) putAsyncInternal(key, value, expiryPolicy, false, true, newStatsCallbackOrNull(false)); } @Override public ICompletableFuture<Boolean> putIfAbsentAsync(K key, V value) { return (ICompletableFuture<Boolean>) putIfAbsentInternal(key, value, null, false, true); } @Override public ICompletableFuture<Boolean> putIfAbsentAsync(K key, V value, ExpiryPolicy expiryPolicy) { return (ICompletableFuture<Boolean>) putIfAbsentInternal(key, value, expiryPolicy, false, true); } @Override public ICompletableFuture<V> getAndPutAsync(K key, V value) { return getAndPutAsync(key, value, null); } @Override public ICompletableFuture<V> getAndPutAsync(K key, V value, ExpiryPolicy expiryPolicy) { return putAsyncInternal(key, value, expiryPolicy, true, false, newStatsCallbackOrNull(true)); } @Override public ICompletableFuture<Boolean> removeAsync(K key) { return (ICompletableFuture<Boolean>) removeAsyncInternal(key, null, false, false, true); } @Override public ICompletableFuture<Boolean> removeAsync(K key, V oldValue) { return (ICompletableFuture<Boolean>) removeAsyncInternal(key, oldValue, true, false, true); } @Override public ICompletableFuture<V> getAndRemoveAsync(K key) { return getAndRemoveAsyncInternal(key); } @Override public ICompletableFuture<Boolean> replaceAsync(K key, V value) { return replaceAsyncInternal(key, null, value, null, false, false, true); } @Override public ICompletableFuture<Boolean> replaceAsync(K key, V value, ExpiryPolicy expiryPolicy) { return replaceAsyncInternal(key, null, value, expiryPolicy, false, false, true); } @Override public ICompletableFuture<Boolean> replaceAsync(K key, V oldValue, V newValue) { return replaceAsyncInternal(key, oldValue, newValue, null, true, false, true); } @Override public ICompletableFuture<Boolean> replaceAsync(K key, V oldValue, V newValue, ExpiryPolicy expiryPolicy) { return replaceAsyncInternal(key, oldValue, newValue, expiryPolicy, true, false, true); } @Override public ICompletableFuture<V> getAndReplaceAsync(K key, V value) { return replaceAndGetAsyncInternal(key, null, value, null, false, false, true); } @Override public ICompletableFuture<V> getAndReplaceAsync(K key, V value, ExpiryPolicy expiryPolicy) { return replaceAndGetAsyncInternal(key, null, value, expiryPolicy, false, false, true); } @Override public V get(K key, ExpiryPolicy expiryPolicy) { ensureOpen(); validateNotNull(key); return toObject(getSyncInternal(toData(key), expiryPolicy)); } @Override public Map<K, V> getAll(Set<? extends K> keys, ExpiryPolicy expiryPolicy) { long startNanos = nowInNanosOrDefault(); ensureOpen(); checkNotNull(keys, NULL_KEY_IS_NOT_ALLOWED); if (keys.isEmpty()) { return emptyMap(); } List<Data> dataKeys = new ArrayList<Data>(keys.size()); objectToDataCollection(keys, dataKeys, getSerializationService(), NULL_KEY_IS_NOT_ALLOWED); Map<K, V> resultMap = createHashMap(keys.size()); return getAllInternal(dataKeys, expiryPolicy, resultMap, startNanos); } protected Map<K, V> getAllInternal(Collection<Data> binaryKeys, ExpiryPolicy expiryPolicy, Map<K, V> resultMap, long startNanos) { Data expiryPolicyData = toData(expiryPolicy); ClientMessage request = CacheGetAllCodec.encodeRequest(nameWithPrefix, binaryKeys, expiryPolicyData); ClientMessage responseMessage = invoke(request); List<Map.Entry<Data, Data>> entries = CacheGetAllCodec.decodeResponse(responseMessage).response; for (Map.Entry<Data, Data> dataEntry : entries) { Data keyData = dataEntry.getKey(); Data valueData = dataEntry.getValue(); K key = toObject(keyData); V value = toObject(valueData); resultMap.put(key, value); } if (statisticsEnabled) { statsHandler.onBatchGet(startNanos, entries.size()); } return resultMap; } @Override public void put(K key, V value, ExpiryPolicy expiryPolicy) { putSyncInternal(key, value, expiryPolicy, false); } @Override public V getAndPut(K key, V value, ExpiryPolicy expiryPolicy) { return putSyncInternal(key, value, expiryPolicy, true); } @Override public void putAll(Map<? extends K, ? extends V> map, ExpiryPolicy expiryPolicy) { long startNanos = nowInNanosOrDefault(); ensureOpen(); checkNotNull(map, "map is null"); if (map.isEmpty()) { return; } putAllInternal(map, expiryPolicy, new List[partitionCount], startNanos); } protected void putAllInternal(Map<? extends K, ? extends V> map, ExpiryPolicy expiryPolicy, List<Map.Entry<Data, Data>>[] entriesPerPartition, long startNanos) { try { // first we fill entry set per partition groupDataToPartitions(map, getContext().getPartitionService(), entriesPerPartition); // then we invoke the operations and sync on completion of these operations putToAllPartitionsAndWaitForCompletion(entriesPerPartition, expiryPolicy, startNanos); } catch (Exception t) { throw rethrow(t); } } private void groupDataToPartitions(Map<? extends K, ? extends V> map, ClientPartitionService partitionService, List<Map.Entry<Data, Data>>[] entriesPerPartition) { for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue(); validateNotNull(key, value); Data keyData = toData(key); Data valueData = toData(value); int partitionId = partitionService.getPartitionId(keyData); List<Map.Entry<Data, Data>> entries = entriesPerPartition[partitionId]; if (entries == null) { entries = new ArrayList<Map.Entry<Data, Data>>(); entriesPerPartition[partitionId] = entries; } entries.add(new AbstractMap.SimpleImmutableEntry<Data, Data>(keyData, valueData)); } } private static final class FutureEntriesTuple { private final Future future; private final List<Map.Entry<Data, Data>> entries; private FutureEntriesTuple(Future future, List<Map.Entry<Data, Data>> entries) { this.future = future; this.entries = entries; } } private void putToAllPartitionsAndWaitForCompletion(List<Map.Entry<Data, Data>>[] entriesPerPartition, ExpiryPolicy expiryPolicy, long startNanos) throws ExecutionException, InterruptedException { Data expiryPolicyData = toData(expiryPolicy); List<FutureEntriesTuple> futureEntriesTuples = new ArrayList<FutureEntriesTuple>(entriesPerPartition.length); for (int partitionId = 0; partitionId < entriesPerPartition.length; partitionId++) { List<Map.Entry<Data, Data>> entries = entriesPerPartition[partitionId]; if (entries != null) { int completionId = nextCompletionId(); // TODO: if there is a single entry, we could make use of a put operation since that is a bit cheaper ClientMessage request = CachePutAllCodec.encodeRequest(nameWithPrefix, entries, expiryPolicyData, completionId); Future future = invoke(request, partitionId, completionId); futureEntriesTuples.add(new FutureEntriesTuple(future, entries)); } } waitResponseFromAllPartitionsForPutAll(futureEntriesTuples, startNanos); } private void waitResponseFromAllPartitionsForPutAll(List<FutureEntriesTuple> futureEntriesTuples, long startNanos) { Throwable error = null; for (FutureEntriesTuple tuple : futureEntriesTuples) { Future future = tuple.future; List<Map.Entry<Data, Data>> entries = tuple.entries; try { future.get(); // Note that we count the batch put only if there is no exception while putting to target partition. // In case of error, some of the entries might have been put and others might fail. // But we simply ignore the actual put count here if there is an error. if (statisticsEnabled) { statsHandler.getStatistics().increaseCachePuts(entries.size()); } } catch (Throwable t) { logger.finest("Error occurred while putting entries as batch!", t); if (error == null) { error = t; } } } if (statisticsEnabled) { statsHandler.getStatistics().addPutTimeNanos(nowInNanosOrDefault() - startNanos); } if (error != null) { /* * There maybe multiple exceptions but we throw only the first one. * There are some ideas to throw all exceptions to caller but all of them have drawbacks: * - `Thread::addSuppressed` can be used to add other exceptions to the first one * but it is available since JDK 7. * - `Thread::initCause` can be used but this is wrong as semantic * since the other exceptions are not cause of the first one. * - We may wrap all exceptions in our custom exception (such as `MultipleCacheException`) * but in this case caller may wait different exception type and this idea causes problem. * For example see this TCK test: * `org.jsr107.tck.integration.CacheWriterTest::shouldWriteThoughUsingPutAll_partialSuccess` * In this test exception is thrown at `CacheWriter` and caller side expects this exception. * So as a result, we only throw the first exception and others are suppressed by only logging. */ throw rethrow(error); } } @Override public boolean putIfAbsent(K key, V value, ExpiryPolicy expiryPolicy) { return (Boolean) putIfAbsentInternal(key, value, expiryPolicy, true, false); } @Override public boolean replace(K key, V oldValue, V newValue, ExpiryPolicy expiryPolicy) { return replaceSyncInternal(key, oldValue, newValue, expiryPolicy, true); } @Override public boolean replace(K key, V value, ExpiryPolicy expiryPolicy) { return replaceSyncInternal(key, null, value, expiryPolicy, false); } @Override public V getAndReplace(K key, V value, ExpiryPolicy expiryPolicy) { long startNanos = nowInNanosOrDefault(); Future<V> future = replaceAndGetAsyncInternal(key, null, value, expiryPolicy, false, true, false); try { V oldValue = future.get(); if (statisticsEnabled) { statsHandler.onReplace(true, startNanos, oldValue); } return oldValue; } catch (Throwable e) { throw rethrowAllowedTypeFirst(e, CacheException.class); } } @Override public int size() { ensureOpen(); try { ClientMessage request = CacheSizeCodec.encodeRequest(nameWithPrefix); ClientMessage resultMessage = invoke(request); return CacheSizeCodec.decodeResponse(resultMessage).response; } catch (Throwable t) { throw rethrowAllowedTypeFirst(t, CacheException.class); } } @Override public CacheStatistics getLocalCacheStatistics() { return statsHandler.getStatistics(); } }