User Guide

7  Transaction Support

Jofti supports transactional Caches through integration with JTA or relies on the caches themselves being transactional (no data is propagated to the listeners before the transaction commits). JBossCache transaction support is managed by the first method and Tangosol Coherence provides the second.

The JTA integration in Jofti is achieved through the use of the javax.transaction.Synchronization callback. Jofti registers a Synchronization object with the TransactionManager which uses the beforeCompletion() and afterCompletion() callback methods to notify registered parties of the transacion lifecycle events.

 

7.1  JBossCache support

When using JBossCache the TransactionManager used by Jofti is obtained directly from JBossCache, it does create or look up its own manager.

In addition, Jofti also relies on the isolation level configured in the Cache. Therefore, similarly to databases, some queries will block waiting for lock releases in the Cache. There is a a greater potential for this type of behaviour with range searches using the higher levels of isolation.

Note: it is not the index which is locking but rather when Jofti attempts to retrieve the cache entry that matches the id(s) returned in the query. This takes place after the query is run against the index.

The Tx levels in JBossCache are:
  • SERIALIZABLE
  • REPEATABLE_READ
  • READ_COMMITTED
  • READ_UNCOMMITTED
  • NONE

 

7.2  NONE and READ_UNCOMMITTED

At levels NONE and READ_UNCOMMITTED the Cache adapter applies the changes within a transaction directly to the index. This makes the changes to the index immediately visible to other transactions, as you would expect.

We can see this demonstrated below:


//assume we have a configured IndexsManager
NameSpacedIndex index = manager.getNameSpacedIndex("jbosscache");

// get a reference to jboss transaction manager
		
TransactionManager mgr =
	((TreeCache)index.getCacheImpl()).getTransactionManager();

// start a transaction	
mgr.begin();

// put a value in namespace "test" under key "test"
cache.put("test", "key", "test");

// commit the transaction
mgr.commit();
		
// we have one result returned here
Map results = index.query(new MatchNSQuery("test","test"));
	
//start another transaction		
mgr.begin();

// insert a new value under the key "test"
index.put("test", "key", "test2");

// this will return 1 result
results = cache.query(new MatchNSQuery("test","test2"));

// get the current transaction and suspend it
Transaction tx = mgr.getTransaction();
mgr.suspend();
		
// start a new transaction
mgr.begin();

// we can see the new value from the suspended transaction
results = cache.query(new MatchNSQuery("test","test2"));

// put a new value under key "test"
index.put("test", "key", "test3");

// commit the second transaction
mgr.commit();
		
// resume the original transaction
mgr.resume(tx);

// this will return 1 entry as we can see the result from the other tx
results = cache.query(new MatchNSQuery("test","test3"));

//commit the first tx
mgr.commit();

// rest of program ....

 

7.3  READ_COMMITTED

At a level of READ_COMMITTED the adapter does not apply the changes within a transaction to the index. Instead, a local index is created which is used in tandem with the main index. Changes within a transaction are propagated to the local index and at commit time are applied to the main index (as part of the commit step of the transaction).

While a transaction is ongoing, query results from the main and local indexes are merged so that the index changes local to that transaction are visible as part of the query results but are not visible to any other transactions.

In concept this is similar in approach to Oracle's Multiversion Concurrency Control in that each transaction has a copy the parts of the index that have been changed.

Even though the Index behaves in this manner JBossCache does not. The isolation level in JBossCache is managed through read and write locks on the nodes in the Cache, not copies of data. This means that access to the data in the Nodes by Jofti as part of its querying is also governed by the same locks (if we are to preserve the locking semantics defined by the Cache). It is therefore worth noting that queries will be susceptible to lock acquisition exceptions depending upon the lock level used and the update profile of your application. This is exactly the same as if you had attempted to use cache.get(nameSpace, key) directly on the Cache.

This can be demonstrated below:


cache = manager.getNameSpacedIndex(cacheName);

TransactionManager mgr = ((TreeCache)cache.getCacheImpl())
    .getTransactionManager();

//uses an implicit tx created by JBossCache if one is not created 
cache.put("test", "key1", "test");

//begin a tx
mgr.begin();

// put a value  in the cache	
cache.put("test", "key", "test");

// the result size is 2 here as both the original and the
//  value in the transaction are visible. 
// Inside JBossCache this will also result in the acquisition
// of a readlock on the /"test" node as part of the query

Map results = cache.query(new MatchNSQuery("test","test"));


mgr.commit();

// the result size is 2 here as both the original and the value
// have been committed		
results = cache.query(new MatchNSQuery("test","test"));

// start tx 2
mgr.begin();

// put in a new value replacing the key
cache.put("test", "key", "test2");

// suspend tx 2
Transaction tx = mgr.getTransaction();
mgr.suspend();

// start tx3
mgr.begin();
	
// not been committed yet -  should not be visible inside tx 
// results size == 0
result = cache.query(new MatchNSQuery("test","test2"));

// this will cause a lock exception to be thrown
col = cache.query(new MatchNSQuery("test","test"));

The interesting point here is that the query for "test2" that produces 0 results in tx3 does not run into locking issues as the value "test2" is not in the main or local index for tx 3 and therefore the query does not attempt to retrieve the value from the Cache. This means that no lock contention is encountered when data is new in a second tx and an exisitng tx searches for it.

However, the second query for "test" inside tx3 will result in an exception as the index does attempt to retrieve a value from a node which is write locked by a previous tx.

The exception always occurs here as tx2 is suspended. In a more real world usage the lock may well be acquired by the second transaction as JBossCache will try and acquire that lock for a specified period of time, by which time the other transaction will most probably have completed. However, that is not definite and you could well still receive exception when you are searching for existing data (not new values) that is in the same node where an update is also taking place.

Note: In JBossCache v2.2.4 and below the implementation of READ_COMMITTED is not entirely correct meaning it is possible to retrieve uncommitted data from the tree in a different tx.

 

7.4  REPEATABLE_READ

As specified in the JBoss documentation:
"Data can be read while there is no write and vice versa. This level prevents "non-repeatable read" but it does not prevent the so-called "phantom read" where new data can be inserted into the tree from the other transaction. "

The impact for Jofti is that as with READ_COMMITTED ,queries that result in gets from the Cache as part of tx will keep readlocks open until the tx is committed or rolledback. Normally this is not that much of a problem as the tx is likely to be short lived. However, for long running or suspended txs you can run into lockouts as shown below:

cache = manager.getNameSpacedIndex(cacheName);

TransactionManager mgr = ((TreeCache)cache.getCacheImpl())
    .getTransactionManager();
    		
cache.put("test", "key1", "test");

//committ some test data for use in the test
mgr.begin();

cache.put("test", "key", "test");

//we have 2 entries visible in this query
Map results = cache.query(new MatchNSQuery("test","test"));

//commit the tx
mgr.commit();

//start a new tx
mgr.begin();

// this will read lock the node and prevent a write lock being acquired
results = cache.query(new MatchNSQuery("test","test"));

//suspend the tx
Transaction tx = mgr.getTransaction();
mgr.suspend();

// start a new one
mgr.begin();

try {
// this will fail to acquire a write lock on the node
// as the first tx is suspended - this will timeout waiting and throw 
// an exception

    cache.put("test", "key", "test2");
	
} catch (JoftiException e){
    // the exception is a wrapper for a JBoss lock exception
}
		
// however, we can get a read lock
col = cache.query(new MatchNSQuery("test","test"));

//suspend this tx
Transaction tx2 = mgr.getTransaction();
mgr.suspend();

// resume first transaction
mgr.resume(tx);

// get rid of the read locks form the first tx
mgr.commit();

resume tx 2
mgr.resume(tx2);

// this now works as the original write lock is no longer in force
cache.put("test", "key", "test2");
		
// commit at the end
mgr.commit();
			

It is important to note that the first attempt by tx2 to insert the data failed and the second one succeeded. This is because of tx1 holding a readlock on node "/test" for the first attempt. This behaviour is consistent with the isolation level.

 

7.5  SERIALIZABLE

In SERIALIZABLE mode JBossCache uses exclusive locks for both reads and writes. Explained in the JBossCache documentation as:
"Data access is synchronized with exclusive locks. Only 1 writer or reader can have the lock at any given time. Locks are released at the end of the transaction."

This means that a query in one transaction can wait for a query in another transaction. Once again it is useful to point out that in the index queries run in parallel, it is the retrieval of the individual objects from the Cache after the full key set for the query is known that is subject to the locking.

E.g.:


cache = manager.getNameSpacedIndex(cacheName);

TransactionManager mgr = ((TreeCache)cache.getCacheImpl())
    .getTransactionManager();

//start a tx
mgr.begin();

// put a value in
cache.put("test", "key", "test");
Map results= cache.query(new MatchNSQuery("test","test"));

// commit the value
mgr.commit();
		

mgr.begin();

// returns 1 result and acquires a read lock
results = cache.query(new MatchNSQuery("test","test"));

Transaction tx = mgr.getTransaction();
mgr.suspend();

// start a new tx
mgr.begin();

try{
// this will throw an exception as a second read lock cannot 
// be acquired on the node "/test"

    results = cache.query(new MatchNSQuery("test","test"));
} catch (JoftiException e){
    // handle error here
}

//suspend this tx
Transaction tx2 = mgr.getTransaction();
mgr.suspend();

// resume tx1 and commit so locks are released		
mgr.resume(tx);
mgr.commit();
		
//resume second tx
mgr.resume(tx2);

// re-perform original query - this now works
results = cache.query(new MatchNSQuery("test","test"));
mgr.commit();

We can see here that the isolation level prohibits two queries that attempt to acquire the same readlock. However, it does not prevent two queries which only get data from different nodes, even if the data types are the same (e.g two queries for String objects that are in different nodes or namespaces.

 

7.6  Tangosol Coherence support

Note: this feature is still in Beta. For Coherence, Jofti relies on the transaction semantics of the cache and the callbacks that are generated from this. Therefore, once data is committed to the cache and a callback is generated, the Index will update itself according to that callback. This means that, for the current release, queries within the context of a transaction may see data that the transaction has removed or be unable to see data that has been added/updated.

In the majority of cases this will be adequate, but a future release should enable the index to participate in the transaction.

Features

  • Multi Cache support
  • Transaction support
  • Type aware searching
  • Configurable property indexing
  • Indexing/searching by interfaces
  • Support for Dynamic Proxies
  • Support for primitve attributes
  • Support for Collections and Arrays
  • String prefix searching
  • Simple query language