# All-or-nothing actions and concurrent changes

Every action is all-or-nothing
([Functions and actions](functions-and-actions.md)): its writes,
and those of the public actions it calls, commit together or not at all.
You write nothing for this.

A write to a record loaded earlier in the action checks that nobody
changed the record in between. If someone did, the runtime throws the
attempt away and runs the whole action again, with fresh reads and a
fresh `clock.now`:

```text
request A: loads todo 7                 request B: loads todo 7
request A: updates todo 7, commits
                                        request B: updates todo 7: changed since loaded
                                        request B: runs again, loads the new todo 7, updates
```

Running it again is safe because an action's outside call comes before its
writes and carries an idempotency key, a value that lets the provider
recognise a repeat. So a rerun repeats the call harmlessly
([Integrations](external-services.md)). There are two exceptions:

- **A changing call with no key.** Once it has gone out, the action isn't
  run again. It stops as `outcome_unknown`, for an operator to look at,
  since sending it again could act twice (see "Parked, never sent again
  blindly" in
  [The seam every outside call goes through](the-seam-every-outside-call-goes-through.md)).
- **An action with several changing calls.** Once its record of the calls
  exists, a rerun replays the recorded calls and keeps the record's
  `clock.now` ([Several outside calls](all-or-nothing-actions-and-concurrent-changes.md#several-outside-calls), below).

After three attempts within a second, the retry budget, the request fails
`unavailable` and the client may try again.

To catch a client that edits a stale copy across two requests, use HTTP's
`If-Match` ([HTTP](http.md)).

## Isolation levels

On PostgreSQL, an action that loads a record runs at the repeatable read
isolation level, where the whole transaction sees one snapshot of the
database. The runtime already retries its "could not serialize" error. An
action that loads no record runs at read committed instead, where each
statement sees what was committed before it began
([Counters and other one-step updates](counters-and-other-one-step-updates.md),
below).

At repeatable read, the snapshot is taken at the action's first
statement, and a record it loaded is the record as of then. An `update`
or `delete` of a row another transaction changed since then is
PostgreSQL's serialisation failure, which the driver reports as a
concurrent change.

The runtime then rolls the transaction back, throws away the attempt's
events, and runs the whole action again from its first line. Its reads
see the other transaction's change, and its new records get new
identities. The budget is three attempts.

A record handed to an action from outside its transaction, as when a test
passes in one an earlier action returned, is checked only for its row
still being there.

## Outside calls hold no transaction

No transaction is open while an action waits on a provider. What an
action reads before its outside call runs in a short read-only snapshot,
which closes before the call goes out. The transaction opens after the
call returns, and the action's writes and any later reads run in it.
Reads between two outside calls get a snapshot of their own.

A write to a record loaded before the call checks that its row is
unchanged since it was loaded, by comparing PostgreSQL's row version,
`xmin`. A change is a concurrent change like any other: the whole action
runs again, and the call repeats harmlessly under its idempotency key.

So a slow provider holds no database connection, and the time it takes
never widens the window in which another request's change forces a rerun.
The guarantee is the one repeatable read gives: every record the action
writes is checked against the copy it loaded.

This is done where every outside call goes through Jadpo's runtime
([The seam every outside call goes through](the-seam-every-outside-call-goes-through.md)):

- **Reads before a call are a snapshot.** An action's transaction still
  begins at its first store operation, at its isolation level. If the
  action has run only reads when a call goes out, that transaction is the
  snapshot. It reads the row versions of the records it loaded (one
  statement per table, `xmin` by `id`) and rolls back, before the call is
  sent. Once the action has written, a call is refused
  (`call_in_transaction`), so no transaction is ever open while a call is
  out. An action with no call keeps its one transaction.
- **The transaction after the call.** The next store operation begins a
  new transaction at the same level, in the same store. Reads after a call
  run in it. If another call comes before any write, those reads were a
  snapshot of their own, and it closes the same way.
- **The row version check.** An update, delete or transition of a row
  loaded in a snapshot that a call closed compares the row's version in
  the write's own `WHERE`. A successful write locks the row and checks it
  in one statement, once per transaction. When no row is changed, a
  `FOR NO KEY UPDATE` probe, run only on that failure, tells whether the
  row has a different version or is gone, which is a concurrent change.
  It runs before the usual refusals for policy, a field, a lifecycle
  condition or a missing row.
- **Nested actions and writes by `id`.** A failed nested action's
  savepoint restores only the version checks whose locks survive its
  rollback. A write by `id` is checked too when the action loaded that row
  before the call, as repeatable read checks any write to a row changed
  since the snapshot. A row is held to the version the action first
  loaded, so a row read again after the call, or in a second snapshot, is
  still checked against that first version.
- **A change reruns the action.** A concurrent change throws the attempt
  away and runs the action again, within the budget of three attempts. Its
  keyed calls take the same steps again, so each repeats under the key it
  was first sent with ([Integrations](external-services.md)). A change found
  at the commit of a route's or a delivery's held transaction does the
  same. A changing call without a key that went out stops the action as
  `outcome_unknown` instead. A reference to a record loaded before a call,
  deleted while the call was out, is a concurrent change too, so the
  rerun's fresh read finds the record gone.
- **A batch's items.** An item of a run whose `each:` action makes outside
  calls is a batch of its own
  ([How a run is kept](how-a-run-is-kept.md)). Its
  calls go out with no transaction open, as any action's do, and its
  writes and the run's position commit after them under these rules. A
  write placed before a later call is refused at compile time
  ([Several outside calls](all-or-nothing-actions-and-concurrent-changes.md#several-outside-calls), below).

## Several outside calls

An action may make up to 8 changing calls, one after another
([Several changing calls in one action](several-changing-calls-in-one-action.md)).
For reads and writes this changes three things and keeps the rest:

- **Writes come after the last call.** The action's transaction opens only
  once its last outside call has returned; a write placed before a later
  call is a compile error naming the call it must follow. No transaction
  is open during any call, as above, and reads between two calls each get
  a snapshot of their own.
- **The runtime keeps its record apart from the action's transaction.**
  Each step is
  recorded before it is sent and its answer after, each in a short
  transaction of the runtime's own in the store the action writes; the
  record is deleted in the action's commit, so a committed action leaves
  none behind.
- **A rerun replays, with the record's time.** When a concurrent change
  reruns the action, recorded steps are replayed without being sent and
  `clock.now` is the time the record holds, not a fresh one; the reads are
  fresh. If a fresh read changes a request the action would send, the
  rerun stops as `diverged` before sending anything, held for an operator
  ([Jobs and events](jobs-and-events.md)), rather than sending a
  different request under a used key.
  Database reads are never recorded, so this check is what keeps a rerun
  honest.

The writes still commit together after every call has answered. So a
write that is refused for good, by a unique rule or a restricted
reference, after a step took effect is warned of at compile time, naming
the fixes: check with a read before the first call, record what happened
and finish in a subscriber, or undo it as another step
([Jobs and events](jobs-and-events.md)).

## A write before a call

A write placed before a later outside call is
[`data.write_before_call`](../errors/data.write_before_call.md). It is
reported at the write and names the last call the write comes before, so
checked code never reaches the runtime's `call_in_transaction` refusal,
which stays as the guard. Each part of the rule takes the most cautious
reading:

- **What writes.** An entity's write; a session or key operation that
  changes one; starting a run (`batches.each` and the file helpers keep
  its row) or cancelling one; and calling an action or fixture that does
  any of these, itself or through an action it calls, since it joins the
  caller's transaction. `emit` writes nothing until the action commits,
  and `batches.status` only reads.
- **What calls.** A provider's operation, a store's `put` or `read`, and
  calling an action or fixture that makes one, itself or through an action
  it calls. A run's `each:` and `finished:` are the run's, never the
  action that starts it.
- **Each way through on its own.** A write on one way through an `if` or
  a `match` and a call on another never meet, and a `return`, `reject` or
  `propagate` ends its way; a loop's body counts as running more than
  once, so a write in it comes before a call after the loop or in its next
  turn.
- **Not declinable.** The order is never a judgement call
  ([Declining a judgement call](declining-a-judgement-call.md)):
  run as written, the call would be refused.

A bakery keeps the price sheet a supplier sends again, marking it
checked: the call first, then one write after it.

```jadpo
use app { sheets }
use bakery/entities { PriceSheet }
use bakery/types { SheetText }
use bakery/failures { SheetNotKept }

/// Keeps the sheet a supplier sent again, then marks it checked: the
/// call first, the write after it.
action recheck_prices(
    price_sheet: PriceSheet,
    sheet_text: SheetText
) fails SheetNotKept -> PriceSheet {
    var sheet: File = attempt sheets.put(encoding.to_utf8(sheet_text), FileType.csv) {
        storage_full: SheetNotKept
    }
    return PriceSheet.update(price_sheet) {
        set: {
            sheet: sheet
            checked: true
        }
    }
}
```
