/*
* 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.internal.nearcache;
import com.hazelcast.config.NearCacheConfig;
import com.hazelcast.internal.adapter.DataStructureAdapter;
import com.hazelcast.internal.adapter.DataStructureAdapter.DataStructureMethods;
import com.hazelcast.internal.adapter.DataStructureAdapterMethod;
import com.hazelcast.internal.adapter.ICacheCompletionListener;
import com.hazelcast.internal.adapter.ICacheReplaceEntryProcessor;
import com.hazelcast.internal.adapter.IMapReplaceEntryProcessor;
import com.hazelcast.test.AssertTask;
import com.hazelcast.test.HazelcastTestSupport;
import org.junit.Test;
import javax.cache.processor.EntryProcessorResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import static com.hazelcast.config.EvictionConfig.MaxSizePolicy.ENTRY_COUNT;
import static com.hazelcast.config.EvictionPolicy.LRU;
import static com.hazelcast.config.EvictionPolicy.NONE;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertNearCacheContent;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertNearCacheEvictionsEventually;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertNearCacheInvalidations;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertNearCacheSize;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertNearCacheSizeEventually;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertNearCacheStats;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertThatMemoryCostsAreGreaterThanZero;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assertThatMemoryCostsAreZero;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assumeThatLocalUpdatePolicyIsCacheOnUpdate;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.assumeThatLocalUpdatePolicyIsInvalidate;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.getFromNearCache;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.getFuture;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.isCacheOnUpdate;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.setEvictionConfig;
import static com.hazelcast.internal.nearcache.NearCacheTestUtils.waitUntilLoaded;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Contains the logic code for unified Near Cache tests.
*
* @param <NK> key type of the tested Near Cache
* @param <NV> value type of the tested Near Cache
*/
@SuppressWarnings("WeakerAccess")
public abstract class AbstractNearCacheBasicTest<NK, NV> extends HazelcastTestSupport {
/**
* The default count to be inserted into the Near Caches.
*/
protected static final int DEFAULT_RECORD_COUNT = 1000;
/**
* The default name used for the data structures which have a Near Cache.
*/
protected static final String DEFAULT_NEAR_CACHE_NAME = "defaultNearCache";
/**
* The partition count to configure in the Hazelcast members.
*/
protected static final String PARTITION_COUNT = "5";
/**
* Defines all {@link DataStructureMethods} which are using EntryProcessors.
*/
private static final List<DataStructureMethods> ENTRY_PROCESSOR_METHODS = asList(
DataStructureMethods.INVOKE,
DataStructureMethods.EXECUTE_ON_KEY,
DataStructureMethods.EXECUTE_ON_KEYS,
DataStructureMethods.INVOKE_ALL
);
/**
* The {@link NearCacheConfig} used by the Near Cache tests.
*
* Needs to be set by the implementations of this class in their {@link org.junit.Before} methods.
*/
protected NearCacheConfig nearCacheConfig;
/**
* Assumes that the {@link DataStructureAdapter} created by the Near Cache test supports a given
* {@link DataStructureAdapterMethod}.
*
* @param method the {@link DataStructureAdapterMethod} to test for
*/
protected abstract void assumeThatMethodIsAvailable(DataStructureAdapterMethod method);
/**
* Creates the {@link NearCacheTestContext} used by the Near Cache tests.
*
* @param <K> key type of the created {@link DataStructureAdapter}
* @param <V> value type of the created {@link DataStructureAdapter}
* @return a {@link NearCacheTestContext} used by the Near Cache tests
*/
protected <K, V> NearCacheTestContext<K, V, NK, NV> createContext() {
return createContext(false);
}
/**
* Creates the {@link NearCacheTestContext} used by the Near Cache tests.
*
* @param loaderEnabled determines if a loader should be configured
* @param <K> key type of the created {@link DataStructureAdapter}
* @param <V> value type of the created {@link DataStructureAdapter}
* @return a {@link NearCacheTestContext} used by the Near Cache tests
*/
protected abstract <K, V> NearCacheTestContext<K, V, NK, NV> createContext(boolean loaderEnabled);
protected final void populateDataAdapter(NearCacheTestContext<Integer, String, NK, NV> context) {
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
context.dataAdapter.put(i, "value-" + i);
}
assertNearCacheInvalidations(context, DEFAULT_RECORD_COUNT);
}
protected final void populateNearCache(NearCacheTestContext<Integer, String, NK, NV> context) {
populateNearCache(context, DataStructureMethods.GET);
}
protected void populateNearCache(NearCacheTestContext<Integer, String, NK, NV> context, DataStructureMethods method) {
populateNearCache(context, method, DEFAULT_RECORD_COUNT);
}
private void populateNearCache(NearCacheTestContext<Integer, String, NK, NV> context, DataStructureMethods method, int size) {
switch (method) {
case GET:
for (int i = 0; i < size; i++) {
String value = context.nearCacheAdapter.get(i);
assertEquals("value-" + i, value);
}
break;
case GET_ASYNC:
List<Future<String>> futures = new ArrayList<Future<String>>(size);
for (int i = 0; i < size; i++) {
futures.add(context.nearCacheAdapter.getAsync(i));
}
for (int i = 0; i < size; i++) {
String value = getFuture(futures.get(i), "Could not get value for index " + i + " via getAsync()");
assertEquals("value-" + i, value);
}
break;
case GET_ALL:
Set<Integer> getAllSet = new HashSet<Integer>(size);
for (int i = 0; i < size; i++) {
getAllSet.add(i);
}
Map<Integer, String> resultMap = context.nearCacheAdapter.getAll(getAllSet);
assertEquals(size, resultMap.size());
for (int i = 0; i < size; i++) {
String value = resultMap.get(i);
assertEquals("value-" + i, value);
}
break;
default:
throw new IllegalArgumentException("Unexpected method: " + method);
}
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#GET} is used
* and that the {@link com.hazelcast.monitor.NearCacheStats} are calculated correctly.
*/
@Test
public void whenGetIsUsed_thenNearCacheShouldBePopulated() {
whenGetIsUsed_thenNearCacheShouldBePopulated(DataStructureMethods.GET);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#GET_ASYNC} is used
* and that the {@link com.hazelcast.monitor.NearCacheStats} are calculated correctly.
*/
@Test
public void whenGetAsyncIsUsed_thenNearCacheShouldBePopulated() {
whenGetIsUsed_thenNearCacheShouldBePopulated(DataStructureMethods.GET_ASYNC);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#GET_ALL} is used
* and that the {@link com.hazelcast.monitor.NearCacheStats} are calculated correctly.
*/
@Test
public void whenGetAllIsUsed_thenNearCacheShouldBePopulated() {
whenGetIsUsed_thenNearCacheShouldBePopulated(DataStructureMethods.GET_ALL);
}
protected void whenGetIsUsed_thenNearCacheShouldBePopulated(DataStructureMethods method) {
assumeThatMethodIsAvailable(method);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
// populate the data structure
populateDataAdapter(context);
assertNearCacheSize(context, 0);
assertNearCacheStats(context, 0, 0, 0);
// populate the Near Cache
populateNearCache(context, method);
assertNearCacheSizeEventually(context, DEFAULT_RECORD_COUNT);
assertNearCacheStats(context, DEFAULT_RECORD_COUNT, 0, DEFAULT_RECORD_COUNT);
// generate Near Cache hits
populateNearCache(context, method);
assertNearCacheSize(context, DEFAULT_RECORD_COUNT);
assertNearCacheStats(context, DEFAULT_RECORD_COUNT, DEFAULT_RECORD_COUNT, DEFAULT_RECORD_COUNT);
assertNearCacheContent(context, DEFAULT_RECORD_COUNT);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#GET_ALL} is used on a half
* filled Near Cache and that the {@link com.hazelcast.monitor.NearCacheStats} are calculated correctly.
*/
@Test
public void whenGetAllWithHalfFilledNearCacheIsUsed_thenNearCacheShouldBePopulated() {
assumeThatMethodIsAvailable(DataStructureMethods.GET_ALL);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
// populate the data structure
populateDataAdapter(context);
assertNearCacheSize(context, 0);
assertNearCacheStats(context, 0, 0, 0);
// populate the Near Cache with half of the entries
int size = DEFAULT_RECORD_COUNT / 2;
populateNearCache(context, DataStructureMethods.GET_ALL, size);
assertNearCacheSizeEventually(context, size);
assertNearCacheStats(context, size, 0, size);
// generate Near Cache hits and populate the missing entries
int expectedHits = size;
populateNearCache(context, DataStructureMethods.GET_ALL);
assertNearCacheSize(context, DEFAULT_RECORD_COUNT);
assertNearCacheStats(context, DEFAULT_RECORD_COUNT, expectedHits, DEFAULT_RECORD_COUNT);
// generate Near Cache hits
expectedHits += DEFAULT_RECORD_COUNT;
populateNearCache(context, DataStructureMethods.GET_ALL);
assertNearCacheSize(context, DEFAULT_RECORD_COUNT);
assertNearCacheStats(context, DEFAULT_RECORD_COUNT, expectedHits, DEFAULT_RECORD_COUNT);
assertNearCacheContent(context, DEFAULT_RECORD_COUNT);
}
/**
* Checks that the Near Cache is not populated when {@link DataStructureMethods#GET_ALL} is used with an empty key set.
*/
@Test
public void whenGetAllWithEmptySetIsUsed_thenNearCacheShouldNotBePopulated() {
assumeThatMethodIsAvailable(DataStructureMethods.GET_ALL);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
// populate the data structure
populateDataAdapter(context);
assertNearCacheSize(context, 0);
assertNearCacheStats(context, 0, 0, 0);
// use getAll() with an empty set, which should not populate the Near Cache
context.nearCacheAdapter.getAll(Collections.<Integer>emptySet());
assertNearCacheSize(context, 0);
assertNearCacheStats(context, 0, 0, 0);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#SET} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenSetIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.SET);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#PUT} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenPutIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.PUT);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#PUT_IF_ABSENT} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenPutIfAbsentIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.PUT_IF_ABSENT);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#PUT_IF_ABSENT_ASYNC} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenPutIfAbsentAsyncIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.PUT_IF_ABSENT_ASYNC);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#REPLACE} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenReplaceIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.REPLACE);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#REPLACE_WITH_OLD_VALUE} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenReplaceWithOldValueIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.REPLACE_WITH_OLD_VALUE);
}
/**
* Checks that the Near Cache is populated when {@link DataStructureMethods#PUT_ALL} with
* {@link com.hazelcast.config.NearCacheConfig.LocalUpdatePolicy#CACHE_ON_UPDATE} is used.
*/
@Test
public void whenPutAllIsUsedWithCacheOnUpdate_thenNearCacheShouldBePopulated() {
whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods.PUT_ALL);
}
private void whenEntryIsAddedWithCacheOnUpdate_thenNearCacheShouldBePopulated(DataStructureMethods method) {
assumeThatMethodIsAvailable(method);
assumeThatLocalUpdatePolicyIsCacheOnUpdate(nearCacheConfig);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
DataStructureAdapter<Integer, String> adapter = context.nearCacheAdapter;
Map<Integer, String> putAllMap = new HashMap<Integer, String>(DEFAULT_RECORD_COUNT);
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
String value = "value-" + i;
switch (method) {
case SET:
adapter.set(i, value);
break;
case PUT:
assertNull(adapter.put(i, value));
break;
case PUT_IF_ABSENT:
assertTrue(adapter.putIfAbsent(i, value));
break;
case PUT_IF_ABSENT_ASYNC:
assertTrue(getFuture(adapter.putIfAbsentAsync(i, value), "Could not put value via putIfAbsentAsync()"));
break;
case REPLACE:
assertNull(adapter.replace(i, value));
break;
case REPLACE_WITH_OLD_VALUE:
String oldValue = "oldValue-" + i;
context.dataAdapter.put(i, oldValue);
assertTrue(adapter.replace(i, oldValue, value));
break;
case PUT_ALL:
putAllMap.put(i, value);
break;
default:
throw new IllegalArgumentException("Unexpected method: " + method);
}
}
if (method == DataStructureMethods.PUT_ALL) {
adapter.putAll(putAllMap);
}
String message = format("Population is not working on %s()", method.getMethodName());
assertNearCacheSizeEventually(context, DEFAULT_RECORD_COUNT, message);
assertNearCacheContent(context, DEFAULT_RECORD_COUNT);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#SET} is used.
*/
@Test
public void whenSetIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.SET);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#SET} is used.
*/
@Test
public void whenSetIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.SET);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#PUT} is used.
*/
@Test
public void whenPutIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.PUT);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#PUT} is used.
*/
@Test
public void whenPutIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.PUT);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REPLACE} is used.
*/
@Test
public void whenReplaceIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REPLACE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REPLACE} is used.
*/
@Test
public void whenReplaceIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REPLACE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REPLACE_WITH_OLD_VALUE} is used.
*/
@Test
public void whenReplaceWithOldValueIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REPLACE_WITH_OLD_VALUE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REPLACE_WITH_OLD_VALUE} is used.
*/
@Test
public void whenReplaceWithOldValueIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REPLACE_WITH_OLD_VALUE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#INVOKE} is used.
*/
@Test
public void whenInvokeIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.INVOKE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#INVOKE} is used.
*/
@Test
public void whenInvokeIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.INVOKE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#EXECUTE_ON_KEY} is used.
*/
@Test
public void whenExecuteOnKeyIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.EXECUTE_ON_KEY);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#EXECUTE_ON_KEY} is used.
*/
@Test
public void whenExecuteOnKeyIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.EXECUTE_ON_KEY);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#EXECUTE_ON_KEYS} is used.
*/
@Test
public void whenExecuteOnKeysIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.EXECUTE_ON_KEYS);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#EXECUTE_ON_KEYS} is used.
*/
@Test
public void whenExecuteOnKeysIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.EXECUTE_ON_KEYS);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#PUT_ALL} is used.
*/
@Test
public void whenPutAllIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.PUT_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#PUT_ALL} is used.
*/
@Test
public void whenPutAllIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.PUT_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#INVOKE_ALL} is used.
*/
@Test
public void whenInvokeAllIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.INVOKE_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#INVOKE_ALL} is used.
*/
@Test
public void whenInvokeAllIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsChanged_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.INVOKE_ALL);
}
/**
* With the {@link NearCacheTestContext#dataAdapter} we have to set {@link NearCacheConfig#setInvalidateOnChange(boolean)}.
* With the {@link NearCacheTestContext#nearCacheAdapter} Near Cache invalidations are not needed.
*/
private void whenEntryIsChanged_thenNearCacheShouldBeInvalidated(boolean useDataAdapter, DataStructureMethods method) {
assumeThatMethodIsAvailable(method);
if (!ENTRY_PROCESSOR_METHODS.contains(method)) {
// since EntryProcessors return a user-defined result we cannot directly put this into the Near Cache,
// so we execute this test also for CACHE_ON_UPDATE configurations
assumeThatLocalUpdatePolicyIsInvalidate(nearCacheConfig);
}
nearCacheConfig.setInvalidateOnChange(useDataAdapter);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
DataStructureAdapter<Integer, String> adapter = useDataAdapter ? context.dataAdapter : context.nearCacheAdapter;
populateDataAdapter(context);
populateNearCache(context);
// this should invalidate the Near Cache
Map<Integer, String> invalidationMap = new HashMap<Integer, String>(DEFAULT_RECORD_COUNT);
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
String value = "value-" + i;
String newValue = "newValue-" + i;
switch (method) {
case SET:
adapter.set(i, newValue);
break;
case PUT:
assertEquals(value, adapter.put(i, newValue));
break;
case REPLACE:
assertEquals(value, adapter.replace(i, newValue));
break;
case REPLACE_WITH_OLD_VALUE:
assertTrue(adapter.replace(i, value, newValue));
break;
case INVOKE:
assertEquals(newValue, adapter.invoke(i, new ICacheReplaceEntryProcessor(), "value", "newValue"));
break;
case EXECUTE_ON_KEY:
assertEquals(newValue, adapter.executeOnKey(i, new IMapReplaceEntryProcessor("value", "newValue")));
break;
case EXECUTE_ON_KEYS:
case PUT_ALL:
case INVOKE_ALL:
invalidationMap.put(i, newValue);
break;
default:
throw new IllegalArgumentException("Unexpected method: " + method);
}
}
if (method == DataStructureMethods.EXECUTE_ON_KEYS) {
Map<Integer, Object> resultMap = adapter.executeOnKeys(invalidationMap.keySet(),
new IMapReplaceEntryProcessor("value", "newValue"));
assertEquals(DEFAULT_RECORD_COUNT, resultMap.size());
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
assertEquals("newValue-" + i, resultMap.get(i));
}
} else if (method == DataStructureMethods.PUT_ALL) {
adapter.putAll(invalidationMap);
} else if (method == DataStructureMethods.INVOKE_ALL) {
Map<Integer, EntryProcessorResult<String>> resultMap = adapter.invokeAll(invalidationMap.keySet(),
new ICacheReplaceEntryProcessor(), "value", "newValue");
assertEquals(DEFAULT_RECORD_COUNT, resultMap.size());
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
assertEquals("newValue-" + i, resultMap.get(i).get());
}
}
String message = format("Invalidation is not working on %s()", method.getMethodName());
assertNearCacheSizeEventually(context, 0, message);
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
assertEquals("newValue-" + i, context.dataAdapter.get(i));
}
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#LOAD_ALL} is used.
*/
@Test
public void whenLoadAllIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.LOAD_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#LOAD_ALL} is used.
*/
@Test
public void whenLoadAllIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.LOAD_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#LOAD_ALL_WITH_KEYS} is used.
*/
@Test
public void whenLoadAllWithKeysIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.LOAD_ALL_WITH_KEYS);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#LOAD_ALL_WITH_KEYS} is used.
*/
@Test
public void whenLoadAllWithKeysIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.LOAD_ALL_WITH_KEYS);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#LOAD_ALL_WITH_LISTENER} is used.
*/
@Test
public void whenLoadAllWithListenerIsUsed_thenNearCacheIsInvalidated_onNearCacheAdapter() {
whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.LOAD_ALL_WITH_LISTENER);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#LOAD_ALL_WITH_LISTENER} is used.
*/
@Test
public void whenLoadAllWithListenerIsUsed_thenNearCacheIsInvalidated_onDataAdapter() {
whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.LOAD_ALL_WITH_LISTENER);
}
/**
* With the {@link NearCacheTestContext#dataAdapter} we have to set {@link NearCacheConfig#setInvalidateOnChange(boolean)}.
* With the {@link NearCacheTestContext#nearCacheAdapter} Near Cache invalidations are not needed.
*/
private void whenEntryIsLoaded_thenNearCacheShouldBeInvalidated(boolean useDataAdapter, DataStructureMethods method) {
assumeThatMethodIsAvailable(method);
nearCacheConfig.setInvalidateOnChange(useDataAdapter);
NearCacheTestContext<Integer, String, NK, NV> context = createContext(true);
DataStructureAdapter<Integer, String> adapter = useDataAdapter ? context.dataAdapter : context.nearCacheAdapter;
// wait until the initial load is done
waitUntilLoaded(context);
populateDataAdapter(context);
populateNearCache(context);
// this should invalidate the Near Cache
Set<Integer> keys = new HashSet<Integer>(DEFAULT_RECORD_COUNT);
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
keys.add(i);
}
switch (method) {
case LOAD_ALL:
context.loader.setKeys(keys);
adapter.loadAll(true);
break;
case LOAD_ALL_WITH_KEYS:
adapter.loadAll(keys, true);
break;
case LOAD_ALL_WITH_LISTENER:
ICacheCompletionListener listener = new ICacheCompletionListener();
adapter.loadAll(keys, true, listener);
listener.await();
break;
default:
throw new IllegalArgumentException("Unexpected method: " + method);
}
// wait until the loader is finished and validate the updated values
waitUntilLoaded(context);
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
assertEquals("newValue-" + i, context.dataAdapter.get(i));
}
String message = format("Invalidation is not working on %s()", method.getMethodName());
assertNearCacheSizeEventually(context, 0, message);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE} is used.
*/
@Test
public void whenRemoveIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REMOVE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE} is used.
*/
@Test
public void whenRemoveIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REMOVE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_ASYNC} is used.
*/
@Test
public void whenRemoveAsyncIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REMOVE_ASYNC);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_ASYNC} is used.
*/
@Test
public void whenRemoveAsyncIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REMOVE_ASYNC);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_WITH_OLD_VALUE)} is used.
*/
@Test
public void whenRemoveWithOldValueIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REMOVE_WITH_OLD_VALUE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_WITH_OLD_VALUE)} is used.
*/
@Test
public void whenRemoveWithOldValueIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REMOVE_WITH_OLD_VALUE);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_ALL)} is used.
*/
@Test
public void whenRemoveAllIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REMOVE_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_ALL)} is used.
*/
@Test
public void whenRemoveAllIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REMOVE_ALL);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_ALL_WITH_KEYS)} is used.
*/
@Test
public void whenRemoveAllWithKeysIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.REMOVE_ALL_WITH_KEYS);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#REMOVE_ALL_WITH_KEYS)} is used.
*/
@Test
public void whenRemoveAllWithKeysIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.REMOVE_ALL_WITH_KEYS);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#CLEAR)} is used.
*/
@Test
public void whenClearIsUsed_thenNearCacheShouldBeInvalidated_onNearCacheAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(false, DataStructureMethods.CLEAR);
}
/**
* Checks that the Near Cache is eventually invalidated when {@link DataStructureMethods#CLEAR)} is used.
*/
@Test
public void whenClearIsUsed_thenNearCacheShouldBeInvalidated_onDataAdapter() {
whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(true, DataStructureMethods.CLEAR);
}
/**
* With the {@link NearCacheTestContext#dataAdapter} we have to set {@link NearCacheConfig#setInvalidateOnChange(boolean)}.
* With the {@link NearCacheTestContext#nearCacheAdapter} Near Cache invalidations are not needed.
*/
private void whenEntryIsRemoved_thenNearCacheShouldBeInvalidated(boolean useDataAdapter, DataStructureMethods method) {
assumeThatMethodIsAvailable(method);
nearCacheConfig.setInvalidateOnChange(useDataAdapter);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
DataStructureAdapter<Integer, String> adapter = useDataAdapter ? context.dataAdapter : context.nearCacheAdapter;
populateDataAdapter(context);
populateNearCache(context);
// this should invalidate the Near Cache
Set<Integer> removeKeys = new HashSet<Integer>();
if (method == DataStructureMethods.REMOVE_ALL) {
adapter.removeAll();
} else {
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
String value = "value-" + i;
switch (method) {
case REMOVE:
adapter.remove(i);
break;
case REMOVE_WITH_OLD_VALUE:
assertTrue(adapter.remove(i, value));
break;
case REMOVE_ASYNC:
assertEquals(value, getFuture(adapter.removeAsync(i), "Could not remove entry via removeAsync()"));
break;
case REMOVE_ALL_WITH_KEYS:
removeKeys.add(i);
break;
case CLEAR:
break;
default:
throw new IllegalArgumentException("Unexpected method: " + method);
}
}
}
if (method == DataStructureMethods.REMOVE_ALL_WITH_KEYS) {
adapter.removeAll(removeKeys);
} else if (method == DataStructureMethods.CLEAR) {
adapter.clear();
}
String message = format("Invalidation is not working on %s()", method.getMethodName());
assertNearCacheSizeEventually(context, 0, message);
}
/**
* Checks that the Near Cache works correctly when {@link DataStructureMethods#CONTAINS_KEY} is used.
*/
@Test
public void testContainsKey_onNearCacheAdapter() {
testContainsKey(false);
}
/**
* Checks that the Near Cache works correctly when {@link DataStructureMethods#CONTAINS_KEY} is used.
*/
@Test
public void testContainsKey_onDataAdapter() {
testContainsKey(true);
}
/**
* With the {@link NearCacheTestContext#dataAdapter} we have to set {@link NearCacheConfig#setInvalidateOnChange(boolean)}.
* With the {@link NearCacheTestContext#nearCacheAdapter} Near Cache invalidations are not needed.
*/
private void testContainsKey(boolean useDataAdapter) {
assumeThatMethodIsAvailable(DataStructureMethods.CONTAINS_KEY);
nearCacheConfig.setInvalidateOnChange(useDataAdapter);
final NearCacheTestContext<Integer, String, NK, NV> context = createContext();
// populate data structure
context.dataAdapter.put(1, "value-1");
context.dataAdapter.put(2, "value-2");
context.dataAdapter.put(3, "value-3");
assertNearCacheInvalidations(context, 3);
// populate Near Cache
context.nearCacheAdapter.get(1);
context.nearCacheAdapter.get(2);
context.nearCacheAdapter.get(3);
assertTrue(context.nearCacheAdapter.containsKey(1));
assertTrue(context.nearCacheAdapter.containsKey(2));
assertTrue(context.nearCacheAdapter.containsKey(3));
assertFalse(context.nearCacheAdapter.containsKey(4));
assertFalse(context.nearCacheAdapter.containsKey(5));
// remove a key which is in the Near Cache
DataStructureAdapter<Integer, String> adapter = useDataAdapter ? context.dataAdapter : context.nearCacheAdapter;
adapter.remove(1);
adapter.remove(3);
assertTrueEventually(new AssertTask() {
@Override
public void run() {
assertFalse(context.nearCacheAdapter.containsKey(1));
assertTrue(context.nearCacheAdapter.containsKey(2));
assertFalse(context.nearCacheAdapter.containsKey(3));
assertFalse(context.nearCacheAdapter.containsKey(4));
assertFalse(context.nearCacheAdapter.containsKey(5));
}
});
}
/**
* Checks that the Near Cache eviction works as expected if the Near Cache is full.
*/
@Test
public void testNearCacheEviction() {
setEvictionConfig(nearCacheConfig, LRU, ENTRY_COUNT, DEFAULT_RECORD_COUNT);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
// all Near Cache implementations use the same eviction algorithm, which evicts a single entry
int expectedEvictions = 1;
// populate data structure with an extra entry
populateDataAdapter(context);
context.dataAdapter.put(DEFAULT_RECORD_COUNT, "value-" + DEFAULT_RECORD_COUNT);
assertNearCacheInvalidations(context, DEFAULT_RECORD_COUNT + 1);
// populate Near Caches
populateNearCache(context);
// we expect (size + the extra entry - the expectedEvictions) entries in the Near Cache
long expectedOwnedEntryCount = DEFAULT_RECORD_COUNT + 1 - expectedEvictions;
long expectedHits = context.stats.getHits();
long expectedMisses = context.stats.getMisses() + 1;
// trigger eviction via fetching the extra entry
context.nearCacheAdapter.get(DEFAULT_RECORD_COUNT);
assertNearCacheEvictionsEventually(context, expectedEvictions);
assertNearCacheStats(context, expectedOwnedEntryCount, expectedHits, expectedMisses, expectedEvictions, 0);
}
/**
* Checks that the memory costs are calculated correctly.
*
* This variant uses a single-threaded approach to fill the Near Cache with data.
*/
@Test
public void testNearCacheMemoryCostCalculation() {
testNearCacheMemoryCostCalculation(1);
}
/**
* Checks that the memory costs are calculated correctly.
*
* This variant uses a multi-threaded approach to fill the Near Cache with data.
*/
@Test
public void testNearCacheMemoryCostCalculation_withConcurrentCacheMisses() {
testNearCacheMemoryCostCalculation(10);
}
private void testNearCacheMemoryCostCalculation(int threadCount) {
final NearCacheTestContext<Integer, String, NK, NV> context = createContext();
populateDataAdapter(context);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
Runnable task = new Runnable() {
@Override
public void run() {
populateNearCache(context);
countDownLatch.countDown();
}
};
ExecutorService executorService = newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(task);
}
assertOpenEventually(countDownLatch);
// the Near Cache is filled, we should see some memory costs now
assertNearCacheSizeEventually(context, DEFAULT_RECORD_COUNT);
assertThatMemoryCostsAreGreaterThanZero(context, nearCacheConfig.getInMemoryFormat());
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
context.nearCacheAdapter.remove(i);
}
// the Near Cache is empty, we shouldn't see memory costs anymore
assertNearCacheSizeEventually(context, 0);
assertThatMemoryCostsAreZero(context);
}
/**
* Checks that the Near Cache never returns its internal {@link NearCache#CACHED_AS_NULL} to the public API.
*/
@Test
public void whenEmptyDataStructure_thenPopulatedNearCacheShouldReturnNull_neverCACHED_AS_NULL() {
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
for (int i = 0; i < DEFAULT_RECORD_COUNT; i++) {
// populate Near Cache
assertNull("Expected null from original data structure for key " + i, context.nearCacheAdapter.get(i));
// fetch value from Near Cache
assertNull("Expected null from near cached data structure for key " + i, context.nearCacheAdapter.get(i));
// fetch internal value directly from Near Cache
NV value = getFromNearCache(context, i);
if (value != null) {
// the internal value should either be `null` or `CACHED_AS_NULL`
assertEquals("Expected CACHED_AS_NULL in Near Cache for key " + i, NearCache.CACHED_AS_NULL, value);
}
}
}
/**
* Checks that the Near Cache updates value for keys which are already in the Near Cache,
* even if the Near Cache is full an the eviction is disabled (via {@link com.hazelcast.config.EvictionPolicy#NONE}.
*/
@Test
public void whenNearCacheIsFull_thenPutOnSameKeyShouldUpdateValue_onNearCacheAdapter() {
whenNearCacheIsFull_thenPutOnSameKeyShouldUpdateValue(false);
}
/**
* Checks that the Near Cache updates value for keys which are already in the Near Cache,
* even if the Near Cache is full an the eviction is disabled (via {@link com.hazelcast.config.EvictionPolicy#NONE}.
*/
@Test
public void whenNearCacheIsFull_thenPutOnSameKeyShouldUpdateValue_onDataAdapter() {
whenNearCacheIsFull_thenPutOnSameKeyShouldUpdateValue(true);
}
/**
* With the {@link NearCacheTestContext#dataAdapter} we have to set {@link NearCacheConfig#setInvalidateOnChange(boolean)}.
* With the {@link NearCacheTestContext#nearCacheAdapter} Near Cache invalidations are not needed.
*/
private void whenNearCacheIsFull_thenPutOnSameKeyShouldUpdateValue(boolean useDataAdapter) {
int size = DEFAULT_RECORD_COUNT / 2;
setEvictionConfig(nearCacheConfig, NONE, ENTRY_COUNT, size);
nearCacheConfig.setInvalidateOnChange(useDataAdapter);
NearCacheTestContext<Integer, String, NK, NV> context = createContext();
DataStructureAdapter<Integer, String> adapter = useDataAdapter ? context.dataAdapter : context.nearCacheAdapter;
boolean isNearCacheDirectlyUpdated = isCacheOnUpdate(nearCacheConfig) && !useDataAdapter;
populateDataAdapter(context);
populateNearCache(context);
// assert that the old value is present
assertNearCacheSize(context, size);
assertEquals("value-1", context.nearCacheAdapter.get(1));
// update the entry
adapter.put(1, "newValue");
// wait for the invalidation to be processed
int expectedSize = isNearCacheDirectlyUpdated ? size : size - 1;
assertNearCacheSizeEventually(context, expectedSize);
assertNearCacheInvalidations(context, 1);
long expectedHits = context.stats.getHits() + (isNearCacheDirectlyUpdated ? 2 : 1);
long expectedMisses = context.stats.getMisses() + (isNearCacheDirectlyUpdated ? 0 : 1);
// assert that the new entry is updated
assertEquals("newValue", context.nearCacheAdapter.get(1));
assertEquals("newValue", context.nearCacheAdapter.get(1));
assertNearCacheStats(context, size, expectedHits, expectedMisses);
}
}