Make AutoTimestampEventListener temporary disabling thread-safe#15952
Closed
codeconsole wants to merge 1 commit into
Closed
Make AutoTimestampEventListener temporary disabling thread-safe#15952codeconsole wants to merge 1 commit into
codeconsole wants to merge 1 commit into
Conversation
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. 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.
This was referenced Jul 10, 2026
✅ All tests passed ✅🏷️ Commit: b0d7fe4 Learn more about TestLens at testlens.app. |
Contributor
|
This has been like this for years and at this point it does revert a behavior so I think this shouldn't go in 7.0 |
jdaugherty
pushed a commit
that referenced
this pull request
Jul 10, 2026
Fixes #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 (#15952) to 8.0.x, identical to the 7.1.x port (#15953), as this class is identical on 7.1.x through 8.0.x.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14510
The bug
withoutTimestamps,withoutDateCreatedandwithoutLastUpdateddisabled timestamping by mutating the listener-wide entity maps (capturing each entry and setting it toOptional.empty(), then restoring). That state is JVM-global, so overlapping windows on concurrent threads corrupt each other:dateCreated/lastUpdatedvalues are silently clobberedReproduction: 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
ThreadLocalholding 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/finallyguarantees restoration when the closure throws; theThreadLocalis removed when empty so nothing is retained on pooled threads.Public API, protected fields and single-thread semantics are unchanged (binary and source compatible).
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
withoutTimestampson 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/beforeUpdateAPI and extended with tests that reproduce the bug — written first and verified to fail on the old implementation:All 218 tests in
grails-datamapping-corepass, as do the timestamp specs ingrails-datamapping-core-test,grails-data-hibernate5-coreandgrails-test-suite-uber; Checkstyle and CodeNarc are clean.