package org.infinispan.statetransfer; import static org.infinispan.test.concurrent.StateSequencerUtil.advanceOnInterceptor; import static org.infinispan.test.concurrent.StateSequencerUtil.matchCommand; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; import java.util.Arrays; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import javax.transaction.RollbackException; import org.infinispan.Cache; import org.infinispan.commands.tx.CommitCommand; import org.infinispan.commands.tx.RollbackCommand; import org.infinispan.commands.tx.VersionedCommitCommand; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.test.MultipleCacheManagersTest; import org.infinispan.test.TestingUtil; import org.infinispan.test.concurrent.StateSequencer; import org.infinispan.test.fwk.CleanupAfterMethod; import org.infinispan.transaction.TransactionMode; import org.infinispan.util.ControlledConsistentHashFactory; import org.infinispan.util.concurrent.locks.LockManager; import org.testng.annotations.Test; /** * Test that a commit command that has timed out on a backup owner cannot write entries after the locks have been * released on the primary owner. */ @Test(groups = "functional", testName = "statetransfer.CommitTimeoutTest") @CleanupAfterMethod public class CommitTimeoutTest extends MultipleCacheManagersTest { private static final String TEST_KEY = "key"; private static final String TX1_VALUE = "value1"; private static final String TX2_VALUE = "value2"; @Override protected void createCacheManagers() throws Throwable { ControlledConsistentHashFactory consistentHashFactory = new ControlledConsistentHashFactory(1, 2); ConfigurationBuilder builder = new ConfigurationBuilder(); builder.clustering().cacheMode(CacheMode.DIST_SYNC); builder.clustering().remoteTimeout(2000); builder.clustering().hash().numSegments(1).consistentHashFactory(consistentHashFactory); builder.transaction().transactionMode(TransactionMode.TRANSACTIONAL); addClusterEnabledCacheManager(builder); addClusterEnabledCacheManager(builder); addClusterEnabledCacheManager(builder); waitForClusterToForm(); } public void testCommitDoesntWriteAfterRollback() throws Exception { // Start a tx on A: put(k, v1), owners(k) = [B (primary) and C (backup)] // Block the commit on C so that it times out // Wait for the rollback command to be executed on B and C, and for the tx to end // Check that locks are released on B // Start another transaction on A: put(k, v2) with the same key // Check that the new transaction writes successfully // Allow the commit to proceed on C // Check that k=v2 everywhere StateSequencer sequencer = new StateSequencer(); sequencer.logicalThread("tx1", "tx1:begin", "tx1:block_commit_on_backup", "tx1:after_rollback_on_primary", "tx1:after_rollback_on_backup", "tx1:resume_commit_on_backup", "tx1:after_commit_on_backup", "tx1:check"); sequencer.logicalThread("tx2", "tx2:begin", "tx2:end"); sequencer.order("tx1:after_rollback_on_backup", "tx2:begin", "tx2:end", "tx1:resume_commit_on_backup"); advanceOnInterceptor(sequencer, cache(2), StateTransferInterceptor.class, matchCommand(VersionedCommitCommand.class).matchCount(0).build()) .before("tx1:block_commit_on_backup", "tx1:resume_commit_on_backup").after("tx1:after_commit_on_backup"); advanceOnInterceptor(sequencer, cache(1), StateTransferInterceptor.class, matchCommand(RollbackCommand.class).build()) .after("tx1:after_rollback_on_primary"); advanceOnInterceptor(sequencer, cache(2), StateTransferInterceptor.class, matchCommand(RollbackCommand.class).build()) .after("tx1:after_rollback_on_backup"); assertEquals(Arrays.asList(address(1), address(2)), advancedCache(0).getDistributionManager().locate(TEST_KEY)); sequencer.advance("tx1:begin"); tm(0).begin(); cache(0).put(TEST_KEY, TX1_VALUE); try { tm(0).commit(); } catch (RollbackException e) { log.debugf("Commit timed out as expected", e); } sequencer.advance("tx2:begin"); LockManager lockManager1 = TestingUtil.extractLockManager(cache(1)); assertFalse(lockManager1.isLocked(TEST_KEY)); tm(0).begin(); cache(0).put(TEST_KEY, TX2_VALUE); tm(0).commit(); checkValue(); sequencer.advance("tx2:end"); sequencer.advance("tx1:check"); checkValue(); } private void checkValue() { for (Cache cache : caches()) { assertEquals(TX2_VALUE, cache.get(TEST_KEY)); } } @Test(enabled = false, description = "Fix for this scenario is not implemented yet - rollback is asynchronous") public void testCommitDoesntWriteAfterTxEnd() throws Exception { // Start a tx on A: put(k, v1), owners(k) = [B (primary) and C (backup)] // Block the commit on C so that it times out // Wait for the rollback command to be executed on B and block before it executes on C // Check that k is still locked on B // Allow the commit to proceed on C // Allow the rollback to proceed on C // Check that k=v1 everywhere // Check that locks are released on B final StateSequencer sequencer = new StateSequencer(); sequencer.logicalThread("tx1", "tx1:begin", "tx1:block_commit_on_backup", "tx1:after_rollback_on_primary", "tx1:block_rollback_on_backup", "tx1:resume_commit_on_backup", "tx1:after_commit_on_backup", "tx1:resume_rollback_on_backup", "tx1:after_rollback_on_backup", "tx1:check"); advanceOnInterceptor(sequencer, cache(2), StateTransferInterceptor.class, matchCommand(CommitCommand.class).matchCount(0).build()) .before("tx1:block_commit_on_backup", "tx1:resume_commit_on_backup").after("tx1:after_commit_on_backup"); advanceOnInterceptor(sequencer, cache(1), StateTransferInterceptor.class, matchCommand(RollbackCommand.class).build()) .after("tx1:after_rollback_on_primary"); advanceOnInterceptor(sequencer, cache(2), StateTransferInterceptor.class, matchCommand(RollbackCommand.class).build()) .before("tx1:block_rollback_on_backup").after("tx1:after_rollback_on_backup"); assertEquals(Arrays.asList(address(1), address(2)), advancedCache(0).getDistributionManager().locate(TEST_KEY)); Future<Object> lockCheckFuture = fork(() -> { sequencer.enter("tx1:resume_rollback_on_backup"); try { assertTrue(TestingUtil.extractLockManager(cache(1)).isLocked(TEST_KEY)); } finally { sequencer.exit("tx1:resume_rollback_on_backup"); } return null; }); sequencer.advance("tx1:begin"); tm(0).begin(); cache(0).put(TEST_KEY, TX1_VALUE); tm(0).commit(); sequencer.advance("tx1:check"); assertFalse(TestingUtil.extractLockManager(cache(1)).isLocked(TEST_KEY)); lockCheckFuture.get(10, TimeUnit.SECONDS); } }