Let single-key atomic operations join an open Transaction - #187
Merged
Conversation
Within an open Transaction, getAndUpdate and updateAndGet write only the key they name. The operation no longer carries the record's other pending state into the transaction: a realm edit, a modified linked record and the author attribution stay pending, and no save notification fires at the commit. A record whose data the database no longer holds is refused with a DeletedRecordException, and the transaction stays usable because the refusal stages nothing.
A Transaction that ends without a commit now leaves no trace of a single-key atomic operation's write on a record that the database holds. The record keeps the value the operation observed, carries no unsaved change for the written key, and a later save writes only the caller's own edits. Previously the write survived the abort as an unsaved change. A managed retry of transactional work was therefore refused instead of retried, and a later save turned a discarded conditional write into an unconditional one. Within an open Transaction, an atomic update on a record that the database no longer holds throws DeletedRecordException whether or not the operated field is primitive. The direct Runway scope is unchanged.
Give the direct Runway retry path in updateAtomically an explicit else, so both branches of the binding dispatch are written out. Correct the abort comment in DatabaseTransaction: the map holds the replacement each atomic operation wrote, not the value it observed. Split the changelog entry for the single-key atomic operations into one sub-bullet per concern, and keep every test name within 80 columns. No behavior changes.
State what a caller of exchange, getAndUpdate and updateAtomically can rely on within a Transaction, and drop the staging and retry mechanics that deliver it. Move the section metadata rationale for canSetIfAbsent out of its Javadoc and into its body, where the audience is a maintainer. Give each ambiguous pronoun its referent, and state the boolean test that governs refreshAtomicableField.
Run the field's validation while the operation is in flight, so a ValidatedBy validator cannot end the enclosing Transaction and leave the swap to resolve against the enclosing Runway. An attempt to end the Transaction from a validator is refused. Cover an exchange against a Record whose existence rests on a save that the same Transaction staged but has not committed.
The README rung for changing one field described only the direct Runway scope. State what the operations do on a record bound to an open Transaction: the read joins the conflict footprint, the write stages, and the function applies once instead of retrying.
jtnelson
added a commit
that referenced
this pull request
Aug 24, 2026
* Let single-key atomic operations join an open Transaction * Keep a transactional atomic update to the key it names Within an open Transaction, getAndUpdate and updateAndGet write only the key they name. The operation no longer carries the record's other pending state into the transaction: a realm edit, a modified linked record and the author attribution stay pending, and no save notification fires at the commit. A record whose data the database no longer holds is refused with a DeletedRecordException, and the transaction stays usable because the refusal stages nothing. * Unwind aborted single-key atomic operations cleanly A Transaction that ends without a commit now leaves no trace of a single-key atomic operation's write on a record that the database holds. The record keeps the value the operation observed, carries no unsaved change for the written key, and a later save writes only the caller's own edits. Previously the write survived the abort as an unsaved change. A managed retry of transactional work was therefore refused instead of retried, and a later save turned a discarded conditional write into an unconditional one. Within an open Transaction, an atomic update on a record that the database no longer holds throws DeletedRecordException whether or not the operated field is primitive. The direct Runway scope is unchanged. * Clarify aborted atomic operation behavior * Tidy style and docs for transactional atomic operations Give the direct Runway retry path in updateAtomically an explicit else, so both branches of the binding dispatch are written out. Correct the abort comment in DatabaseTransaction: the map holds the replacement each atomic operation wrote, not the value it observed. Split the changelog entry for the single-key atomic operations into one sub-bullet per concern, and keep every test name within 80 columns. No behavior changes. * Trim the atomic operation docs to contract State what a caller of exchange, getAndUpdate and updateAtomically can rely on within a Transaction, and drop the staging and retry mechanics that deliver it. Move the section metadata rationale for canSetIfAbsent out of its Javadoc and into its body, where the audience is a maintainer. Give each ambiguous pronoun its referent, and state the boolean test that governs refreshAtomicableField. * Reduce transactional atomic update round trips * Keep a transactional exchange inside its operation window Run the field's validation while the operation is in flight, so a ValidatedBy validator cannot end the enclosing Transaction and leave the swap to resolve against the enclosing Runway. An attempt to end the Transaction from a validator is refused. Cover an exchange against a Record whose existence rests on a save that the same Transaction staged but has not committed. * Document the transactional scope of the single-key atomics The README rung for changing one field described only the direct Runway scope. State what the operations do on a record bound to an open Transaction: the read joins the conflict footprint, the write stages, and the function applies once instead of retrying.
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.
Summary
The single-key atomic operations on
Record(exchange,getAndUpdateandupdateAndGet) previously refused aRecordbound to an openTransaction. They now resolve against the binding, the waysaveand every read already do: within an openTransactionthe operation stages and becomes durable when the commit succeeds, and outside of one nothing changes.Behavior
getAndUpdateandupdateAndGeton a bound record answer against the transaction's snapshot: the value the update rests on is read through the transaction, so it joins the conflict footprint, and the update applies once. The result is visible to reads within the transaction, invisible outside of it, and durable only when the owner commits. No retry runs within a transaction;RetryExhaustedExceptionremains the contract of the direct scope and of managedtransactAndSupplywork.exchangeon a bound record verifies its expected value against the snapshot and stages the swap: atrueanswer holds only if the transaction commits. An exchange that expects absence keeps its contract, including the refusal to write to a record the database does not hold.TransactionExceptionat the operation or fails the commit; either way, nothing the operation staged becomes durable.Runway, unchanged.getAndUpdateandupdateAndGetrefuse a record with unsaved changes, and all three refuse a record staged for deletion or one that is not pinned to aRunway.Scope
The change covers the three single-key atomic operations on
Recordand the transaction bookkeeping they need. The find-and-update operations onRunway,TransactionInterfaceandAudienceare unchanged, as are the save, load and delete paths.