/*
* 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.map.impl.nearcache;
import com.hazelcast.config.Config;
import com.hazelcast.config.EvictionConfig;
import com.hazelcast.config.EvictionPolicy;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.MapStoreConfig;
import com.hazelcast.config.NearCacheConfig;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.hazelcast.core.MapStoreAdapter;
import com.hazelcast.internal.nearcache.NearCache;
import com.hazelcast.map.AbstractEntryProcessor;
import com.hazelcast.map.impl.MapService;
import com.hazelcast.map.impl.MapServiceContext;
import com.hazelcast.map.impl.proxy.NearCachedMapProxyImpl;
import com.hazelcast.map.listener.EntryEvictedListener;
import com.hazelcast.monitor.NearCacheStats;
import com.hazelcast.monitor.impl.NearCacheStatsImpl;
import com.hazelcast.spi.impl.NodeEngineImpl;
import com.hazelcast.test.AssertTask;
import com.hazelcast.test.HazelcastTestSupport;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static com.hazelcast.config.EvictionConfig.MaxSizePolicy.ENTRY_COUNT;
import static com.hazelcast.instance.BuildInfoProvider.BUILD_INFO;
import static com.hazelcast.map.impl.MapService.SERVICE_NAME;
import static com.hazelcast.spi.properties.GroupProperty.MAP_INVALIDATION_MESSAGE_BATCH_FREQUENCY_SECONDS;
import static com.hazelcast.spi.properties.GroupProperty.MAP_INVALIDATION_MESSAGE_BATCH_SIZE;
import static com.hazelcast.spi.properties.GroupProperty.PARTITION_COUNT;
import static java.lang.String.format;
import static java.util.concurrent.Executors.newFixedThreadPool;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class NearCacheTestSupport extends HazelcastTestSupport {
protected static final int MAX_CACHE_SIZE = 5000;
protected static final int MAX_TTL_SECONDS = 2;
protected static final int MAX_IDLE_SECONDS = 1;
protected void testNearCacheEviction(IMap<Integer, Integer> map, int size) {
// all Near Cache implementations use the same eviction algorithm, which evicts a single entry
int expectedEvictions = 1;
// populate map with an extra entry
populateMap(map, size + 1);
// populate Near Caches
populateNearCache(map, size);
NearCacheStats statsBeforeEviction = getNearCacheStatsCopy(map);
// trigger eviction via fetching the extra entry
map.get(size);
waitForNearCacheEvictions(map, expectedEvictions);
// we expect (size + the extra entry - the expectedEvictions) entries in the Near Cache
int expectedOwnedEntryCount = size + 1 - expectedEvictions;
NearCacheStats stats = getNearCacheStats(map);
assertEquals("got the wrong ownedEntryCount", expectedOwnedEntryCount, stats.getOwnedEntryCount());
assertEquals("got the wrong eviction count", expectedEvictions, stats.getEvictions());
assertEquals("got the wrong expiration count", 0, stats.getExpirations());
assertEquals("we expect the same hits", statsBeforeEviction.getHits(), stats.getHits());
assertEquals("we expect one miss more", statsBeforeEviction.getMisses() + 1, stats.getMisses());
}
protected void testNearCacheExpiration(final IMap<Integer, Integer> map, final int size, int expireSeconds) {
populateMap(map, size);
populateNearCache(map, size);
final NearCacheStats statsBeforeExpiration = getNearCacheStatsCopy(map);
assertTrueEventually(new AssertTask() {
@Override
public void run() throws Exception {
assertTrue(format("we expected to have all map entries in the Near Cache or already expired (%s)",
statsBeforeExpiration),
statsBeforeExpiration.getOwnedEntryCount() + statsBeforeExpiration.getExpirations() >= size);
}
});
sleepSeconds(expireSeconds + 1);
final AtomicInteger invocationCounter = new AtomicInteger();
assertTrueEventually(new AssertTask() {
public void run() {
// map.get() triggers Near Cache eviction/expiration process,
// but we need to call this on every assert since the Near Cache has a cooldown for expiration cleanups
map.get(0);
long invocations = invocationCounter.incrementAndGet();
NearCacheStats stats = getNearCacheStatsCopy(map);
long hits = stats.getHits();
long misses = stats.getMisses();
long oldHits = statsBeforeExpiration.getHits();
long oldMisses = statsBeforeExpiration.getMisses();
assertEquals(format("we expect just the 'trigger entry' to be left in the Near Cache (%s)", stats),
1, stats.getOwnedEntryCount());
assertTrue(format("we expect hits between %d and %d (%s)", oldHits, oldHits + invocations, stats),
hits >= oldHits && hits <= oldHits + invocations);
assertTrue(format("we expect misses between %d and %d (%s)", oldMisses, oldMisses + invocations, stats),
misses >= oldMisses && misses <= oldMisses + invocations);
assertTrue(format("we expect at least %d entries to be expired from the Near Cache (%s)", size, stats),
stats.getExpirations() >= size);
assertEquals(format("we expect no entries to be evicted (%s)", stats), 0, stats.getEvictions());
}
});
}
/**
* Tests the Near Cache memory cost calculation.
* <p>
* Depending on the parameters the following memory costs are asserted:
* <ul>
* <li>{@link NearCacheStats#getOwnedEntryMemoryCost()}</li>
* <li>{@link com.hazelcast.monitor.LocalMapStats#getHeapCost()}</li>
* </ul>
*
* @param map the {@link IMap} with a Near Cache to be tested
* @param isMember determines if the heap costs will be asserted, which are just available for member nodes
* @param threadCount the thread count for concurrent population of the Near Cache
*/
protected void testNearCacheMemoryCostCalculation(final IMap<Integer, Integer> map, boolean isMember, int threadCount) {
populateMap(map, MAX_CACHE_SIZE);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
Runnable task = new Runnable() {
@Override
public void run() {
populateNearCache(map, MAX_CACHE_SIZE);
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
assertTrue("The Near Cache is filled, there should be some owned entry memory costs",
getNearCacheStats(map).getOwnedEntryMemoryCost() > 0);
if (isMember && !BUILD_INFO.isEnterprise()) {
// the heap costs are just calculated on member on-heap maps
assertTrue("The Near Cache is filled, there should be some heap costs", map.getLocalMapStats().getHeapCost() > 0);
}
for (int i = 0; i < MAX_CACHE_SIZE; i++) {
map.remove(i);
}
// the Near Cache is empty, we shouldn't see memory costs anymore
assertEquals("The Near Cache is empty, there should be no owned entry memory costs",
0, getNearCacheStats(map).getOwnedEntryMemoryCost());
// this assert will work in all scenarios, since the default value should be 0 if no costs are calculated
assertEquals("The Near Cache is empty, there should be no heap costs", 0, map.getLocalMapStats().getHeapCost());
}
protected NearCacheConfig newNearCacheConfigWithEntryCountEviction(EvictionPolicy evictionPolicy, int size) {
return newNearCacheConfig()
.setCacheLocalEntries(true)
.setEvictionConfig(new EvictionConfig(size, ENTRY_COUNT, evictionPolicy));
}
protected NearCacheConfig newNearCacheConfig() {
return new NearCacheConfig();
}
protected void triggerEviction(IMap<Integer, Integer> map) {
map.put(0, 0);
}
/**
* There is a time-window in that an "is Near Cache evictable?" check may return {@code false},
* although the Near Cache size is bigger than the configured Near Cache max-size.
* This can happen because eviction process is offloaded to a different thread
* and there is no synchronization between the thread that puts the entry to the Near Cache
* and the thread which sweeps the entries from the Near Cache.
* This method continuously triggers the eviction to bring the Near Cache size under the configured max-size.
* Only needed for testing purposes.
*/
protected void triggerNearCacheEviction(IMap<Integer, Integer> map) {
populateMap(map, 1);
populateNearCache(map, 1);
}
protected void waitForNearCacheEvictions(final IMap map, final int evictionCount) {
assertTrueEventually(new AssertTask() {
public void run() {
long evictions = getNearCacheStats(map).getEvictions();
assertTrue(
format("Near Cache eviction count didn't reach the desired value (%d vs. %d)", evictions, evictionCount),
evictions >= evictionCount);
}
});
}
protected void waitUntilEvictionEventsReceived(CountDownLatch latch) {
assertOpenEventually(latch);
}
protected void addEntryEvictedListener(IMap<Integer, Integer> map, final CountDownLatch latch) {
map.addLocalEntryListener(new EntryEvictedListener<Integer, Integer>() {
@Override
public void entryEvicted(EntryEvent<Integer, Integer> event) {
latch.countDown();
}
});
}
protected void populateMapWithExpirableEntries(IMap<Integer, Integer> map, int mapSize, long ttl, TimeUnit timeunit) {
for (int i = 0; i < mapSize; i++) {
map.put(i, i, ttl, timeunit);
}
}
protected void populateMap(Map<Integer, Integer> map, int mapSize) {
for (int i = 0; i < mapSize; i++) {
map.put(i, i);
}
}
protected void populateNearCache(Map<Integer, ?> map, int mapSize) {
for (int i = 0; i < mapSize; i++) {
map.get(i);
}
}
protected Config createNearCachedMapConfig(String mapName) {
Config config = getConfig();
config.setProperty(MAP_INVALIDATION_MESSAGE_BATCH_FREQUENCY_SECONDS.getName(), "5");
config.setProperty(MAP_INVALIDATION_MESSAGE_BATCH_SIZE.getName(), "10000");
config.setProperty(PARTITION_COUNT.getName(), "1");
NearCacheConfig nearCacheConfig = newNearCacheConfig();
nearCacheConfig.setCacheLocalEntries(true);
MapConfig mapConfig = config.getMapConfig(mapName);
mapConfig.setNearCacheConfig(nearCacheConfig);
return config;
}
protected Config createNearCachedMapConfigWithMapStoreConfig(String mapName) {
SimpleMapStore store = new SimpleMapStore();
MapStoreConfig mapStoreConfig = new MapStoreConfig();
mapStoreConfig.setEnabled(true);
mapStoreConfig.setImplementation(store);
Config config = createNearCachedMapConfig(mapName);
config.getMapConfig(mapName).setMapStoreConfig(mapStoreConfig);
return config;
}
protected NearCache getNearCache(String mapName, HazelcastInstance instance) {
NodeEngineImpl nodeEngine = getNode(instance).nodeEngine;
MapService service = nodeEngine.getService(SERVICE_NAME);
MapServiceContext mapServiceContext = service.getMapServiceContext();
MapNearCacheManager mapNearCacheManager = mapServiceContext.getMapNearCacheManager();
NearCacheConfig nearCacheConfig = nodeEngine.getConfig().getMapConfig(mapName).getNearCacheConfig();
return mapNearCacheManager.getOrCreateNearCache(mapName, nearCacheConfig);
}
protected int getNearCacheSize(IMap map) {
return ((NearCachedMapProxyImpl) map).getNearCache().size();
}
protected NearCacheStats getNearCacheStatsCopy(IMap map) {
return new NearCacheStatsImpl(getNearCacheStats(map));
}
protected NearCacheStats getNearCacheStats(IMap map) {
return map.getLocalMapStats().getNearCacheStats();
}
protected void assertThatOwnedEntryCountEquals(IMap<Integer, Integer> clientMap, long expected) {
assertEquals(expected, getNearCacheStats(clientMap).getOwnedEntryCount());
}
protected void assertThatOwnedEntryCountIsSmallerThan(IMap<Integer, Integer> clientMap, long expected) {
long ownedEntryCount = getNearCacheStats(clientMap).getOwnedEntryCount();
assertTrue(format("ownedEntryCount should be smaller than %d, but was %d", expected, ownedEntryCount),
ownedEntryCount < expected);
}
public static class SimpleMapStore<K, V> extends MapStoreAdapter<K, V> {
private final Map<K, V> store = new ConcurrentHashMap<K, V>();
private boolean loadAllKeys = true;
@Override
public void delete(final K key) {
store.remove(key);
}
@Override
public V load(final K key) {
return store.get(key);
}
@Override
public void store(final K key, final V value) {
store.put(key, value);
}
@Override
public Set<K> loadAllKeys() {
if (loadAllKeys) {
return store.keySet();
}
return null;
}
public void setLoadAllKeys(boolean loadAllKeys) {
this.loadAllKeys = loadAllKeys;
}
@Override
public void storeAll(final Map<K, V> kvMap) {
store.putAll(kvMap);
}
}
public static class IncrementEntryProcessor extends AbstractEntryProcessor<Integer, Integer> {
@Override
public Object process(Map.Entry<Integer, Integer> entry) {
int currentValue = entry.getValue();
int newValue = currentValue + 1000;
entry.setValue(newValue);
return newValue;
}
}
}