You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@grails.apache.org by GitBox <gi...@apache.org> on 2026/09/01 00:50:00 UTC
[GH] (grails-core/fix/dirty-checking-collection-tracking-8.0.x): Workflow run "CI" failed!
The GitHub Actions job "CI" on grails-core.git/fix/dirty-checking-collection-tracking-8.0.x has failed.
Run started by GitHub user codeconsole (triggered by codeconsole).
Head commit for run:
e88cb0beb97d90a04aa7de6fc52781f5768cc7f5 / Scott Murphy Heiberg <sc...@alwaysvip.com>
Keep collection properties dirty-checked after reassignment; track iterator-based removals
Interception-based stores (MongoDB) rely exclusively on the
DirtyChecking* wrappers — there is no flush-time snapshot comparison —
so mutations that escape them are silently lost: save() sees a clean
entity and persists nothing.
Two real-world escapes:
1. Reassignment through a generated setter stored the raw value, so
'entity.items = []' over a tracked list replaced the wrapper with a
plain untracked ArrayList. The common defensive re-init
'if (!entity.items) entity.items = []' triggers this on every load
(an empty tracked collection is falsy in Groovy), and because
[] == [] the equality-suppressed markDirty never flagged the
assignment either. The in-place add() that followed was lost.
2. DirtyCheckingCollection never overrode iterator(), so every
iterator-based removal — including Groovy's removeAll(Closure) and
retainAll(Closure) and Java's removeIf — bypassed tracking, along
with retainAll(Collection), List.sort and List.replaceAll.
The fix stays interception-only (no snapshots, no flush-time diffing):
- Wrappers override iterator()/listIterator() with dirty-marking
iterators plus the missing direct mutators — the same approach as
Hibernate's PersistentCollection.
- Generated setters for Collection/List/Set/Map-typed properties assign
through DirtyCheckingSupport.rewrap, which wraps the incoming value
ONLY when the value being replaced was itself a tracked wrapper. A
never-tracked property (transient instance, or a store like Hibernate
with its own dirty checking) stores the raw value as before, and
non-collection properties compile to identical bytecode.
- A replacement wrapper is flagged isAssigned() so
PersistentEntityCodec takes the full-rewrite path rather than
per-element diffing — a replacement's layout need not match the
stored array (a same-size replacement of clean elements previously
emitted no update at all once wrapped).
Specs reproduce each escape before the fix: DirtyCheckingCollectionSpec
(wrapper mutation paths), DirtyCheckCollectionReassignmentSpec
(setter reassignment), and EmbeddedCollectionDirtyTrackingSpec
(end-to-end against MongoDB, replicating the production shape where an
auto-timestamped entity dropped an embedded-collection add).
Report URL: https://github.com/apache/grails-core/actions/runs/33448845373
With regards,
GitHub Actions via GitBox