package org.infinispan.stats; import static org.infinispan.test.TestingUtil.k; import static org.infinispan.test.TestingUtil.wrapInboundInvocationHandler; import static org.testng.AssertJUnit.assertNull; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; import org.infinispan.commands.ReplicableCommand; import org.infinispan.commands.remote.CacheRpcCommand; import org.infinispan.commands.remote.SingleRpcCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.tx.VersionedPrepareCommand; import org.infinispan.commands.tx.totalorder.TotalOrderNonVersionedPrepareCommand; import org.infinispan.commands.tx.totalorder.TotalOrderVersionedPrepareCommand; import org.infinispan.commands.write.ClearCommand; import org.infinispan.commands.write.PutKeyValueCommand; import org.infinispan.commands.write.PutMapCommand; import org.infinispan.commands.write.RemoveCommand; import org.infinispan.commands.write.ReplaceCommand; import org.infinispan.commands.write.WriteCommand; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.InterceptorConfiguration; import org.infinispan.interceptors.AsyncInterceptorChain; import org.infinispan.remoting.inboundhandler.DeliverOrder; import org.infinispan.remoting.inboundhandler.PerCacheInboundInvocationHandler; import org.infinispan.remoting.inboundhandler.Reply; import org.infinispan.remoting.transport.Address; import org.infinispan.stats.wrappers.ExtendedStatisticInterceptor; import org.infinispan.test.MultipleCacheManagersTest; import org.infinispan.transaction.TransactionProtocol; import org.infinispan.util.concurrent.IsolationLevel; import org.testng.AssertJUnit; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** * @author Pedro Ruivo * @since 6.0 */ @Test(groups = "functional") public abstract class BaseClusteredExtendedStatisticTest extends MultipleCacheManagersTest { private static final int NUM_NODES = 2; private static final String VALUE_1 = "value_1"; private static final String VALUE_2 = "value_2"; private static final String VALUE_3 = "value_3"; private static final String VALUE_4 = "value_4"; private final List<ControlledPerCacheInboundInvocationHandler> inboundHandlerList = new ArrayList<>(NUM_NODES); private final CacheMode mode; private final boolean totalOrder; protected BaseClusteredExtendedStatisticTest(CacheMode mode, boolean totalOrder) { this.mode = mode; this.totalOrder = totalOrder; } protected static Collection<Address> getOwners(Cache<?, ?> cache, Object key) { return new ArrayList<>(cache.getAdvancedCache().getDistributionManager().getCacheTopology().getWriteOwners(key)); } protected static Collection<Address> getOwners(Cache<?, ?> cache, Collection<Object> keys) { return new ArrayList<>(cache.getAdvancedCache().getDistributionManager().getCacheTopology().getWriteOwners(keys)); } public void testPut(Method method) throws InterruptedException { final String key1 = k(method, 1); final String key2 = k(method, 2); final String key3 = k(method, 3); assertEmpty(key1, key2, key3); put(0, key1, VALUE_1); assertCacheValue(key1, VALUE_1); Map<Object, Object> map = new HashMap<>(); map.put(key2, VALUE_2); map.put(key3, VALUE_3); cache(0).putAll(map); awaitPutMap(0, map.keySet()); assertCacheValue(key1, VALUE_1); assertCacheValue(key2, VALUE_2); assertCacheValue(key3, VALUE_3); assertNoTransactions(); assertNoTxStats(); } public void testRemove(Method method) throws InterruptedException { final String key1 = k(method, 1); assertEmpty(key1); put(1, key1, VALUE_1); assertCacheValue(key1, VALUE_1); remove(0, key1); assertCacheValue(key1, null); put(0, key1, VALUE_1); assertCacheValue(key1, VALUE_1); remove(0, key1); assertCacheValue(key1, null); assertNoTransactions(); assertNoTxStats(); } public void testPutIfAbsent(Method method) throws InterruptedException { final String key1 = k(method, 1); final String key2 = k(method, 2); assertEmpty(key1, key2); put(1, key1, VALUE_1); assertCacheValue(key1, VALUE_1); //read-only tx cache(0).putIfAbsent(key1, VALUE_2); assertCacheValue(key1, VALUE_1); put(1, key1, VALUE_3); assertCacheValue(key1, VALUE_3); //read-only tx cache(0).putIfAbsent(key1, VALUE_4); assertCacheValue(key1, VALUE_3); putIfAbsent(0, key2, VALUE_1); assertCacheValue(key2, VALUE_1); assertNoTransactions(); assertNoTxStats(); } public void testRemoveIfPresent(Method method) throws InterruptedException { final String key1 = k(method, 1); assertEmpty(key1); put(0, key1, VALUE_1); assertCacheValue(key1, VALUE_1); put(1, key1, VALUE_2); assertCacheValue(key1, VALUE_2); //read-only tx cache(0).remove(key1, VALUE_1); assertCacheValue(key1, VALUE_2); remove(0, key1, VALUE_2); assertCacheValue(key1, null); assertNoTransactions(); assertNoTxStats(); } public void testClear(Method method) throws InterruptedException { final String key1 = k(method, 1); assertEmpty(key1); put(0, key1, VALUE_1); assertCacheValue(key1, VALUE_1); cache(0).clear(); awaitClear(0); assertCacheValue(key1, null); assertNoTransactions(); assertNoTxStats(); } public void testReplace(Method method) throws InterruptedException { final String key1 = k(method, 1); assertEmpty(key1); put(1, key1, VALUE_1); assertCacheValue(key1, VALUE_1); AssertJUnit.assertEquals(replace(0, key1, VALUE_2), VALUE_1); assertCacheValue(key1, VALUE_2); put(0, key1, VALUE_3); assertCacheValue(key1, VALUE_3); replace(0, key1, VALUE_3); assertCacheValue(key1, VALUE_3); put(0, key1, VALUE_4); assertCacheValue(key1, VALUE_4); assertNoTransactions(); assertNoTxStats(); } public void testReplaceWithOldVal(Method method) throws InterruptedException { final String key1 = k(method, 1); assertEmpty(key1); put(1, key1, VALUE_1); assertCacheValue(key1, VALUE_1); put(0, key1, VALUE_2); assertCacheValue(key1, VALUE_2); //read-only tx cache(0).replace(key1, VALUE_3, VALUE_4); assertCacheValue(key1, VALUE_2); replace(0, key1, VALUE_2, VALUE_4); assertCacheValue(key1, VALUE_4); assertNoTransactions(); assertNoTxStats(); } public void testRemoveUnexistingEntry(Method method) throws InterruptedException { final String key1 = k(method, 1); assertEmpty(key1); remove(0, key1); assertCacheValue(key1, null); assertNoTransactions(); assertNoTxStats(); } @BeforeMethod(alwaysRun = true) public void resetInboundHandler() { inboundHandlerList.forEach(ControlledPerCacheInboundInvocationHandler::reset); } @Override protected void createCacheManagers() throws Throwable { for (int i = 0; i < 2; ++i) { ConfigurationBuilder builder = getDefaultClusteredCacheConfig(mode, true); if (totalOrder) { builder.transaction().transactionProtocol(TransactionProtocol.TOTAL_ORDER); } builder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ); builder.clustering().hash().numOwners(1); builder.transaction().recovery().disable(); builder.customInterceptors().addInterceptor().interceptor(new ExtendedStatisticInterceptor()) .position(InterceptorConfiguration.Position.FIRST); addClusterEnabledCacheManager(builder); } waitForClusterToForm(); replaceAllPerCacheInboundInvocationHandler(); } protected void assertEmpty(Object... keys) { for (Cache cache : caches()) { for (Object key : keys) { assertNull(cache.get(key)); } } } protected void assertCacheValue(Object key, Object value) { for (int index = 0; index < caches().size(); ++index) { if (mode.isSynchronous()) { assertEquals(index, key, value); } else { assertEventuallyEquals(index, key, value); } } } protected abstract void awaitPut(int cacheIndex, Object key) throws InterruptedException; protected abstract void awaitReplace(int cacheIndex, Object key) throws InterruptedException; protected abstract void awaitRemove(int cacheIndex, Object key) throws InterruptedException; private void awaitClear(int cacheIndex) throws InterruptedException { Set<Address> all = new HashSet<>(cache(cacheIndex).getAdvancedCache().getRpcManager().getMembers()); all.remove(address(cacheIndex)); awaitOperation(Operation.CLEAR, all); } protected abstract void awaitPutMap(int cacheIndex, Collection<Object> keys) throws InterruptedException; protected final void awaitOperation(Operation operation, Collection<Address> owners) throws InterruptedException { for (int i = 0; i < NUM_NODES; ++i) { if (owners.contains(cache(i).getAdvancedCache().getRpcManager().getAddress())) { inboundHandlerList.get(i).await(operation, 30, TimeUnit.SECONDS); } } } private void put(int cacheIndex, Object key, Object value) throws InterruptedException { cache(cacheIndex).put(key, value); awaitPut(cacheIndex, key); } private void putIfAbsent(int cacheIndex, Object key, Object value) throws InterruptedException { cache(cacheIndex).putIfAbsent(key, value); awaitPut(cacheIndex, key); } private Object replace(int cacheIndex, Object key, Object value) throws InterruptedException { Object val = cache(cacheIndex).replace(key, value); awaitReplace(cacheIndex, key); return val; } private Object replace(int cacheIndex, Object key, Object oldValue, Object newValue) throws InterruptedException { Object val = cache(cacheIndex).replace(key, oldValue, newValue); awaitReplace(cacheIndex, key); return val; } private void remove(int cacheIndex, Object key) throws InterruptedException { cache(cacheIndex).remove(key); awaitRemove(cacheIndex, key); } private void remove(int cacheIndex, Object key, Object oldValue) throws InterruptedException { cache(cacheIndex).remove(key, oldValue); awaitRemove(cacheIndex, key); } private void assertNoTxStats() { final ExtendedStatisticInterceptor[] statisticInterceptors = new ExtendedStatisticInterceptor[caches().size()]; for (int i = 0; i < caches().size(); ++i) { statisticInterceptors[i] = getExtendedStatistic(cache(i)); } eventually(() -> { for (ExtendedStatisticInterceptor interceptor : statisticInterceptors) { if (interceptor.getCacheStatisticManager().hasPendingTransactions()) { return false; } } return true; }); } private void assertEquals(int index, Object key, Object value) { AssertJUnit.assertEquals(cache(index).get(key), value); } private ExtendedStatisticInterceptor getExtendedStatistic(Cache<?, ?> cache) { AsyncInterceptorChain interceptorChain = cache.getAdvancedCache().getAsyncInterceptorChain(); ExtendedStatisticInterceptor extendedStatisticInterceptor = interceptorChain.findInterceptorExtending(ExtendedStatisticInterceptor.class); if (extendedStatisticInterceptor != null) { extendedStatisticInterceptor.resetStatistics(); } return extendedStatisticInterceptor; } private void replaceAllPerCacheInboundInvocationHandler() { for (Cache<?, ?> cache : caches()) { inboundHandlerList.add(wrapInboundInvocationHandler(cache, ControlledPerCacheInboundInvocationHandler::new)); } } protected enum Operation { PUT, REMOVE, REPLACE, CLEAR, PUT_MAP } private static class ControlledPerCacheInboundInvocationHandler implements PerCacheInboundInvocationHandler { private final PerCacheInboundInvocationHandler delegate; private final Queue<Operation> operationQueue = new LinkedList<>(); private ControlledPerCacheInboundInvocationHandler(PerCacheInboundInvocationHandler delegate) { this.delegate = delegate; } @Override public void handle(CacheRpcCommand command, Reply reply, DeliverOrder order) { checkCommand(command); delegate.handle(command, reply, order); } private void reset() { synchronized (operationQueue) { operationQueue.clear(); } } private void await(Operation operation, long timeout, TimeUnit timeUnit) throws InterruptedException { final long timeoutNanos = System.nanoTime() + timeUnit.toNanos(timeout); synchronized (operationQueue) { while (operationQueue.peek() != operation && System.nanoTime() - timeoutNanos < 0) { operationQueue.wait(timeUnit.toMillis(timeout)); } AssertJUnit.assertEquals(operation, operationQueue.poll()); } } private void checkCommand(ReplicableCommand cacheRpcCommand) { synchronized (operationQueue) { switch (cacheRpcCommand.getCommandId()) { case PutKeyValueCommand.COMMAND_ID: operationQueue.add(Operation.PUT); break; case ReplaceCommand.COMMAND_ID: operationQueue.add(Operation.REPLACE); break; case RemoveCommand.COMMAND_ID: operationQueue.add(Operation.REMOVE); break; case ClearCommand.COMMAND_ID: operationQueue.add(Operation.CLEAR); break; case PutMapCommand.COMMAND_ID: operationQueue.add(Operation.PUT_MAP); break; case PrepareCommand.COMMAND_ID: case VersionedPrepareCommand.COMMAND_ID: case TotalOrderNonVersionedPrepareCommand.COMMAND_ID: case TotalOrderVersionedPrepareCommand.COMMAND_ID: for (WriteCommand command : ((PrepareCommand) cacheRpcCommand).getModifications()) { checkCommand(command); } break; case SingleRpcCommand.COMMAND_ID: checkCommand(((SingleRpcCommand) cacheRpcCommand).getCommand()); break; } operationQueue.notifyAll(); } } } }