/*
* JBoss, Home of Professional Open Source
* Copyright 2009 Red Hat Inc. and/or its affiliates and other
* contributors as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.infinispan.api.mvcc.repeatable_read;
import org.infinispan.Cache;
import org.infinispan.CacheException;
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.configuration.cache.VersioningScheme;
import org.infinispan.context.InvocationContextContainer;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.AbstractInfinispanTest;
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.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
@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 InvocationContextContainer icc;
protected EmbeddedCacheManager cacheManager;
protected volatile Cache cache;
@BeforeTest
public void setUp() {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder
.transaction()
.transactionMode(TransactionMode.TRANSACTIONAL)
.locking()
.lockAcquisitionTimeout(3000)
.isolationLevel(IsolationLevel.REPEATABLE_READ);
// The default cache is NOT write skew enabled.
cacheManager = TestCacheManagerFactory.createCacheManager(configurationBuilder);
configurationBuilder.locking().writeSkewCheck(true).versioning().enable().scheme(VersioningScheme.SIMPLE);
cacheManager.defineConfiguration("writeSkew", configurationBuilder.build());
}
@AfterTest
public void tearDown() {
TestingUtil.killCacheManagers(cacheManager);
cacheManager = null;
cache =null;
lockManager = null;
tm = null;
icc = null;
}
private void postStart() {
lockManager = TestingUtil.extractComponentRegistry(cache).getComponent(LockManager.class);
icc = TestingUtil.extractComponentRegistry(cache).getComponent(InvocationContextContainer.class);
tm = TestingUtil.extractComponentRegistry(cache).getComponent(TransactionManager.class);
}
protected void assertNoLocks() {
LockAssert.assertNoLocks(lockManager, icc);
}
private void setCacheWithWriteSkewCheck() {
cache = cacheManager.getCache("writeSkew");
}
private void setCacheWithoutWriteSkewCheck() {
// Use the default cache here.
cache = cacheManager.getCache();
}
public void testDontCheckWriteSkew() throws Exception {
setCacheWithoutWriteSkewCheck();
postStart();
doTest(true);
}
public void testCheckWriteSkew() throws Exception {
setCacheWithWriteSkewCheck();
postStart();
doTest(false);
}
/**
* Verifies we can insert and then remove a value in the same transaction.
* See also ISPN-2075.
*/
public void testDontFailOnImmediateRemoval() throws Exception {
setCacheWithWriteSkewCheck();
postStart();
tm.begin();
cache.put("testDontOnImmediateRemoval-Key", "testDontOnImmediateRemoval-Value");
Assert.assertEquals(cache.get("testDontOnImmediateRemoval-Key"), "testDontOnImmediateRemoval-Value");
cache.put("testDontOnImmediateRemoval-Key", "testDontOnImmediateRemoval-Value-Second");
cache.remove("testDontOnImmediateRemoval-Key");
tm.commit();
}
/**
* 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 {
setCacheWithWriteSkewCheck();
postStart();
final String key = "key1";
final String subKey = "subK";
tm.begin();
FineGrainedAtomicMap 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);
Assert.assertEquals( "v", object);
AtomicMapLookup.removeAtomicMap(cache, key);
tm.commit();
}
public void testWriteSkewWithOnlyPut() throws Exception {
setCacheWithWriteSkewCheck();
postStart();
tm.begin();
try {
cache.put("k", "init");
} catch (Exception e) {
tm.setRollbackOnly();
throw e;
} finally {
if (tm.getStatus() == Status.STATUS_ACTIVE) tm.commit();
else tm.rollback();
}
int nbWriters = 10;
CyclicBarrier barrier = new CyclicBarrier(nbWriters + 1);
List<Future<Void>> futures = new ArrayList<Future<Void>>(nbWriters);
ExecutorService executorService = Executors.newCachedThreadPool(new ThreadFactory() {
volatile int i = 0;
@Override
public Thread newThread(Runnable r) {
int ii = i++;
return new Thread(r, "EntryWriter-" + ii + ", WriteSkewTest");
}
});
try {
for (int i = 0; i < nbWriters; i++) {
log.debug("Schedule execution");
Future<Void> future = executorService.submit(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();
} finally {
executorService.shutdownNow();
}
}
private void doTest(final boolean allowWriteSkew) throws Exception {
cache.put("k", "v");
final Set<Exception> w1exceptions = new HashSet<Exception>();
final Set<Exception> w2exceptions = new HashSet<Exception>();
final CountDownLatch w1Signal = new CountDownLatch(1);
final CountDownLatch w2Signal = new CountDownLatch(1);
final CountDownLatch threadSignal = new CountDownLatch(2);
Thread w1 = new Thread("Writer-1, WriteSkewTest") {
@Override
public void run() {
boolean didCoundDown = false;
try {
tm.begin();
assert "v".equals(cache.get("k"));
threadSignal.countDown();
didCoundDown = true;
w1Signal.await();
cache.put("k", "v2");
tm.commit();
}
catch (Exception e) {
w1exceptions.add(e);
}
finally {
if (!didCoundDown) threadSignal.countDown();
}
}
};
Thread w2 = new Thread("Writer-2, WriteSkewTest") {
@Override
public void run() {
boolean didCoundDown = false;
try {
tm.begin();
assert "v".equals(cache.get("k"));
threadSignal.countDown();
didCoundDown = true;
w2Signal.await();
cache.put("k", "v3");
tm.commit();
}
catch (Exception e) {
w2exceptions.add(e);
// the exception will be thrown when doing a cache.put(). We should make sure we roll back the tx to release locks.
if (!allowWriteSkew) {
try {
tm.rollback();
}
catch (SystemException e1) {
// do nothing.
}
}
}
finally {
if (!didCoundDown) threadSignal.countDown();
}
}
};
w1.start();
w2.start();
threadSignal.await();
// now. both txs have read.
// let tx1 start writing
w1Signal.countDown();
w1.join();
w2Signal.countDown();
w2.join();
if (allowWriteSkew) {
// should have no exceptions!!
throwExceptions(w1exceptions, w2exceptions);
assert w2exceptions.isEmpty();
assert w1exceptions.isEmpty();
assert "v3".equals(cache.get("k")) : "W2 should have overwritten W1's work!";
assertNoLocks();
} else {
Collection<Exception> combined = new HashSet<Exception>(w1exceptions);
combined.addAll(w2exceptions);
assert !combined.isEmpty();
assert combined.size() == 1;
assert combined.iterator().next() instanceof CacheException;
}
}
private void throwExceptions(Collection<Exception>... exceptions) throws Exception {
for (Collection<Exception> ce : exceptions) {
for (Exception e : ce) throw e;
}
}
protected class EntryWriter implements Callable<Void> {
private final CyclicBarrier barrier;
public 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.put("k", "_lockthisplease_");
} catch (Exception e) {
log.error("Unexpected", e);
tm.setRollbackOnly();
throw e;
} finally {
if (tm.getStatus() == Status.STATUS_ACTIVE) tm.commit();
else tm.rollback();
}
return null;
} finally {
log.debug("Wait for all execution paths to finish");
barrier.await();
}
}
}
}