When using nested (savepoint) transactions in Ebean, committing a savepoint immediately flushed L2 cache changes even though the parent transaction had not yet committed. If the parent transaction subsequently rolled back, the database changes were correctly undone, but the L2 cache had already been invalidated or updated — leaving it in a stale/incorrect state.
This could manifest as:
- Stale cache entries after a parent transaction rollback
- Unnecessary cache evictions causing extra database round-trips
- Queries returning rolled-back data from cache (if a cache put had occurred)
Root Cause
SavepointTransaction.commitSavepoint() called manager.notifyOfCommit(this) directly on savepoint release. This triggered PostCommitProcessing.notifyLocalCache() immediately, without waiting for the parent transaction to commit.
Fix
Instead of flushing cache changes on savepoint commit, the savepoint's TransactionEvent is now merged into the parent transaction's event via a new mergeIntoParent() call. Cache changes are deferred and only applied when the parent transaction commits (or silently discarded if it rolls back).
New merge() methods were added to TransactionEvent, CacheChangeSet, ManyChange, CacheChangeBeanRemove, and DeleteByIdMap to support the merge operation.
How to reproduce
try (Transaction outer = DB.beginTransaction()) {
outer.setNestedUseSavepoint();
try (Transaction nested = DB.beginTransaction()) {
bean.setName("rolled-back-change");
DB.save(bean);
nested.commit(); // savepoint released — cache polluted here (before fix)
}
outer.rollback(); // DB undone, but cache already dirty
}
// Cache now serves stale/missing data
DB.find(MyBean.class, bean.getId()); // unexpected DB hit or wrong value
When using nested (savepoint) transactions in Ebean, committing a savepoint immediately flushed L2 cache changes even though the parent transaction had not yet committed. If the parent transaction subsequently rolled back, the database changes were correctly undone, but the L2 cache had already been invalidated or updated — leaving it in a stale/incorrect state.
This could manifest as:
Root Cause
SavepointTransaction.commitSavepoint()calledmanager.notifyOfCommit(this)directly on savepoint release. This triggeredPostCommitProcessing.notifyLocalCache()immediately, without waiting for the parent transaction to commit.Fix
Instead of flushing cache changes on savepoint commit, the savepoint's
TransactionEventis now merged into the parent transaction's event via a newmergeIntoParent()call. Cache changes are deferred and only applied when the parent transaction commits (or silently discarded if it rolls back).New
merge()methods were added toTransactionEvent,CacheChangeSet,ManyChange,CacheChangeBeanRemove, andDeleteByIdMapto support the merge operation.How to reproduce