package org.infinispan.api.mvcc.repeatable_read;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Future;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import org.infinispan.Cache;
import org.infinispan.api.mvcc.LockAssert;
import org.infinispan.atomic.AtomicMapLookup;
import org.infinispan.atomic.FineGrainedAtomicMap;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.context.Flag;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.AbstractInfinispanTest;
import org.infinispan.test.Exceptions;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.util.concurrent.IsolationLevel;
import org.infinispan.util.concurrent.locks.LockManager;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@Test(groups = "functional", testName = "api.mvcc.repeatable_read.WriteSkewTest")
public class WriteSkewTest extends AbstractInfinispanTest {
private static final Log log = LogFactory.getLog(WriteSkewTest.class);
protected TransactionManager tm;
protected LockManager lockManager;
protected EmbeddedCacheManager cacheManager;
protected volatile Cache<String, String> cache;
@BeforeClass
public void setUp() {
ConfigurationBuilder configurationBuilder = createConfigurationBuilder();
configurationBuilder.locking().isolationLevel(IsolationLevel.READ_COMMITTED);
// The default cache is NOT write skew enabled.
cacheManager = TestCacheManagerFactory.createCacheManager(configurationBuilder);
configurationBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ);
cacheManager.defineConfiguration("writeSkew", configurationBuilder.build());
}
protected ConfigurationBuilder createConfigurationBuilder() {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder
.transaction()
.transactionMode(TransactionMode.TRANSACTIONAL)
.locking()
.lockAcquisitionTimeout(TestingUtil.shortTimeoutMillis())
.isolationLevel(IsolationLevel.REPEATABLE_READ);
return configurationBuilder;
}
@AfterClass
public void tearDown() {
TestingUtil.killCacheManagers(cacheManager);
cacheManager = null;
cache =null;
lockManager = null;
tm = null;
}
@BeforeMethod
public void postStart() {
cache = cacheManager.getCache("writeSkew");
lockManager = TestingUtil.extractComponentRegistry(cache).getComponent(LockManager.class);
tm = TestingUtil.extractComponentRegistry(cache).getComponent(TransactionManager.class);
}
protected void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException {
tm.commit();
}
protected void assertNoLocks() {
LockAssert.assertNoLocks(lockManager);
}
public void testDontCheckWriteSkew() throws Exception {
// Use the default cache here.
cache = cacheManager.getCache();
lockManager = TestingUtil.extractComponentRegistry(cache).getComponent(LockManager.class);
tm = TestingUtil.extractComponentRegistry(cache).getComponent(TransactionManager.class);
doTest(true);
}
public void testCheckWriteSkew() throws Exception {
doTest(false);
}
/**
* Tests write skew with two concurrent transactions that each execute two put() operations. One put() is done on the
* same key to create a write skew. The second put() is only needed to avoid optimizations done by
* OptimisticLockingInterceptor for single modification transactions and force it reach the code path that triggers
* ISPN-2092.
*/
public void testCheckWriteSkewWithMultipleModifications() throws Exception {
final CountDownLatch latch1 = new CountDownLatch(1);
final CountDownLatch latch2 = new CountDownLatch(1);
final CountDownLatch latch3 = new CountDownLatch(1);
Future<Void> t1 = fork(() -> {
latch1.await();
tm.begin();
try {
try {
cache.get("k1");
cache.put("k1", "v1");
cache.put("k2", "thread 1");
} finally {
latch2.countDown();
}
latch3.await();
Exceptions.expectException(RollbackException.class, this::commit);
} catch (Exception e) {
log.error("Unexpected exception in transaction 1", e);
tm.rollback();
}
return null;
});
Future<Void> t2 = fork(() -> {
latch2.await();
tm.begin();
try {
try {
cache.get("k1");
cache.put("k1", "v2");
cache.put("k3", "thread 2");
commit();
} finally {
latch3.countDown();
}
} catch (Exception e) {
// the TX is most likely rolled back already, but we attempt a rollback just in case it isn't
if (tm.getTransaction() != null) {
try {
tm.rollback();
} catch (SystemException e1) {
log.error("Failed to rollback", e1);
}
}
// Pass the exception to the main thread
throw e;
}
return null;
});
latch1.countDown();
t1.get(10, SECONDS);
t2.get(10, SECONDS);
assertTrue("k1 is expected to be in cache.", cache.containsKey("k1"));
assertEquals("Wrong value for key k1.", "v2", cache.get("k1"));
}
/** Checks that multiple modifications compare the initial value and the write skew does not fire */
public void testNoWriteSkewWithMultipleModifications() throws Exception {
cache.put("k1", "init");
tm.begin();
assertEquals("init", cache.get("k1"));
cache.put("k1", "v2");
cache.put("k2", "v3");
commit();
}
/**
* Verifies we can insert and then remove a value in the same transaction.
* See also ISPN-2075.
*/
public void testDontFailOnImmediateRemoval() throws Exception {
final String key = "testDontOnImmediateRemoval-Key";
tm.begin();
cache.put(key, "testDontOnImmediateRemoval-Value");
assertEquals("Wrong value for key " + key, "testDontOnImmediateRemoval-Value", cache.get(key));
cache.put(key, "testDontOnImmediateRemoval-Value-Second");
cache.remove(key);
commit();
assertFalse("Key " + key + " was not removed as expected.", cache.containsKey(key));
}
/**
* Verifies we can create a new AtomicMap, use it and then remove it while in the same transaction
* See also ISPN-2075.
*/
public void testDontFailOnImmediateRemovalOfAtomicMaps() throws Exception {
final String key = "key1";
final String subKey = "subK";
TestingUtil.withTx(tm, () -> {
FineGrainedAtomicMap<String, String> fineGrainedAtomicMap = AtomicMapLookup.getFineGrainedAtomicMap(cache, key);
fineGrainedAtomicMap.put(subKey, "some value");
fineGrainedAtomicMap = AtomicMapLookup.getFineGrainedAtomicMap(cache, key);
fineGrainedAtomicMap.get(subKey);
fineGrainedAtomicMap.put(subKey, "v");
fineGrainedAtomicMap.put(subKey + 2, "v2");
fineGrainedAtomicMap = AtomicMapLookup.getFineGrainedAtomicMap(cache, key);
Object object = fineGrainedAtomicMap.get(subKey);
assertEquals("Wrong FGAM sub-key value.", "v", object);
AtomicMapLookup.removeAtomicMap(cache, key);
return null;
});
}
public void testNoWriteSkew() throws Exception {
//simplified version of testWriteSkewWithOnlyPut
final String key = "k";
tm.begin();
try {
cache.put(key, "init");
} catch (Exception e) {
tm.setRollbackOnly();
throw e;
} finally {
if (tm.getStatus() == Status.STATUS_ACTIVE) {
commit();
} else {
tm.rollback();
}
}
Cache<String, String> putCache = cache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES);
tm.begin();
putCache.put(key, "v1");
final Transaction tx1 = tm.suspend();
tm.begin();
putCache.put(key, "v2");
final Transaction tx2 = tm.suspend();
tm.begin();
putCache.put(key, "v3");
final Transaction tx3 = tm.suspend();
//the following commits should not fail the write skew check
tm.resume(tx1);
commit();
tm.resume(tx2);
commit();
tm.resume(tx3);
commit();
}
public void testWriteSkew() throws Exception {
//simplified version of testWriteSkewWithOnlyPut
final String key = "k";
tm.begin();
try {
cache.put(key, "init");
} catch (Exception e) {
tm.setRollbackOnly();
throw e;
} finally {
if (tm.getStatus() == Status.STATUS_ACTIVE) {
commit();
} else {
tm.rollback();
}
}
tm.begin();
cache.put(key, "v1");
final Transaction tx1 = tm.suspend();
tm.begin();
cache.put(key, "v2");
final Transaction tx2 = tm.suspend();
tm.begin();
cache.put(key, "v3");
final Transaction tx3 = tm.suspend();
//the first commit should succeed
tm.resume(tx1);
commit();
//the remaining should fail
try {
tm.resume(tx2);
commit();
fail("Transaction should fail!");
} catch (RollbackException e) {
//expected
}
try {
tm.resume(tx3);
commit();
fail("Transaction should fail!");
} catch (RollbackException e) {
//expected
}
}
// Write skew should not fire when the read is based purely on previously written value
// (the first put does not read the value)
// This test actually tests only local write skew check
public void testPreviousValueIgnored() throws Exception {
cache.put("k", "init");
tm.begin();
cache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES).put("k", "v1");
assertEquals("v1", cache.put("k", "v2"));
Transaction tx = tm.suspend();
assertEquals("init", cache.put("k", "other"));
tm.resume(tx);
commit();
}
public void testWriteSkewWithOnlyPut() throws Exception {
tm.begin();
try {
cache.put("k", "init");
} catch (Exception e) {
tm.setRollbackOnly();
throw e;
} finally {
if (tm.getStatus() == Status.STATUS_ACTIVE) commit();
else tm.rollback();
}
int nbWriters = 10;
CyclicBarrier barrier = new CyclicBarrier(nbWriters + 1);
List<Future<Void>> futures = new ArrayList<>(nbWriters);
for (int i = 0; i < nbWriters; i++) {
log.debug("Schedule execution");
Future<Void> future = fork(new EntryWriter(barrier));
futures.add(future);
}
barrier.await(); // wait for all threads to be ready
barrier.await(); // wait for all threads to finish
log.debug("All threads finished, let's shutdown the executor and check whether any exceptions were reported");
for (Future<Void> future : futures) future.get();
}
private void doTest(final boolean disabledWriteSkewCheck) throws Exception {
final String key = "k";
final CountDownLatch w1Signal = new CountDownLatch(1);
final CountDownLatch w2Signal = new CountDownLatch(1);
final CountDownLatch threadSignal = new CountDownLatch(2);
cache.put(key, "v");
Future<Void> w1 = fork(() -> {
tm.begin();
assertEquals("Wrong value in Writer-1 for key " + key + ".", "v", cache.get(key));
threadSignal.countDown();
w1Signal.await();
cache.put(key, "v2");
commit();
return null;
});
Future<Void> w2 = fork(() -> {
tm.begin();
assertEquals("Wrong value in Writer-2 for key " + key + ".", "v", cache.get(key));
threadSignal.countDown();
w2Signal.await();
cache.put(key, "v3");
if (disabledWriteSkewCheck) {
commit();
} else {
Exceptions.expectException(RollbackException.class, this::commit);
}
return null;
});
threadSignal.await(10, SECONDS);
// now. both txs have read.
// let tx1 start writing
w1Signal.countDown();
w1.get(10, SECONDS);
w2Signal.countDown();
w2.get(10, SECONDS);
if (disabledWriteSkewCheck) {
assertEquals("W2 should have overwritten W1's work!", "v3", cache.get(key));
assertNoLocks();
} else {
assertEquals("W2 should *not* have overwritten W1's work!", "v2", cache.get(key));
assertNoLocks();
}
}
protected class EntryWriter implements Callable<Void> {
private final CyclicBarrier barrier;
EntryWriter(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public Void call() throws Exception {
try {
log.debug("Wait for all executions paths to be ready to perform calls");
barrier.await();
tm.begin();
try {
cache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES).put("k", "_lockthisplease_");
} catch (Exception e) {
log.error("Unexpected", e);
tm.setRollbackOnly();
throw e;
} finally {
if (tm.getStatus() == Status.STATUS_ACTIVE) commit();
else tm.rollback();
}
return null;
} finally {
log.debug("Wait for all execution paths to finish");
barrier.await();
}
}
}
}