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
{{ message }}
Repository navigation
native: save() overwrites a record changed after it was read (lost update) #172
On the native build, save() of a record object overwrites a record that another transaction changed after the object was read, and silently reverts that transaction's committed values (a lost update). The JVM build refuses the save with ConcurrentModificationException.
Scenario (a probe run on the 26.11.1.dev0 dev wheels, Linux x86-64): a document with a = 1, b = 1 is read and turned into a mutable object with db.lookup_by_rid(rid).modify(); another transaction sets b = 2 and commits; then a = 10 is set on the object and save() is called in a transaction.
build
save()
stored record afterwards
arcadedb-jvm
raises ArcadeDBError (ConcurrentModificationException: Record #1:0 was modified by a concurrent ...)
a = 1, b = 2
arcadedb (native)
succeeds
a = 10, b = 1
Cause: native save() of an existing record runs UPDATE <rid> MERGE :row with the Python snapshot of every property (src/arcadedb/_native/records.py), and carries nothing that identifies the image it was read from. The engine has no record version; the JVM check is the engine's own comparison of the bytes a MutableDocument was read from with the bytes stored now (TransactionContext.checkRecordIsStillTheOneRead).
Fix in progress: the native glue fingerprints the stored record bytes when a record row is read and checks the fingerprint in the same transaction as the update, raising the engine's ConcurrentModificationException on a mismatch. Because the fingerprint would be computed for every record row a read returns, the change gets a timed A/B of the read paths before a PR, so the read speed of the native build is not traded silently for this check.
The fix on fix/native-save-version (head 8e11677) computes the record fingerprint on every read. We timed it against main on the native wheel (same engine e353330c5c, -O3, interleaved pairs, ab.py --noise on two pinned laptop cores), with a rule written before the run. The fingerprint costs reads more than the rule allows; these figures have narrow intervals (ratio = fix / main, 95% CI):
read
clean pairs
ratio 95% CI
a 10k-row SELECT, small records
26
[1.075, 1.079]
a lookup by RID, medium records
26
[1.063, 1.075]
the first row of a query, medium records
26
[1.055, 1.060]
a 2-hop Cypher query, medium records
26
[1.053, 1.071]
The two controls (a projection that reads no record) did not move: [0.997, 1.003], [0.984, 1.002].
So the fix will compute the fingerprint lazily, from the values the record already holds, only when modify() or save() needs it; a record that is read and never saved pays nothing. The redesigned head gets the same timed comparison before its PR is merged.
Activity
tae898 commentedon Nov 2, 2026
The fix on
fix/native-save-version(head8e11677) computes the record fingerprint on every read. We timed it against main on the native wheel (same enginee353330c5c,-O3, interleaved pairs,ab.py --noiseon two pinned laptop cores), with a rule written before the run. The fingerprint costs reads more than the rule allows; these figures have narrow intervals (ratio = fix / main, 95% CI):The two controls (a projection that reads no record) did not move: [0.997, 1.003], [0.984, 1.002].
So the fix will compute the fingerprint lazily, from the values the record already holds, only when
modify()orsave()needs it; a record that is read and never saved pays nothing. The redesigned head gets the same timed comparison before its PR is merged.native: keep the f-string statements of the save check out of bandit'…
native: save() of an existing record needs a transaction, as on the J…
native: encode the save fingerprint straight to bytes (#172)