Background workDelivery and failure
Delivery: at least once, safe to repeat
On this page
Every committed event reaches each of its subscribers at least once.
A crash can deliver it again, so Jadpo makes a repeat harmless: database
work happens once, and outside calls carry keys that let the provider
ignore a repeat. You write nothing for this. Here an action places an order
and emits OrderPlaced, and a subscriber packs the order:
domains/shop/subscribers/pack_placed_order.jadpo:
use shop/actions { pack_order }use shop/events { OrderPlaced }/// Packs each order placed.subscriber pack_placed_order { on: OrderPlaced run: pack_order(event.order_id)}
domains/shop/actions/pack_order.jadpo:
use shop/entities { Order, OrderNotFound }/// Marks an order packed. Running it twice changes nothing more.action pack_order(order_id: Order.id) fails OrderNotFound -> Order { return attempt Order.update(order_id) { set: { packed: true } }}
domains/shop/actions/place_order.jadpo:
use shop/entities { Order }use shop/events { OrderPlaced }/// Places an order, and says so.action place_order() -> Order { var order: Order = Order.create { set: { packed: false } } emit OrderPlaced { order_id: order.id } return order}
domains/shop/events/order_placed.jadpo:
use shop/entities { Order }/// An order was placed.type OrderPlaced = Event { order_id: Order.id}
domains/shop/entities/order.jadpo:
/// An order someone placed.entity Order { id: Uuid packed: Bool policy: { create: [Access.public] read: [Access.system] update: [Access.system] }}
subscriber, event, emit and Event are built in, and every other name
is declared here. OrderNotFound is the failure the compiler provides for
an Order that isn't there. If the app crashes after pack_order commits,
the delivery may start again, finds it was already handled and does
nothing. If pack_order fails on the way, Jadpo decides whether to try
again (Retries and giving up).
At least once
An event committed is delivered to each subscriber at least once. A crash can deliver it again.
Database work happens once
The subscriber's writes commit together with a handled record, a note that this delivery was dealt with. A repeated delivery finds the note and does nothing.
The handled record (jadpo_handled_deliveries, keyed by the delivery's
id) is written after the action's outbox rows, in the store its
transaction is in by then: the store it writes or, if it writes none,
the one events: { store: } names, where its events are. So an action
that only emits keeps one, and a repeat emits nothing twice. An action
that neither writes nor emits keeps none, since running it again changes
nothing in a database. The handled record references nothing in another
store.
Outside calls can repeat
A call to a provider can't join the database transaction. So each delivery has a stable key, made from the event and the subscriber, and the runtime derives from it the idempotency key each provider operation sends (Calling, Several changing calls in one action). A provider that supports idempotency keys ignores the repeat.
Every action repeats as a unit
A changing call is a call that changes something at the provider, such
as taking a payment. A changing call that takes no idempotency key, and
that its package doesn't mark as safe to repeat, must be the only
changing call in its action. Once it has gone out, it is never sent
again blindly. A retry the action would otherwise get, or a rerun after
a concurrent change
(All-or-nothing actions),
stops it as outcome_unknown for an operator to check instead.
Changing calls that do take keys may be several, in order. Each is recorded as it goes, so a rerun replays what already happened rather than sending it again. Writes aren't limited this way: every write to the action's store, across any number of domains, commits together after the last call.
No order between events
No order is promised between events. A subscriber that cares about order reads the current state rather than trusting the event's copy, which is also what makes repeats safe.
A payload is the contract's JSON
An event saved in the outbox, a job's arguments and the values a batch captures are all written as the JSON of their types, by the same codec a request's body goes through. So a standard type keeps its currency, its unit and every decimal place (How a standard value is sent, stored and shown). Every event type is numbered in the plan's JSON table by its identity key, and the outbox writes and reads its payload through that table (Structured values).
A payload is read with its type as the type is when the payload is delivered. An event type's shape is recorded as an API record's is (The event record), and a change that would break an API record, such as a field removed, renamed or narrowed, needs an acceptance. A payload saved before such a change that no longer reads is parked with its problem, never dropped, as a message that keeps failing is (Events from outside).
A delivery reads its payload by the encoding its outbox row records
(Changing a standard type's encoding):
this release's encoding, or none for a payload kept before encodings
were recorded, which is read as encoding 1's. A payload in an encoding
this release doesn't read, such as a later release's after a rollback,
is never guessed at. Its delivery is parked, failed with
payload_unreadable and why, as a payload that no longer reads as its
type is, with its problem: the rule and the field, never the value.
How a delivery runs
Once the action commits, each delivery is left to the runner, which runs it after the response (Running background work). The runner:
- claims the delivery's record in one statement, at the host's time,
marking it
runningwith a lease of what is left of the invocation's budget plus a minute. A delivery another runner holds, or one not yet due, is left alone; - looks for its handled record. A delivery already handled is marked
succeededwithout running again; - otherwise runs the subscriber's action as the system, limited to its event's tenant (Background work and tenants), in a boundary of its own, the unit that commits or rolls back as one, and in a new span of the event's trace;
- records how it ended.
Tracing follows the work through events. A request continues a valid
W3C traceparent it brings, in a span of its own, or starts a trace. An
event keeps the trace context of the action that emitted it, so what a
delivery emits keeps it too.
A delivery to a subscriber the build no longer has fails as a defect. An
event 16 deep in a chain of events fails with event_chain_limit.
Markdown for agents: /docs/delivery-at-least-once-safe-to-repeat.md.Built from Jadpo main at aee2e8f.