ErrorsBackground work · rule
step.context
An action makes several changing calls from somewhere a retry can't carry on where it stopped, such as a route whose API doesn't require Idempotency-Key.
Area 12, Background work; not declinable; the next step is usually the human's.
Why Jadpo refuses it
A changing call, such as a store's put or a provider operation with
changes: true, takes effect as soon as it is made. When an action
makes several, Jadpo records each one under the action's key, so a retry
replays the calls already answered instead of making them again. That
only works when the retry has the same key as the first try.
A subscriber's delivery keeps its key across retries, and so does a
route whose API requires the client's Idempotency-Key. A route of an
API that doesn't require it gets a new key each time its client tries
again, so a retry would make every call a second time. Jadpo won't
choose between the fixes for you, because each changes what the route's
clients see.
What it looks like
domains/bakery/routes/price_sheets.jadpo, in a bakery app whose
file_prices keeps a supplier's price sheet in sheets, a folder of
files, and then a copy for the accountant:
use app { sheets }use bakery/types { PriceSheetUpload }use bakery/failures { SheetNotKept }/// Keeps a supplier's price sheet, then a copy for the accountant.action file_prices(sent_sheet: PriceSheetUpload) fails SheetNotKept -> Unit { var sheet: File = attempt sheets.put(encoding.to_utf8(sent_sheet.sheet_text), FileType.csv) { storage_full: SheetNotKept } var invoice_copy: File = attempt sheets.put( encoding.to_utf8(sent_sheet.sheet_text), FileType.csv ) { storage_full: SheetNotKept } assert sheet != invoice_copy}
use bakery/types { PriceSheetUpload }use bakery/actions { file_prices }/// Where suppliers send their price sheets.routes PriceSheets { prefix: /price-sheets /// Keeps a supplier's price sheet. POST / { authentication: RouteAuthentication.none input: PriceSheetUpload status: SuccessStatus.no_content run: file_prices(input) }}
error POST /price-sheets runs file_prices, which makes 2 changing calls, but API main doesn't require Idempotency-Key
--> domains/bakery/routes/price_sheets.jadpo:9:5
|9 | POST / { | ^^^^^^ a retry can't resume them
- reason
- Several changing calls are each recorded under their action's key, so a retry carries on from the last answer instead of sending them again; a route keeps its key across a retry only when its API requires the client's
Idempotency-Key. - next
- Require the key with
idempotency: Idempotency.requiredon APImain, or answer202and make the calls in a subscriber (human) - rule
- step.
context - help
- https://jadpo.
dev/docs/errors/step. context
(human) at the end of the next: line tells your coding agent to stop
and ask you which fix you want, because the choice is yours to make: it
decides whether clients must send Idempotency-Key or wait for the work
in the background.
A schedule's retry is its next run, under a key of its own, and a run
keeps each item to one changing call. So a schedule, and a batches.each
whose action makes several changing calls, are refused the same way, at
the schedule and at the run's call.
How to fix it
The question is what the route's clients should do.
If clients can send Idempotency-Key, require it on API main in
app.jadpo. This app writes no database yet, and the record of the calls
needs one (step.store_required), so the fix
also connects main and names it in events: { store: }:
main: { driver: Driver.postgres url: config.main_url } apis: { main: { prefix: / idempotency: Idempotency.required } } events: { store: main }
A request to POST /price-sheets without the header now answers 400
invalid_request, and the API's OpenAPI document lists the header. A
client that retries with the same key gets the first try's answer, and
file_prices's calls are replayed rather than made twice.
If clients shouldn't wait for the work, have the route answer 202
and make the calls in a subscriber. The route runs a new action,
receive_prices, in domains/bakery/routes/price_sheets.jadpo:
output: SheetReceipt status: SuccessStatus.accepted run: receive_prices(input)
receive_prices announces the sheet with an event, PriceSheetSent,
in domains/bakery/actions/receive_prices.jadpo:
action receive_prices(sent_sheet: PriceSheetUpload) -> SheetReceipt { emit PriceSheetSent { sheet_text: sent_sheet.sheet_text } ...}
A subscriber runs file_prices for each one, in
domains/bakery/subscribers/file_sent_prices.jadpo, and file_prices
now takes the event:
use bakery/actions { file_prices }use bakery/events { PriceSheetSent }/// Files each price sheet a supplier sends.subscriber file_sent_prices { on: PriceSheetSent run: file_prices(event)}
The route now answers 202 with a SheetReceipt once the event is
saved. The two sheets.put calls happen in the subscriber's delivery,
which keeps its key when it is tried again. The event waits in main,
which events: { store: } names, since receive_prices writes no
database. An event's JSON is at most 64 KB, so this suits sheets up to
that size.
Related
Markdown for agents: /docs/errors/step.context.md.Built from Jadpo main at aee2e8f.