Skip to content

Savepoint transaction commit prematurely flushes L2 cache, causing cache pollution on parent rollback #3803

Description

@dragkes

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions