You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while measuring #6129 (PR #6140), which does not change this behaviour.
What happens
Two transactions updating the SAME record silently lose one of the two writes, with no ConcurrentModificationException at all, when that record has spilled into a placeholder: an 8-byte pointer in its own slot, and the content on another page.
T1: begin; R.set("v", A).save() // R is a placeholder-backed record
T2: transaction { R.set("v", B).save() } // commits
T1: commit // succeeds. B is gone, no conflict raised.
Confirmed on main with a Holder type of one bucket, a record grown to 30 KB so it becomes a placeholder pointer on page 0 with its content on page 1.
Why
A record update is deferred. TransactionContext.addUpdatedRecord pins the record's own page (LocalBucket.fetchPageInTransaction) when save() is called, and the write itself runs in commit1stPhase, after the file's commit lock is taken. So:
the page holding the placeholder POINTER is pinned, but neither transaction writes a single byte to it - an update whose content record can absorb the new value leaves the pointer alone, and commit1stPhase drops a modified page whose modified range is empty. Nothing to version-check;
the CONTENT page is not pinned by anything. Both transactions load it fresh, under the lock, at the newest committed version - so its version check can never fire either.
The result is a blind last-writer-wins on the content record. The same reasoning applies to any update that reaches a page the transaction did not pin, but the placeholder is the shape where the record's whole content lives there.
Multi-page (chunked) records are not affected: any update of one rewrites its head chunk on the pinned page, so the page-version check fires. That is exactly the property PR #6140 had to preserve explicitly, with LocalBucket.chunkChainTailFingerprint, once the head chunk became mergeable.
Worth considering
Fingerprint the content record the way fix(#6129): the slot merge now covers the records that outgrew their page #6140 fingerprints a chunk chain's tail: take it in addUpdatedRecord (the moment the record is taken for update, the same moment its page is pinned) and compare it in updateRecordInternal before overwriting the content. A mismatch is a genuine concurrent modification and should raise a retryable ConcurrentModificationException. Cost is proportional to a write the transaction is doing anyway.
(1) looks strictly better: it is precise, and it does not add conflicts anywhere.
Not a regression
This has been the behaviour since placeholders existed; nothing in #6140 makes it better or worse. Filing it because the measurement made it visible and the engine's stated contract - "two transactions writing the same record conflict by design, the byte-for-byte pre-image check catches it and the application must reload" (engine/CLAUDE.md) - does not hold for this shape.
Found while measuring #6129 (PR #6140), which does not change this behaviour.
What happens
Two transactions updating the SAME record silently lose one of the two writes, with no
ConcurrentModificationExceptionat all, when that record has spilled into a placeholder: an 8-byte pointer in its own slot, and the content on another page.Confirmed on
mainwith aHoldertype of one bucket, a record grown to 30 KB so it becomes a placeholder pointer on page 0 with its content on page 1.Why
A record update is deferred.
TransactionContext.addUpdatedRecordpins the record's own page (LocalBucket.fetchPageInTransaction) whensave()is called, and the write itself runs incommit1stPhase, after the file's commit lock is taken. So:commit1stPhasedrops a modified page whose modified range is empty. Nothing to version-check;The result is a blind last-writer-wins on the content record. The same reasoning applies to any update that reaches a page the transaction did not pin, but the placeholder is the shape where the record's whole content lives there.
Multi-page (chunked) records are not affected: any update of one rewrites its head chunk on the pinned page, so the page-version check fires. That is exactly the property PR #6140 had to preserve explicitly, with
LocalBucket.chunkChainTailFingerprint, once the head chunk became mergeable.Worth considering
addUpdatedRecord(the moment the record is taken for update, the same moment its page is pinned) and compare it inupdateRecordInternalbefore overwriting the content. A mismatch is a genuine concurrent modification and should raise a retryableConcurrentModificationException. Cost is proportional to a write the transaction is doing anyway.fetchPageInTransaction, so ordinary MVCC covers it. Cheaper to describe, but it makes that page conflict for reasons that have nothing to do with this record (any other record on it), which is the false conflict Is ArcadeDB ready for a real multi-user environment? The problem of ConcurrentModificationExceptions #5279/Slot merge does not cover records that outgrew their page: chunked and placeholder updates poison the page unconditionally #6129 exist to remove - so it would need the page to be rebasable first.(1) looks strictly better: it is precise, and it does not add conflicts anywhere.
Not a regression
This has been the behaviour since placeholders existed; nothing in #6140 makes it better or worse. Filing it because the measurement made it visible and the engine's stated contract - "two transactions writing the same record conflict by design, the byte-for-byte pre-image check catches it and the application must reload" (
engine/CLAUDE.md) - does not hold for this shape.