package org.infinispan.api; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; import org.infinispan.commands.read.GetCacheEntryCommand; import org.infinispan.commands.read.GetKeyValueCommand; import org.infinispan.commands.tx.CommitCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.context.InvocationContext; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.distribution.MagicKey; import org.infinispan.interceptors.base.BaseCustomInterceptor; import org.infinispan.interceptors.distribution.VersionedDistributionInterceptor; import org.infinispan.test.MultipleCacheManagersTest; import org.infinispan.transaction.LockingMode; import org.infinispan.util.concurrent.IsolationLevel; import org.infinispan.util.concurrent.ReclosableLatch; import org.testng.annotations.Test; /** * @author Mircea Markus * @since 5.2 */ @Test(groups = "functional", testName = "api.ConditionalOperationsConcurrentWriteSkewTest") public class ConditionalOperationsConcurrentWriteSkewTest extends MultipleCacheManagersTest { private static final int NODES_NUM = 3; private final CacheMode mode = CacheMode.DIST_SYNC; protected LockingMode lockingMode = LockingMode.OPTIMISTIC; protected boolean writeSkewCheck; protected boolean transactional; public ConditionalOperationsConcurrentWriteSkewTest() { transactional = true; writeSkewCheck = true; } @Override protected void createCacheManagers() throws Throwable { ConfigurationBuilder dcc = getDefaultClusteredCacheConfig(mode, true); dcc.transaction().lockingMode(lockingMode); if (writeSkewCheck) { dcc.transaction().locking().isolationLevel(IsolationLevel.REPEATABLE_READ); } createCluster(dcc, NODES_NUM); waitForClusterToForm(); } public void testSimpleConcurrentReplace() throws Exception { doSimpleConcurrentTest(Operation.REPLACE); } public void testSimpleConcurrentPut() throws Exception { doSimpleConcurrentTest(Operation.PUT); } public void testSimpleConcurrentRemove() throws Exception { doSimpleConcurrentTest(Operation.REMOVE); } private void doSimpleConcurrentTest(final Operation operation) throws Exception { //default owners are 2 assertEquals("Wrong number of owner. Please change the configuration", 2, cache(0).getCacheConfiguration().clustering().hash().numOwners()); final Object key = new MagicKey(cache(0), cache(1)); try { CommandInterceptorController controller = injectController(cache(1)); if (operation == Operation.REMOVE || operation == Operation.REPLACE) { cache(0).put(key, "v1"); } controller.awaitCommit.close(); controller.blockCommit.close(); final Future<Boolean> tx1 = fork(() -> { tm(1).begin(); cache(1).put(key, "tx1"); tm(1).commit(); return Boolean.TRUE; }); //await tx1 commit on cache1... the commit will be blocked! //tx1 has already committed in cache(0) but not in cache(1) //we block the remote get in order to force the tx2 to read the most recent value from cache(0) controller.awaitCommit.await(30, TimeUnit.SECONDS); controller.blockRemoteGet.close(); final Future<Boolean> tx2 = fork(() -> { tm(2).begin(); switch (operation) { case REMOVE: cache(2).remove(key, "v1"); break; case REPLACE: cache(2).replace(key, "v1", "tx2"); break; case PUT: cache(2).putIfAbsent(key, "tx2"); break; } tm(2).commit(); return Boolean.TRUE; }); //tx2 will not prepare the transaction remotely since the operation should fail. assertTrue("Tx2 has not finished", tx2.get(20, TimeUnit.SECONDS)); //let everything run normally controller.reset(); assertTrue("Tx1 has not finished", tx1.get(20, TimeUnit.SECONDS)); //check if no transactions are active assertNoTransactions(); for (Cache cache : caches()) { assertEquals("Wrong value for cache " + address(cache), "tx1", cache.get(key)); } } finally { removeController(cache(1)); } } private CommandInterceptorController injectController(Cache cache) { CommandInterceptorController commandInterceptorController = new CommandInterceptorController(); cache.getAdvancedCache().addInterceptorBefore(commandInterceptorController, VersionedDistributionInterceptor.class); return commandInterceptorController; } private void removeController(Cache cache) { cache.getAdvancedCache().removeInterceptor(CommandInterceptorController.class); } private enum Operation { PUT, REPLACE, REMOVE } private class CommandInterceptorController extends BaseCustomInterceptor { private final ReclosableLatch blockRemoteGet = new ReclosableLatch(true); private final ReclosableLatch blockCommit = new ReclosableLatch(true); private final ReclosableLatch awaitPrepare = new ReclosableLatch(true); private final ReclosableLatch awaitCommit = new ReclosableLatch(true); @Override public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable { try { return invokeNextInterceptor(ctx, command); } finally { getLog().debug("visit GetKeyValueCommand"); if (!ctx.isOriginLocal() && blockRemoteGet != null) { getLog().debug("Remote Get Received... blocking..."); blockRemoteGet.await(30, TimeUnit.SECONDS); } } } @Override public Object visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command) throws Throwable { try { return invokeNextInterceptor(ctx, command); } finally { getLog().debug("visit GetCacheEntryCommand"); if (!ctx.isOriginLocal() && blockRemoteGet != null) { getLog().debug("Remote Get Received... blocking..."); blockRemoteGet.await(30, TimeUnit.SECONDS); } } } @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { try { return invokeNextInterceptor(ctx, command); } finally { getLog().debug("visit Prepare"); if (awaitPrepare != null) { getLog().debug("Prepare Received... unblocking"); awaitPrepare.open(); } } } @Override public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable { try { return invokeNextInterceptor(ctx, command); } finally { if (ctx.isOriginLocal()) { getLog().debug("visit Commit"); if (awaitCommit != null) { getLog().debug("Commit Received... unblocking..."); awaitCommit.open(); } if (blockCommit != null) { getLog().debug("Commit Received... blocking..."); blockCommit.await(30, TimeUnit.SECONDS); } } } } public void reset() { if (blockCommit != null) { blockCommit.open(); } if (blockRemoteGet != null) { blockRemoteGet.open(); } if (awaitPrepare != null) { awaitPrepare.open(); } if (awaitCommit != null) { awaitCommit.open(); } } } }