Skip to content

Make AutoTimestampEventListener temporary disabling thread-safe (7.1.x)#15953

Closed
codeconsole wants to merge 1 commit into
apache:7.1.xfrom
codeconsole:fix/autotimestamp-thread-safety-7.1.x
Closed

Make AutoTimestampEventListener temporary disabling thread-safe (7.1.x)#15953
codeconsole wants to merge 1 commit into
apache:7.1.xfrom
codeconsole:fix/autotimestamp-thread-safety-7.1.x

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Fixes #14510 on 7.1.x. Forward-port of #15952 (7.0.x) across the audit-enhancements divergence (#15118).

The bug

withoutTimestamps, withoutDateCreated and withoutLastUpdated disabled timestamping by mutating the listener-wide entity maps (capturing each entry and setting it to Optional.empty(), then restoring). That state is JVM-global, so overlapping windows on concurrent threads corrupt each other:

  1. Thread A enters: captures ON, sets OFF
  2. Thread B enters: captures OFF, sets OFF
  3. Thread A exits: restores ON
  4. Thread B's save now runs with timestamping — its historical dateCreated/lastUpdated values are silently clobbered

Reproduction: https://github.com/codeconsole/grails-autotimestamp-bug

A second facet: there was no try/finally, so an exception inside the closure left timestamping permanently disabled for the whole JVM.

The fix

The shared metadata maps are no longer mutated. Suppression is tracked per thread: the property-name getters consult a ThreadLocal holding an all-entities nesting depth plus the set of entity names disabled by per-class scopes. Each scope only re-enables the names it added, so nested and overlapping scopes restore correctly; try/finally guarantees restoration when the closure throws; the ThreadLocal is removed when empty so nothing is retained on pooled threads.

Public API, protected fields and single-thread semantics are unchanged (binary and source compatible). The CreatedBy/UpdatedBy auditing maps introduced in #15118 are untouched — no withoutXxx API covers them.

Branch strategy

  • This class is identical on 7.1.x, 7.2.x and 8.0.x, so this PR merges forward cleanly with no conflicts.
  • When merging 7.0.x → 7.1.x after Make AutoTimestampEventListener temporary disabling thread-safe #15952, the listener conflict resolves as keep 7.1.x's version (this branch already contains the fix); the spec and doc changes are identical on both sides and merge clean.

Behavioral note

Disabling is now scoped to the calling thread. Threads spawned inside the closure are no longer affected — under the old global behavior they were (by accident). Code relying on that must call withoutTimestamps on each thread. The auto-timestamping guide now documents the thread scoping. Worth a mention in the release notes.

Tests

The spec was rewritten to exercise the listener through the public beforeInsert/beforeUpdate API and extended with tests that reproduce the bug — verified to fail on the unfixed 7.1.x listener before applying the fix:

All 224 tests in grails-datamapping-core pass, as do the timestamp specs in grails-datamapping-core-test (including the #15118 auditing specs), grails-data-hibernate5-core and grails-test-suite-uber; Checkstyle and CodeNarc are clean.

Fixes apache#14510. The withoutTimestamps, withoutDateCreated and
withoutLastUpdated methods disabled timestamping by mutating the
listener-wide entity maps (capturing each entry and setting it to
Optional.empty()), which is JVM-global state. Overlapping windows on
concurrent threads corrupted each other: a thread leaving its window
re-enabled timestamping for every other thread still inside one, so
historical dateCreated/lastUpdated values were silently overwritten.
An exception inside the closure also left timestamping permanently
disabled because the previous state was restored without try/finally.

Suppression is now tracked per thread: the property-name getters
consult a ThreadLocal holding an all-entities nesting depth plus the
set of entity names disabled by per-class scopes. Each scope only
re-enables the names it added, so nested and overlapping scopes
restore correctly, and try/finally guarantees restoration when the
closure throws. The shared metadata maps are no longer mutated. The
CreatedBy/UpdatedBy auditing maps are untouched, as no withoutXxx
API covers them.

Behavioral note: disabling is now scoped to the calling thread;
threads spawned inside the closure are no longer affected. The GORM
auto-timestamping guide documents this.

The spec now exercises the listener through beforeInsert/beforeUpdate
and covers cross-thread isolation, overlapping windows, nested and
overlapping same-class scopes, exception restoration and concurrent
use.

Forward-port of the 7.0.x fix (apache#15952) to 7.1.x; merges cleanly into
7.2.x and 8.0.x, where this class is identical.
@testlens-app

testlens-app Bot commented Jul 10, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: ac9d748
▶️ Tests: 44741 executed
⚪️ Checks: 55/55 completed


Learn more about TestLens at testlens.app.

@jdaugherty

Copy link
Copy Markdown
Contributor

7.1 has ceased for new features, I think this change should go in 8.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants