Skip to content

Let single-key atomic operations join an open Transaction - #187

Merged
jtnelson merged 9 commits into
developfrom
feature/atomic-operations-join-transactions
Aug 24, 2026
Merged

Let single-key atomic operations join an open Transaction#187
jtnelson merged 9 commits into
developfrom
feature/atomic-operations-join-transactions

Conversation

@jtnelson

@jtnelson jtnelson commented Aug 23, 2026

Copy link
Copy Markdown
Member

Summary

The single-key atomic operations on Record (exchange, getAndUpdate and updateAndGet) previously refused a Record bound to an open Transaction. They now resolve against the binding, the way save and every read already do: within an open Transaction the operation stages and becomes durable when the commit succeeds, and outside of one nothing changes.

Behavior

  • getAndUpdate and updateAndGet on 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; RetryExhaustedException remains the contract of the direct scope and of managed transactAndSupply work.
  • exchange on a bound record verifies its expected value against the snapshot and stages the swap: a true answer 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.
  • A conflict with a concurrent writer surfaces as a TransactionException at the operation or fails the commit; either way, nothing the operation staged becomes durable.
  • A transaction that ends without a commit leaves the database untouched by these operations, and the record carries no trace of the write: it keeps the value the operation observed, so the operations resume without a refresh and a later save writes only the caller's own edits.
  • After a transaction ends, the operations resume against the enclosing Runway, unchanged.
  • The refusals that protect these operations apply in both scopes: getAndUpdate and updateAndGet refuse a record with unsaved changes, and all three refuse a record staged for deletion or one that is not pinned to a Runway.

Scope

The change covers the three single-key atomic operations on Record and the transaction bookkeeping they need. The find-and-update operations on Runway, TransactionInterface and Audience are unchanged, as are the save, load and delete paths.

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
jtnelson merged commit e71bce1 into develop Aug 24, 2026
4 checks passed
@jtnelson
jtnelson deleted the feature/atomic-operations-join-transactions branch August 24, 2026 18:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant