I'm building a application around an Infinispan cache and the Atomikos transaction manager. I found out that the transaction isolation is not working for transactions open in two different threads on the same JVM.
The cache is instanciated using the following code:
cacheManager = new DefaultCacheManager();
final Configuration config = new Configuration().fluent().transactionManagerLookup(this.tmLookup).recovery().locking()
.isolationLevel(IsolationLevel.READ_COMMITTED).build();
this.cacheManager.defineConfiguration("Gruik", config);
this.cache = this.cacheManager.getCache("Gruik");
With this.tmLookup
is a simple implementation of org.infinispan.transaction.lookup.TransactionManagerLookup
returning the configure Atomikos transaction manager.
I set up a little test by populating the cache with a single value and I launch in two threads a reader and a writer each in a separate transaction. Basically, the writer will get the value stored in the cache, change the value and save it into the cache. On the other end, the read with get and display the value at various stages: before any change performed by the writer, after the writer had changed the pojo, after the writer has saved the updated pojo and finally after the commit of the writer's transaction.
The writer code is:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void performTrans() throws InterruptedException, BrokenBarrierException {
LOGGER.info("Wait to start");
pBarrier.await(); // 1
final Pojo entity = cache.get(KEY);
LOGGER.info("Start entity: {}", entity);
pBarrier.await(); // 2
entity.setValue(entity.getValue() + 42);
LOGGER.info("Entity changed wait for reader");
pBarrier.await(); // 3
cache.put(KEY, entity);
LOGGER.info("Entity saved wait for reader");
pBarrier.await(); // 4
}
The reader code is:
public void performTrans() throws InterruptedException, BrokenBarrierException {
LOGGER.info("Wait to start");
pBarrier.await(); // 1
final Pojo entity = cache.get(KEY);
LOGGER.info("Start entity: {}", entity);
pBarrier.await(); // 2
LOGGER.info("Wait writer to make changes");
pBarrier.await(); // 3
LOGGER.info("After change: {}", entity);
pBarrier.await(); // 4
Pojo newEntity = cache.get(KEY);
LOGGER.info("After save: {}", newEntity);
pBarrier.await(); // 5
newEntity = cache.get(KEY);
LOGGER.info("After transaction end: {}", newEntity);
}
In order to trace the entities returned by the cache I implemented the Pojo toString()
like this:
public String toString() {
return "[" + System.identityHashCode(this) + "] id: " + this.id + ", value: " + this.value;
}
As the cache is configured to be isolated, I expected to have different pojo instances between the reader and the writer and that only the change will be only visible after the writer's transaction has been committed.
However I got the following output:
[Reader] - Wait to start
[Writer] - Wait to start
[Writer] - Start entity: [19682788] id: 1, value: 666
[Reader] - Start entity: [19682788] id: 1, value: 666
[Reader] - Wait writer to make changes
[Writer] - Entity changed wait for reader
[Reader] - After change: [19682788] id: 1, value: 708
[Writer] - Entity saved wait for reader
[Reader] - After save: [19682788] id: 1, value: 708
[Reader] - After transaction end: [19682788] id: 1, value: 708
So basically, the cache is performing like a hashmap as it returns the same pojo instance for both threads.
Question is: did I miss something in the configuration or in the expected behavior?
I'm pretty sure the transaction manager is working as I can get log messages from Atomikos indicating the start of distinct transactions on both reader and writer.
However I tried the same test using Ehcache instead of Infinispan and I got the expected results. Comparing the logs between the two tests I found out similar messages the only obvious difference being the absence of transaction id for Infinispan:
INFO atomikos - addParticipant [...]
vs
INFO atomikos ehcache-txid=0 - addParticipant