package org.infinispan.container.versioning; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.fail; import javax.transaction.HeuristicRollbackException; import javax.transaction.RollbackException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.infinispan.AdvancedCache; import org.infinispan.Cache; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.context.Flag; import org.infinispan.distribution.MagicKey; import org.infinispan.persistence.dummy.DummyInMemoryStoreConfigurationBuilder; import org.infinispan.test.MultipleCacheManagersTest; import org.infinispan.test.fwk.TestCacheManagerFactory; import org.infinispan.transaction.LockingMode; import org.infinispan.util.concurrent.IsolationLevel; import org.testng.annotations.Test; @Test(testName = "container.versioning.AbstractClusteredWriteSkewTest", groups = "functional") public abstract class AbstractClusteredWriteSkewTest extends MultipleCacheManagersTest { private static final String PASSIVATION_CACHE = "passivation-cache"; private static final int MAX_ENTRIES = 2; public final void testPutIgnoreReturnValueOnNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.PUT, false); } public final void testPutIgnoreReturnValueOnNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.PUT, false); } public final void testPutIgnoreReturnValueNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.PUT, true); } public final void testPutIgnoreReturnValueNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.PUT, true); } public final void testRemoveIgnoreReturnValueOnNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.REMOVE, false); } public final void testRemoveIgnoreReturnValueOnNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.REMOVE, false); } public final void testRemoveIgnoreReturnValueNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.REMOVE, true); } public final void testRemoveIgnoreReturnValueNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.REMOVE, true); } public final void testReplaceIgnoreReturnValueOnNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.REPLACE, false); } public final void testReplaceIgnoreReturnValueOnNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.REPLACE, false); } public final void testReplaceIgnoreReturnValueNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.REPLACE, true); } public final void testReplaceIgnoreReturnValueNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.REPLACE, true); } public final void testPutIfAbsentIgnoreReturnValueOnNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.CONDITIONAL_PUT, false); } public final void testPutIfAbsentIgnoreReturnValueOnNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.CONDITIONAL_PUT, false); } public final void testPutIfAbsentIgnoreReturnValueNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.CONDITIONAL_PUT, true); } public final void testPutIfAbsentIgnoreReturnValueNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.CONDITIONAL_PUT, true); } public final void testConditionalRemoveIgnoreReturnValueOnNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.CONDITIONAL_REMOVE, false); } public final void testConditionalRemoveIgnoreReturnValueOnNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.CONDITIONAL_REMOVE, false); } public final void testConditionalRemoveIgnoreReturnValueNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.CONDITIONAL_REMOVE, true); } public final void testConditionalRemoveIgnoreReturnValueNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.CONDITIONAL_REMOVE, true); } public final void testConditionalReplaceIgnoreReturnValueOnNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.CONDITIONAL_REPLACE, false); } public final void testConditionalReplaceIgnoreReturnValueOnNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.CONDITIONAL_REPLACE, false); } public final void testConditionalReplaceIgnoreReturnValueNonExistingKey() throws Exception { doIgnoreReturnValueTest(true, Operation.CONDITIONAL_REPLACE, true); } public final void testConditionalReplaceIgnoreReturnValueNonExistingKeyOnNonOwner() throws Exception { doIgnoreReturnValueTest(false, Operation.CONDITIONAL_REPLACE, true); } public final void testPutWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.PUT, false); } public final void testPutFailWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.PUT, true); } public final void testPutWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.PUT, false); } public final void testPutFailWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.PUT, true); } public final void testRemoveWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.REMOVE, false); } public final void testRemoveFailWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.REMOVE, true); } public final void testRemoveWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.REMOVE, false); } public final void testRemoveFailWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.REMOVE, true); } public final void testReplaceWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.REPLACE, false); } public final void testReplaceFailWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.REPLACE, true); } public final void testReplaceWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.REPLACE, false); } public final void testReplaceFailWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.REPLACE, true); } public final void testConditionalPutWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.CONDITIONAL_PUT, false); } public final void testConditionalPutFailWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.CONDITIONAL_PUT, true); } public final void testConditionalPutWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.CONDITIONAL_PUT, false); } public final void testConditionalPutFailWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.CONDITIONAL_PUT, true); } public final void testConditionalRemoveWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.CONDITIONAL_REMOVE, false); } public final void testConditionalRemoveFailWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.CONDITIONAL_REMOVE, true); } public final void testConditionalRemoveWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.CONDITIONAL_REMOVE, false); } public final void testConditionalRemoveFailWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.CONDITIONAL_REMOVE, true); } public final void testConditionalReplaceWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.CONDITIONAL_REPLACE, false); } public final void testConditionalReplaceFailWriteSkewWithPassivation() throws Exception { doTestWriteSkewWithPassivation(true, Operation.CONDITIONAL_REPLACE, true); } public final void testConditionalReplaceWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.CONDITIONAL_REPLACE, false); } public final void testConditionalReplaceFailWriteSkewWithPassivationOnNonOwner() throws Exception { doTestWriteSkewWithPassivation(false, Operation.CONDITIONAL_REPLACE, true); } public final void testIgnorePreviousValueAndReadOnPrimary() throws Exception { doTestIgnoreReturnValueAndRead(false); } public final void testIgnorePreviousValueAndReadOnNonOwner() throws Exception { doTestIgnoreReturnValueAndRead(true); } protected void doTestIgnoreReturnValueAndRead(boolean executeOnPrimaryOwner) throws Exception { final Object key = new MagicKey("ignore-previous-value", cache(0)); final AdvancedCache<Object, Object> c = executeOnPrimaryOwner ? advancedCache(0) : advancedCache(1); final TransactionManager tm = executeOnPrimaryOwner ? tm(0) : tm(1); for (Cache cache : caches()) { assertNull("wrong initial value for " + address(cache) + ".", cache.get(key)); } c.put("k", "init"); tm.begin(); c.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES).put("k", "v1"); assertEquals("v1", c.put("k", "v2")); Transaction tx = tm.suspend(); assertEquals("init", c.put("k", "other")); tm.resume(tx); tm.commit(); } @Override protected void createCacheManagers() throws Throwable { ConfigurationBuilder builder = defaultConfigurationBuilder(); decorate(builder); createCluster(builder, clusterSize()); waitForClusterToForm(); builder = defaultConfigurationBuilder(); builder.persistence().passivation(true).addStore(DummyInMemoryStoreConfigurationBuilder.class); builder.eviction().maxEntries(MAX_ENTRIES); decorate(builder); defineConfigurationOnAllManagers(PASSIVATION_CACHE, builder); waitForClusterToForm(PASSIVATION_CACHE); } protected void decorate(ConfigurationBuilder builder) { // No-op } protected abstract CacheMode getCacheMode(); protected abstract int clusterSize(); private void doTestWriteSkewWithPassivation(boolean executeOnPrimaryOwner, Operation operation, boolean fail) throws Exception { final AdvancedCache<Object, Object> primaryOwner = advancedCache(0, PASSIVATION_CACHE); final Object key = new MagicKey("ignore-return-value", primaryOwner); final AdvancedCache<Object, Object> executeOnCache = executeOnPrimaryOwner ? advancedCache(0, PASSIVATION_CACHE) : advancedCache(1, PASSIVATION_CACHE); final TransactionManager tm = executeOnPrimaryOwner ? tm(0, PASSIVATION_CACHE) : tm(1, PASSIVATION_CACHE); for (Cache cache : caches(PASSIVATION_CACHE)) { assertNull("wrong initial value for " + address(cache) + ".", cache.get(key)); } switch (operation) { case CONDITIONAL_REMOVE: case CONDITIONAL_REPLACE: case REPLACE: log.debug("Initialize key"); primaryOwner.put(key, "init"); } log.debugf("Start the transaction and perform a %s operation", operation); tm.begin(); switch (operation) { case PUT: executeOnCache.put(key, "v1"); break; case REMOVE: executeOnCache.remove(key); break; case REPLACE: executeOnCache.replace(key, "v1"); break; case CONDITIONAL_PUT: executeOnCache.putIfAbsent(key, "v1"); break; case CONDITIONAL_REMOVE: executeOnCache.remove(key, "init"); break; case CONDITIONAL_REPLACE: executeOnCache.replace(key, "init", "v1"); break; default: tm.rollback(); fail("Unknown operation " + operation); } final Transaction tx = tm.suspend(); if (fail) { primaryOwner.put(key, "v2"); } primaryOwner.evict(key); log.debugf("It is going to try to commit the suspended transaction"); try { tm.resume(tx); tm.commit(); if (fail) { fail("Rollback expected!"); } } catch (RollbackException | HeuristicRollbackException e) { if (!fail) { fail("Rollback *not* expected!"); } //no-op } Object finalValue; switch (operation) { case REMOVE: case CONDITIONAL_REMOVE: finalValue = fail ? "v2" : null; break; default: finalValue = fail ? "v2" : "v1"; } log.debugf("So far so good. Check the key final value"); assertNoTransactions(); for (Cache cache : caches(PASSIVATION_CACHE)) { assertEquals("wrong final value for " + address(cache) + ".", finalValue, cache.get(key)); } } private ConfigurationBuilder defaultConfigurationBuilder() { ConfigurationBuilder builder = TestCacheManagerFactory.getDefaultCacheConfiguration(true); builder.clustering().cacheMode(getCacheMode()) .locking().isolationLevel(IsolationLevel.REPEATABLE_READ) .transaction().lockingMode(LockingMode.OPTIMISTIC); return builder; } private void doIgnoreReturnValueTest(boolean executeOnPrimaryOwner, Operation operation, boolean initKey) throws Exception { final Object key = new MagicKey("ignore-return-value", cache(0)); final AdvancedCache<Object, Object> c = executeOnPrimaryOwner ? advancedCache(0) : advancedCache(1); final TransactionManager tm = executeOnPrimaryOwner ? tm(0) : tm(1); for (Cache cache : caches()) { assertNull("wrong initial value for " + address(cache) + ".", cache.get(key)); } log.debugf("Initialize the key? %s", initKey); if (initKey) { cache(0).put(key, "init"); } Object finalValue = null; boolean rollbackExpected = false; log.debugf("Start the transaction and perform a %s operation", operation); tm.begin(); switch (operation) { case PUT: finalValue = "v1"; rollbackExpected = false; c.withFlags(Flag.IGNORE_RETURN_VALUES).put(key, "v1"); break; case REMOVE: finalValue = null; rollbackExpected = false; c.withFlags(Flag.IGNORE_RETURN_VALUES).remove(key); break; case REPLACE: finalValue = "v2"; rollbackExpected = initKey; c.withFlags(Flag.IGNORE_RETURN_VALUES).replace(key, "v1"); break; case CONDITIONAL_PUT: finalValue = "v2"; rollbackExpected = !initKey; c.withFlags(Flag.IGNORE_RETURN_VALUES).putIfAbsent(key, "v1"); break; case CONDITIONAL_REMOVE: finalValue = "v2"; rollbackExpected = initKey; c.withFlags(Flag.IGNORE_RETURN_VALUES).remove(key, "init"); break; case CONDITIONAL_REPLACE: finalValue = "v2"; rollbackExpected = initKey; c.withFlags(Flag.IGNORE_RETURN_VALUES).replace(key, "init", "v1"); break; default: tm.rollback(); fail("Unknown operation " + operation); } Transaction tx = tm.suspend(); log.debugf("Suspend the transaction and update the key"); c.put(key, "v2"); log.debugf("Checking if all the keys has the same value"); for (Cache cache : caches()) { assertEquals("wrong intermediate value for " + address(cache) + ".", "v2", cache.get(key)); } log.debugf("It is going to try to commit the suspended transaction"); try { tm.resume(tx); tm.commit(); if (rollbackExpected) { fail("Rollback expected!"); } } catch (RollbackException | HeuristicRollbackException e) { if (!rollbackExpected) { fail("Rollback *not* expected!"); } //no-op } log.debugf("So far so good. Check the key final value"); assertNoTransactions(); for (Cache cache : caches()) { assertEquals("wrong final value for " + address(cache) + ".", finalValue, cache.get(key)); } } private enum Operation { PUT, REMOVE, REPLACE, CONDITIONAL_PUT, CONDITIONAL_REMOVE, CONDITIONAL_REPLACE } }