package org.infinispan.client.hotrod.event; import static org.infinispan.server.hotrod.test.HotRodTestingUtil.hotRodCacheConfiguration; import static org.testng.AssertJUnit.assertEquals; import java.io.Serializable; import java.lang.management.BufferPoolMXBean; import java.lang.management.ManagementFactory; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; import org.infinispan.client.hotrod.annotation.ClientListener; import org.infinispan.client.hotrod.test.MultiHotRodServersTest; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.filter.NamedFactory; import org.infinispan.marshall.core.ExternalPojo; import org.infinispan.metadata.Metadata; import org.infinispan.notifications.cachelistener.filter.CacheEventConverter; import org.infinispan.notifications.cachelistener.filter.CacheEventConverterFactory; import org.infinispan.notifications.cachelistener.filter.EventType; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.testng.annotations.Test; /** * @author anistor@redhat.com * @since 8.2 */ @Test(groups = "functional", testName = "client.hotrod.event.ClientEventsOOMTest") public class ClientEventsOOMTest extends MultiHotRodServersTest { private static final int NUM_ENTRIES = Integer.getInteger("client.stress.num_entries", 1000); private static final long SLEEP_TIME = Long.getLong("client.stress.sleep_time", 10); // ms private static final int NUM_NODES = 2; private static BufferPoolMXBean DIRECT_POOL = getDirectMemoryPool(); private RemoteCache<Integer, byte[]> remoteCache; // There is only one Godzilla in heap, but we can use Netty's off-heap pools to multiply them and destroy the world private static final byte[] GODZILLA = makeGodzilla(); @Override protected void createCacheManagers() throws Throwable { ConfigurationBuilder cfgBuilder = getConfigurationBuilder(); createHotRodServers(NUM_NODES, cfgBuilder); waitForClusterToForm(); for (int i = 0; i < NUM_NODES; i++) { server(i).addCacheEventConverterFactory("godzilla-growing-converter-factory", new CustomConverterFactory()); } remoteCache = client(0).getCache(); } private ConfigurationBuilder getConfigurationBuilder() { ConfigurationBuilder builder = getDefaultClusteredCacheConfig(CacheMode.REPL_SYNC, false); //playing with OOM - weird things might happen when JVM will struggle for life builder.clustering().remoteTimeout(5, TimeUnit.MINUTES); return hotRodCacheConfiguration(builder); } public void testOOM() throws Throwable { try { log.debugf("Max direct memory is: %s%n", humanReadableByteCount(maxDirectMemory0(), false)); logDirectMemory(log); byte[] babyGodzilla = new byte[]{13}; for (int i = 0; i < NUM_ENTRIES; i++) { remoteCache.put(i, babyGodzilla); } log.debugf("ADDED %d BABY GODZILLAS\n", NUM_ENTRIES); log.debugf("ADDING LISTENER!"); CountDownLatch latch = new CountDownLatch(1); ClientEntryListener listener = new ClientEntryListener(latch); logDirectMemory(log); remoteCache.addClientListener(listener); // the world ends here log.debugf("ADDED LISTENER"); logDirectMemory(log); latch.await(1, TimeUnit.MINUTES); remoteCache.removeClientListener(listener); assertEquals(NUM_ENTRIES, listener.eventCount); } catch(Throwable t) { log.debug("Exception reported, direct memory usage is:", t); logDirectMemory(log); throw t; } } private static void logDirectMemory(Log log) { log.debugf("Direct memory: used=%s, capacity=%s%n", humanReadableByteCount(DIRECT_POOL.getMemoryUsed(), false), humanReadableByteCount(DIRECT_POOL.getTotalCapacity(), false)); } private static BufferPoolMXBean getDirectMemoryPool() { List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class); BufferPoolMXBean directPool = null; for (BufferPoolMXBean pool : pools) { if (pool.getName().equals("direct")) directPool = pool; } return directPool; } private static long maxDirectMemory0() { try { // Try to get from sun.misc.VM.maxDirectMemory() which should be most accurate. Class<?> vmClass = Class.forName("sun.misc.VM", true, ClassLoader.getSystemClassLoader()); Method m = vmClass.getDeclaredMethod("maxDirectMemory"); return ((Number) m.invoke(null)).longValue(); } catch (Throwable t) { // Ignore return -1; } } private static String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } @ClientListener(converterFactoryName = "godzilla-growing-converter-factory", useRawData = true, includeCurrentState = true) private static class ClientEntryListener { private static final Log log = LogFactory.getLog(ClientEntryListener.class); private final CountDownLatch latch; int eventCount = 0; ClientEntryListener(CountDownLatch latch) { this.latch = latch; } @ClientCacheEntryCreated @SuppressWarnings("unused") public void handleClientCacheEntryCreatedEvent(ClientCacheEntryCustomEvent event) { int length = ((byte[]) event.getEventData()).length; eventCount++; log.debugf("ClientEntryListener.handleClientCacheEntryCreatedEvent eventCount=%d length=%d\n", eventCount, length); logDirectMemory(log); if (eventCount == NUM_ENTRIES) latch.countDown(); try { Thread.sleep(SLEEP_TIME); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } @NamedFactory(name = "godzilla-growing-converter-factory") private static class CustomConverterFactory implements CacheEventConverterFactory { @Override public <K, V, C> CacheEventConverter<K, V, C> getConverter(Object[] params) { return new CustomConverter<K, V, C>(); } static class CustomConverter<K, V, C> implements CacheEventConverter<K, V, C>, Serializable, ExternalPojo { @Override public C convert(Object key, Object previousValue, Metadata previousMetadata, Object value, Metadata metadata, EventType eventType) { // all baby godzillas get converted to full grown godzillas return (C) GODZILLA; } } } private static byte[] makeGodzilla() { // this will not fit through the keyhole byte[] godzilla = new byte[1024 * 1024 * 42]; Arrays.fill(godzilla, (byte) 13); return godzilla; } }