Working with GemFire Cache Transactions
Working with GemFire Cache Transactions
This section contains guidelines and additional information on working with GemFire and its cache transactions.
Setting Global Copy on Read
Copy-on-read is not specifically a transaction setting, but if you are using transactions, you probably want copy-on-read behavior. To enable global copy-on-read for all reads, modify cache.xml or use the Java API.
<cache lock-lease="120" lock-timeout="60" search-timeout="300"copy-on-read="true">
Cache c = CacheFactory.getInstance(system) c.setCopyOnRead(true);The copy-on-read attribute and the operations affected by the attribute setting are discussed in detail in Managing Data Entries.
Making a Safe Change Within a Transaction Using CopyHelper.copy
If you do not have copy-on-read set globally and are using replicated regions, you must explicitly make copies of the cache objects that you are modifying in your transactions. You can use CopyHelper.copy to make these copies.
CacheTransactionManager cTxMgr = cache.getCacheTransactionManager(); cTxMgr.begin(); Object o = (StringBuffer)r.get("stringBuf"); StringBuffer s = (StringBuffer) CopyHelper.copy(o); s.append("Changes unseen before commit. Read Committed."); r.put("stringBuf", s); cTxMgr.commit();
Transactions and Functions
You can run a function inside a transaction and you can run a transaction inside a function, as long as your combination of functions and transactions does not result in nested transactions. You cannot call a function from inside a transaction if the function runs its own transaction.
You can also have multiple functions participate within a single transaction.
If you are suspending and resuming a transaction with multiple function calls, all function calls participating in the transaction must execute on the same member.
See Transactional Using a Function Example for an example.
Using Queries and Indexes with Transactions
Queries and indexes reflect the cache and ignore the changes made by ongoing transactions. If you do a query from inside a transaction, the query does not reflect the changes you made inside that transaction.
Collections and Region.Entry Instances in Transactions
Collections and region entries used in a transaction must be created inside the transaction. After the transaction has completed, the application can no longer use any region entry or collection or associated iterator created within the transaction. If you try to use these, you get an IllegalStateException.
Region collection operations include Region.keySet, Region.entrySet, and Region.values. You can create instances of Region.Entry through the Region.getEntry operation or by looking at the contents of the result returned by a Region.entrySet operation.
Using Eviction and Expiration Operations
Entry expiration and LRU eviction affect the committed state. They are not part of the transaction, and they cannot be rolled back.
LRU eviction operations do not cause write conflicts with existing transactions, despite destroying or invalidating entries. LRU eviction is deferred on entries modified by the transaction until the commit completes. Because anything touched by the transaction has had its LRU clock reset, eviction of those entries is not likely to happen immediately after the commit.
When a transaction commits its changes in a region with distributed scope, the operation can invoke eviction controllers in the remote caches, as well as in the local cache.
- When you are using transactions on local, preloaded or empty regions, make
expiration local if possible. For every instance of that region, configure an
expiration action of local invalidate or local destroy. In a cache.xml declaration,
use a line similar to
<expiration-attributes timeout="60" action="local-invalidate"/>
In regions modified by a transaction, local expiration is suspended. Expiration operations are batched and deferred per region until the transactions complete. Once cleanup starts, the manager processes pending expirations. Transaction that need to change the region wait until the expirations are complete.
- With partitioned and replicated regions, you cannot use local expiration. When you
are using distributed expiration, the expiration is not suspended during a
transaction, and expiration operations distributed from another member can cause write
conflicts. In replicated regions, you can avoid conflicts by setting up your
distributed system this way:
- Choose an instance of the region to drive region-wide expiration. Use a replicated region, if there is one.
- Configure distributed expiration only in that region instance. The expiration
action must be either invalidate or destroy. In a cache.xml declaration, use a
line similar to
<expiration-attributes timeout="300" action="destroy"/>
- Run your transactions in the member where you configured expiration.
Transactions and Consistent Regions
A transaction that modifies a region having consistency checking enabled generates all necessary version information for region updates when the transaction commits.
If a transaction modifies a normal, preloaded or empty region, the transaction is first delegated to a GemFire member that holds a replicate for the region. This behavior is similar to the transactional behavior for partitioned regions, where the partitioned region transaction is forwarded to a member that hosts the primary for the partitioned region update.
The limitation for transactions with a normal, preloaded or empty region is that, when consistency checking is enabled, a transaction cannot perform a localDestroy or localInvalidate operation against the region. GemFire throws an UnsupportedOperationInTransactionException exception in such cases. An application should use a Destroy or Invalidate operation in place of a localDestroy or localInvalidate when consistency checks are enabled.
Suspending and Resuming Transactions
GemFire APIs provide the ability to suspend and resume transactions. The ability to suspend and resume is useful when a thread must perform some operations that should not be part of the transaction before the transaction can complete.
When a transaction is suspended, it loses the transactional view of the cache. None of the previous operations (before calling suspend) are visible to the thread. Subsequently any operations that are performed by the thread do not participate in the suspended transaction.
When a transaction is resumed, the resuming thread assumes the transactional view. A transaction that is suspended on a member must be resumed on the same member. Before resuming a transaction, you may want to check if the transaction exists on the member and whether it is suspended.
You may optionally use the tryResume method.
If the member with the primary copy of the data crashes, the transactional view that applied to that data is lost. The secondary member for the data will not be able to resume any transactions suspended on the crashed member. You will need to take remedial steps to retry the transaction on a new primary copy of the data.
If a suspended transaction is not touched for a period of time, GemFire cleans it up automatically. By default, the timeout for a suspended transaction is 30 minutes and can be configured using the system property gemfire.suspendedtxTimeout. For example, to modify the default, you could set gemfire.suspendedtxTimeout=60 (time in minutes).
Using Cache Writer and Cache Listener Plug-Ins
All standard GemFire application plug-ins work with transactions. In addition, the transaction interface offers specialized plug-ins that support transactional operation.
- CacheLoader. When a cache loader is called by a transaction operation, values loaded by the cache loader may cause a write conflict when the transaction commits.
- CacheWriter. During a transaction, if a cache writer exists, its methods are invoked as usual for all operations, as the operations are called in the transactions. The netWrite operation is not used. The only cache writer used is the one in the member where the transactional data resides.
- CacheListener. The cache listener callbacks - local and remote - are triggered after the transaction commits. The system sends the conflated transaction events, in the order they were stored.
Configuring Transaction Plug-In Event Handlers
GemFire has two types of transaction plug-ins-- Transaction Writers and Transaction Listeners. You can optionally install one transaction writer and one or more transaction listener per cache.
When you commit a transaction, if a transaction writer is installed in the cache where the data updates were performed, it is called. The writer can do whatever work you need, including aborting the transaction.
The transaction writer is the last place that an application can rollback a transaction. If the transaction writer throws any exception, the transaction is rolled back. For example, you might use a transaction writer to update a backend data source before the GemFire cache transaction completes the commit. If the backend data source update fails, the transaction writer implementation can throw a TransactionWriterException to veto the transaction.
A typical usage scenario would be to use the transaction writer to prepare the commit on the external database. Then in a transaction listener, you can apply the commit on the database.
When the transaction ends, its thread calls the transaction listener to perform the appropriate follow-up for successful commits, failed commits, or voluntary rollbacks. The transaction that caused the listener to be called no longer exists by the time the listener code executes.
Transaction listeners have access to the transactional view and thus are not affected by non-transactional update operations. TransactionListener methods cannot make transactional changes or cause a rollback. They can, however, start a new transaction. Multiple transactions on the same cache can cause concurrent invocation of TransactionListener methods, so implement methods that do the appropriate synchronization of the multiple threads for thread-safe operation.
A transaction listener can preserve the result of a transaction, perhaps to compare with other transactions, or for reference in case of a failed commit. When a commit fails and the transaction ends, the application cannot just retry the transaction, but must build up the data again. For most applications, the most efficient action is just to start a new transaction and go back through the application logic again.
The rollback and failed commit operations are local to the member where the transactional operations are run. When a successful commit writes to a distributed or partitioned region, however, the transaction results are distributed to other members the same as other updates. The transaction listener on the receiving members reflect the changes the transaction makes in that member, not the originating member. Any exceptions thrown by the transaction listener are caught by GemFire and logged.
If you implement a transaction listener, you can configure one or more listeners as a cache attribute.
To configure a transaction listener, add a cache-transaction-manager configuration to the cache definition and define one or more instances of transaction-listener there. The only parameter to this transaction-listener is URL, which must be a string, as shown in the following cache.xml example.
<cache search-timeout="60"> <cache-transaction-manager> <transaction-listener> <class-name>com.company.data.MyTransactionListener</class-name> <parameter name="URL"> <string>jdbc:cloudscape:rmi:MyData</string> </parameter> </transaction-listener> <transaction-listener> . . . </transaction-listener> </cache-transaction-manager> . . . </cache>
CacheTransactionManager manager = cache.getCacheTransactionManager(); manager.addListener(new LoggingTransactionListener());