apsimo-hostworker 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo_hostworker/__init__.py +134 -0
- apsimo_hostworker/_private_io.py +267 -0
- apsimo_hostworker/admission.py +297 -0
- apsimo_hostworker/catalog.py +754 -0
- apsimo_hostworker/client.py +292 -0
- apsimo_hostworker/compat.py +53 -0
- apsimo_hostworker/conformance/__init__.py +52 -0
- apsimo_hostworker/conformance/__main__.py +29 -0
- apsimo_hostworker/conformance/harness.py +278 -0
- apsimo_hostworker/conformance/suite.py +1109 -0
- apsimo_hostworker/contract.py +402 -0
- apsimo_hostworker/gate.py +446 -0
- apsimo_hostworker/intent.py +221 -0
- apsimo_hostworker/sqlite_store.py +1815 -0
- apsimo_hostworker/store.py +446 -0
- apsimo_hostworker/worker.py +643 -0
- apsimo_hostworker-0.2.0.dist-info/METADATA +24 -0
- apsimo_hostworker-0.2.0.dist-info/RECORD +22 -0
- apsimo_hostworker-0.2.0.dist-info/WHEEL +5 -0
- apsimo_hostworker-0.2.0.dist-info/licenses/LICENSE +21 -0
- apsimo_hostworker-0.2.0.dist-info/top_level.txt +2 -0
- colony_hostworker/__init__.py +4 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""The ActionStore protocol: durable state a governed host worker runs on.
|
|
2
|
+
|
|
3
|
+
This module is the CONTRACT. The numbered invariants below are not
|
|
4
|
+
descriptions of the reference implementation — they are the obligations any
|
|
5
|
+
conforming store must uphold, each one enforced by a named case in
|
|
6
|
+
:mod:`apsimo_hostworker.conformance`. A store that satisfies the method
|
|
7
|
+
signatures but violates an invariant WILL dispatch a second mutation, honor
|
|
8
|
+
a dead approval, or resurrect a consumed authorization under crash or
|
|
9
|
+
concurrency; the conformance suite exists to catch exactly that before a
|
|
10
|
+
host runs live.
|
|
11
|
+
|
|
12
|
+
Vocabulary
|
|
13
|
+
==========
|
|
14
|
+
* *action* — one immutable proposed side effect: identity fields plus a
|
|
15
|
+
canonical-JSON payload pinned by ``payload_sha256`` (UTF-8 canonical-JSON
|
|
16
|
+
convention).
|
|
17
|
+
* *receipt* — one immutable, append-only evidence record attached to an
|
|
18
|
+
action, addressed by ``(action_id, receipt_key)``, its evidence pinned by
|
|
19
|
+
``evidence_sha256``.
|
|
20
|
+
* *lease* — an exclusive, expiring claim (``owner``, ``lease_expires_at``)
|
|
21
|
+
required for every mutating call.
|
|
22
|
+
* *gate* — the receipt of ``kind == "gate"`` holding the owner-approval
|
|
23
|
+
evidence validated by :func:`apsimo_hostworker.gate.validate_owner_gate`.
|
|
24
|
+
* *recovery contract* — the immutable receipt of ``kind ==
|
|
25
|
+
"dispatch_recovery"`` that converts a dispatched action from "ambiguous,
|
|
26
|
+
fail on lease expiry" into "reconcile by bounded GET-only observation".
|
|
27
|
+
|
|
28
|
+
Lifecycle
|
|
29
|
+
=========
|
|
30
|
+
``proposed -> gated -> dispatched -> accepted -> verified -> completed``
|
|
31
|
+
with ``failed`` reachable from every non-terminal state. ``dispatched``
|
|
32
|
+
NEVER returns to ``gated``: the governed subset pins every action to
|
|
33
|
+
``max_attempts == 1``, so once a mutation may have been attempted the only
|
|
34
|
+
forward paths are acceptance or explicit ambiguity. (This is deliberately
|
|
35
|
+
stricter than a general-purpose action queue.)
|
|
36
|
+
|
|
37
|
+
TRANSACTIONAL INVARIANTS — THE CONTRACT
|
|
38
|
+
=======================================
|
|
39
|
+
|
|
40
|
+
I1. IMMUTABLE IDENTITY. ``action_id``, ``idempotency_key``, ``source``,
|
|
41
|
+
``source_ref``, ``action_type``, the payload, ``payload_sha256``,
|
|
42
|
+
``max_attempts``, and ``created_at`` never change after insertion.
|
|
43
|
+
``get_action`` re-derives the payload digest from the stored payload and
|
|
44
|
+
refuses to return a row whose digest no longer matches.
|
|
45
|
+
|
|
46
|
+
I2. APPEND-ONLY RECEIPTS. Receipts are never updated or deleted. Writing
|
|
47
|
+
``(action_id, receipt_key)`` again with byte-identical evidence, kind,
|
|
48
|
+
status, and external_id is an idempotent no-op returning the original;
|
|
49
|
+
with anything else it raises :class:`ActionIdempotencyConflict`.
|
|
50
|
+
|
|
51
|
+
I3. SINGLE-WRITER LEASES. Every mutating method verifies, inside its own
|
|
52
|
+
transaction, that the caller currently holds an unexpired lease on the
|
|
53
|
+
action; otherwise it raises :class:`ActionLeaseConflict` and writes
|
|
54
|
+
NOTHING. Lease checks use the store's clock, never the caller's.
|
|
55
|
+
|
|
56
|
+
I4. IN-TRANSACTION GATE RE-VALIDATION. ``begin_owner_authorized_dispatch``
|
|
57
|
+
takes the gate validator AS A PARAMETER and invokes it INSIDE the same
|
|
58
|
+
transaction that performs the ``gated -> dispatched`` transition,
|
|
59
|
+
passing the durable action projection, the durable receipt list re-read
|
|
60
|
+
within that transaction, and the store's own clock reading. A store
|
|
61
|
+
implementation therefore CANNOT forget to re-validate, and the evidence
|
|
62
|
+
validated is what is committed — not what the caller looked at earlier.
|
|
63
|
+
If the validator raises, returns anything but a
|
|
64
|
+
:class:`~apsimo_hostworker.gate.GateAuthorization`, returns one marked
|
|
65
|
+
``expired``, or returns one whose ``receipt_key`` / ``approval_id`` /
|
|
66
|
+
``decision_id`` differ from the caller's expectations, the transaction
|
|
67
|
+
aborts with no state change and no receipt written.
|
|
68
|
+
|
|
69
|
+
I5. ATOMIC RECOVERY CONTRACT. The ``gated -> dispatched`` transition and
|
|
70
|
+
the insertion of the immutable GET-only recovery contract (bound to the
|
|
71
|
+
exact ``execution_digest`` about to be PUT) commit in ONE transaction —
|
|
72
|
+
never one without the other. A second recovery contract for the same
|
|
73
|
+
action is refused. Together with I4 this is the one-mutation
|
|
74
|
+
guarantee's durable half: any crash after commit leaves an action that
|
|
75
|
+
can only ever be observed, and any crash before commit leaves an action
|
|
76
|
+
whose gate is still unconsumed and whose attempt count is unchanged.
|
|
77
|
+
|
|
78
|
+
I6. ONE MUTATION, EVER. ``attempt_count`` increments exactly once, inside
|
|
79
|
+
``begin_owner_authorized_dispatch``. A ``dispatched`` action is never
|
|
80
|
+
returned by ``lease_next`` and never transitions back to ``gated``.
|
|
81
|
+
The ONLY way to lease it is ``lease_dispatched_observation``, and only
|
|
82
|
+
while a valid recovery contract with remaining budget exists; a
|
|
83
|
+
dispatched action WITHOUT a valid contract is terminalized as
|
|
84
|
+
explicitly ambiguous (``failed``, dead-lettered) when its lease
|
|
85
|
+
expires — it is never silently retried.
|
|
86
|
+
|
|
87
|
+
I7. BOUNDED OBSERVATION. ``begin_dispatched_observation`` durably journals
|
|
88
|
+
observation attempt N (an immutable receipt) BEFORE the caller performs
|
|
89
|
+
any network read, and refuses — terminalizing as ambiguous — once the
|
|
90
|
+
contract's ``max_observations`` or ``observation_deadline`` is reached.
|
|
91
|
+
A corrupt or forged observation journal terminalizes as ambiguous
|
|
92
|
+
rather than granting more attempts.
|
|
93
|
+
|
|
94
|
+
I8. MONOTONE LIFECYCLE. Only the transitions in
|
|
95
|
+
:data:`ALLOWED_TRANSITIONS` are possible, checked inside each
|
|
96
|
+
transaction; terminal states are frozen forever. Every transition
|
|
97
|
+
appends an immutable event record.
|
|
98
|
+
|
|
99
|
+
I9. VERIFIED MEANS DURABLY WITNESSED. ``verify`` refuses unless every
|
|
100
|
+
named qualifying receipt already exists on the same action in the same
|
|
101
|
+
store, so an action can never reach ``verified`` (and then
|
|
102
|
+
``completed``) without its durable acceptance evidence.
|
|
103
|
+
|
|
104
|
+
I10. FAILURE IS TERMINAL AND EVIDENT. ``fail_attempt`` moves the action to
|
|
105
|
+
``failed``, records the error, clears the lease, and dead-letters the
|
|
106
|
+
action in the same transaction. It never re-opens a dispatched action
|
|
107
|
+
for another mutation regardless of the ``retryable`` argument.
|
|
108
|
+
|
|
109
|
+
I11. THE STORE'S CLOCK JUDGES TIME. Expiries, lease deadlines, observation
|
|
110
|
+
deadlines, and the ``now`` handed to the gate validator all come from
|
|
111
|
+
the store's injected clock read inside the transaction. Caller-
|
|
112
|
+
supplied timestamps are data, never authority.
|
|
113
|
+
|
|
114
|
+
Error taxonomy
|
|
115
|
+
==============
|
|
116
|
+
All refusals raise :class:`ActionStoreError` subclasses. Callers treat any
|
|
117
|
+
of them as "no mutation happened here" — which is only true because of
|
|
118
|
+
I3/I4/I8 (refusals roll back whole transactions).
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
from __future__ import annotations
|
|
122
|
+
|
|
123
|
+
from typing import Any, Callable, Mapping, Protocol, Sequence, runtime_checkable
|
|
124
|
+
|
|
125
|
+
from .gate import GateAuthorization
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ActionStoreError(RuntimeError):
|
|
129
|
+
"""Base class: the store refused an operation and wrote nothing."""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ActionNotFound(ActionStoreError):
|
|
133
|
+
"""The requested durable record does not exist."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ActionIdempotencyConflict(ActionStoreError):
|
|
137
|
+
"""An idempotency key or receipt key was reused for different content."""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ActionLeaseConflict(ActionStoreError):
|
|
141
|
+
"""The action is not currently leased by the requesting owner."""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ActionTransitionError(ActionStoreError):
|
|
145
|
+
"""The requested lifecycle transition or binding is not permitted."""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
STATE_PROPOSED = "proposed"
|
|
149
|
+
STATE_GATED = "gated"
|
|
150
|
+
STATE_DISPATCHED = "dispatched"
|
|
151
|
+
STATE_ACCEPTED = "accepted"
|
|
152
|
+
STATE_VERIFIED = "verified"
|
|
153
|
+
STATE_COMPLETED = "completed"
|
|
154
|
+
STATE_FAILED = "failed"
|
|
155
|
+
|
|
156
|
+
ACTION_STATES = frozenset(
|
|
157
|
+
{
|
|
158
|
+
STATE_PROPOSED,
|
|
159
|
+
STATE_GATED,
|
|
160
|
+
STATE_DISPATCHED,
|
|
161
|
+
STATE_ACCEPTED,
|
|
162
|
+
STATE_VERIFIED,
|
|
163
|
+
STATE_COMPLETED,
|
|
164
|
+
STATE_FAILED,
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
TERMINAL_STATES = frozenset({STATE_COMPLETED, STATE_FAILED})
|
|
169
|
+
|
|
170
|
+
# NOTE: no dispatched -> gated edge (invariant I6). The general-purpose
|
|
171
|
+
# queue this was extracted from allows that edge for retryable work with a
|
|
172
|
+
# remaining attempt budget; the governed subset pins max_attempts == 1 and
|
|
173
|
+
# removes the edge entirely so no store bug can re-open a mutation.
|
|
174
|
+
ALLOWED_TRANSITIONS: dict[str, frozenset[str]] = {
|
|
175
|
+
STATE_PROPOSED: frozenset({STATE_GATED, STATE_FAILED}),
|
|
176
|
+
STATE_GATED: frozenset({STATE_DISPATCHED, STATE_FAILED}),
|
|
177
|
+
STATE_DISPATCHED: frozenset({STATE_ACCEPTED, STATE_FAILED}),
|
|
178
|
+
STATE_ACCEPTED: frozenset({STATE_VERIFIED, STATE_FAILED}),
|
|
179
|
+
STATE_VERIFIED: frozenset({STATE_COMPLETED, STATE_FAILED}),
|
|
180
|
+
STATE_COMPLETED: frozenset(),
|
|
181
|
+
STATE_FAILED: frozenset(),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Receipt kinds with reserved semantics (I5/I7); stores must not let callers
|
|
185
|
+
# forge them through generic receipt insertion paths that skip their checks.
|
|
186
|
+
RECOVERY_RECEIPT_KIND = "dispatch_recovery"
|
|
187
|
+
OBSERVATION_RECEIPT_KIND = "dispatch_observation"
|
|
188
|
+
GATE_RECEIPT_KIND = "gate"
|
|
189
|
+
|
|
190
|
+
# The in-transaction gate validator (I4): called by the store as
|
|
191
|
+
# ``gate_validator(action, receipts, now)`` where ``action`` is the durable
|
|
192
|
+
# action projection, ``receipts`` the durable receipt list re-read inside
|
|
193
|
+
# the dispatch transaction, and ``now`` the store's clock reading. It must
|
|
194
|
+
# return a non-expired GateAuthorization or raise.
|
|
195
|
+
GateValidator = Callable[
|
|
196
|
+
[Mapping[str, Any], Sequence[Mapping[str, Any]], float], GateAuthorization
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@runtime_checkable
|
|
201
|
+
class ActionStore(Protocol):
|
|
202
|
+
"""Durable action store contract — see the module docstring invariants.
|
|
203
|
+
|
|
204
|
+
Method docstrings state each method's obligations; the invariants I1-I11
|
|
205
|
+
above bind every method. ``action`` return values are plain mappings
|
|
206
|
+
with at least: ``action_id``, ``idempotency_key``, ``source``,
|
|
207
|
+
``source_ref``, ``action_type``, ``payload``, ``payload_sha256``,
|
|
208
|
+
``state``, ``attempt_count``, ``max_attempts``, ``next_attempt_at``,
|
|
209
|
+
``lease_owner``, ``lease_expires_at``, ``last_error``, ``result``,
|
|
210
|
+
``created_at``, ``updated_at``, ``terminal_at``. Receipt mappings carry
|
|
211
|
+
at least: ``action_id``, ``receipt_key``, ``kind``, ``status``,
|
|
212
|
+
``external_id``, ``evidence``, ``evidence_sha256``, ``observed_at``,
|
|
213
|
+
``created_at``.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def recover_expired_leases(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
source: str | None = None,
|
|
220
|
+
source_prefix: str | None = None,
|
|
221
|
+
action_type: str | None = None,
|
|
222
|
+
action_ids: Sequence[str] | None = None,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Reap expired leases within the scope, atomically per action.
|
|
225
|
+
|
|
226
|
+
Expired ``gated`` / ``accepted`` / ``verified`` leases are simply
|
|
227
|
+
released (the work is safely repeatable in those states). An
|
|
228
|
+
expired ``dispatched`` lease is released ONLY when a valid recovery
|
|
229
|
+
contract with remaining observation budget exists; otherwise — or
|
|
230
|
+
when the contract is exhausted or its journal invalid — the action
|
|
231
|
+
is terminalized as explicitly ambiguous (I6/I7).
|
|
232
|
+
"""
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
def lease_next(
|
|
236
|
+
self,
|
|
237
|
+
owner: str,
|
|
238
|
+
*,
|
|
239
|
+
lease_seconds: float,
|
|
240
|
+
states: Sequence[str],
|
|
241
|
+
source: str | None = None,
|
|
242
|
+
source_prefix: str | None = None,
|
|
243
|
+
action_type: str | None = None,
|
|
244
|
+
action_ids: Sequence[str] | None = None,
|
|
245
|
+
) -> Mapping[str, Any] | None:
|
|
246
|
+
"""Atomically claim one ready action in ``states`` for ``owner``.
|
|
247
|
+
|
|
248
|
+
Only ``gated``, ``accepted``, and ``verified`` are leaseable here —
|
|
249
|
+
NEVER ``dispatched`` (I6). A row is ready when ``next_attempt_at``
|
|
250
|
+
has passed and no live lease exists. Returns ``None`` when nothing
|
|
251
|
+
is claimable.
|
|
252
|
+
"""
|
|
253
|
+
...
|
|
254
|
+
|
|
255
|
+
def lease_dispatched_observation(
|
|
256
|
+
self,
|
|
257
|
+
owner: str,
|
|
258
|
+
*,
|
|
259
|
+
lease_seconds: float,
|
|
260
|
+
source: str | None = None,
|
|
261
|
+
source_prefix: str | None = None,
|
|
262
|
+
action_type: str | None = None,
|
|
263
|
+
action_ids: Sequence[str] | None = None,
|
|
264
|
+
) -> Mapping[str, Any] | None:
|
|
265
|
+
"""Claim one dispatched action carrying a live recovery contract.
|
|
266
|
+
|
|
267
|
+
The ONLY way a dispatched action becomes leaseable, and only for
|
|
268
|
+
GET-only reconciliation (I6). Dispatched rows whose contract is
|
|
269
|
+
exhausted or invalid are terminalized as ambiguous instead of being
|
|
270
|
+
returned. Never returns an action to ``gated``.
|
|
271
|
+
"""
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
def begin_owner_authorized_dispatch(
|
|
275
|
+
self,
|
|
276
|
+
action_id: str,
|
|
277
|
+
owner: str,
|
|
278
|
+
*,
|
|
279
|
+
gate_receipt_key: str,
|
|
280
|
+
expected_source: str,
|
|
281
|
+
expected_source_ref: str,
|
|
282
|
+
expected_action_type: str,
|
|
283
|
+
expected_payload: Any,
|
|
284
|
+
expected_approval_id: str,
|
|
285
|
+
expected_decision_id: str,
|
|
286
|
+
expected_execution_digest: str,
|
|
287
|
+
observation_window_seconds: float,
|
|
288
|
+
max_observations: int,
|
|
289
|
+
gate_validator: GateValidator,
|
|
290
|
+
) -> Mapping[str, Any]:
|
|
291
|
+
"""Atomically consume one still-live owner gate and open the one
|
|
292
|
+
mutation attempt.
|
|
293
|
+
|
|
294
|
+
In ONE transaction (I4 + I5), the store must:
|
|
295
|
+
|
|
296
|
+
1. verify the caller's live lease (I3) and ``state == gated`` with
|
|
297
|
+
``next_attempt_at`` elapsed;
|
|
298
|
+
2. verify the durable row matches EVERY ``expected_*`` binding —
|
|
299
|
+
source, source_ref, action_type, byte-identical canonical
|
|
300
|
+
payload, and payload digest — so the caller authorized what is
|
|
301
|
+
actually stored, not what it remembered;
|
|
302
|
+
3. verify exactly one ``gate`` receipt exists and its
|
|
303
|
+
``receipt_key`` equals ``gate_receipt_key``;
|
|
304
|
+
4. re-read the durable receipts and CALL ``gate_validator(action,
|
|
305
|
+
receipts, now)`` with the store's clock; require a non-expired
|
|
306
|
+
:class:`~apsimo_hostworker.gate.GateAuthorization` whose
|
|
307
|
+
``receipt_key``, ``approval_id``, and ``decision_id`` equal the
|
|
308
|
+
expected values (the store additionally re-checks the structural
|
|
309
|
+
gate bindings itself — two layers, both inside the transaction);
|
|
310
|
+
5. insert the immutable GET-only recovery contract bound to
|
|
311
|
+
``expected_execution_digest`` with the given observation window
|
|
312
|
+
and budget, refusing if one already exists;
|
|
313
|
+
6. increment ``attempt_count`` (0 -> 1) and transition
|
|
314
|
+
``gated -> dispatched``.
|
|
315
|
+
|
|
316
|
+
Any failure anywhere aborts the whole transaction: no transition,
|
|
317
|
+
no receipt, no attempt consumed. On return, the caller holds the
|
|
318
|
+
one permission that will ever exist to PUT this execution request.
|
|
319
|
+
"""
|
|
320
|
+
...
|
|
321
|
+
|
|
322
|
+
def begin_dispatched_observation(
|
|
323
|
+
self, action_id: str, owner: str, execution_digest: str
|
|
324
|
+
) -> tuple[Mapping[str, Any], Mapping[str, Any] | None]:
|
|
325
|
+
"""Durably consume one bounded GET attempt BEFORE observing (I7).
|
|
326
|
+
|
|
327
|
+
Requires a live lease, ``state == dispatched``, and a valid
|
|
328
|
+
recovery contract matching ``execution_digest``. Journals attempt
|
|
329
|
+
N as an immutable receipt and returns ``(action, receipt)``. When
|
|
330
|
+
the budget or deadline is exhausted — or the journal is invalid —
|
|
331
|
+
terminalizes as ambiguous and returns ``(action, None)``.
|
|
332
|
+
"""
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
def defer_dispatched_observation(
|
|
336
|
+
self,
|
|
337
|
+
action_id: str,
|
|
338
|
+
owner: str,
|
|
339
|
+
execution_digest: str,
|
|
340
|
+
observation_receipt_key: str,
|
|
341
|
+
reason: str,
|
|
342
|
+
delay_seconds: float,
|
|
343
|
+
) -> Mapping[str, Any]:
|
|
344
|
+
"""Record an unresolved GET and either defer or end as ambiguous.
|
|
345
|
+
|
|
346
|
+
``observation_receipt_key`` must name the journal's CURRENT (last)
|
|
347
|
+
attempt — a stale caller cannot defer over a newer attempt. If the
|
|
348
|
+
contract still has budget, releases the lease with
|
|
349
|
+
``next_attempt_at = now + delay_seconds``; otherwise terminalizes
|
|
350
|
+
as ambiguous (I7). Never re-dispatches.
|
|
351
|
+
"""
|
|
352
|
+
...
|
|
353
|
+
|
|
354
|
+
def accept(
|
|
355
|
+
self,
|
|
356
|
+
action_id: str,
|
|
357
|
+
owner: str,
|
|
358
|
+
receipt_key: str,
|
|
359
|
+
kind: str,
|
|
360
|
+
evidence: Any,
|
|
361
|
+
*,
|
|
362
|
+
external_id: str | None = None,
|
|
363
|
+
result: Any = None,
|
|
364
|
+
) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
|
|
365
|
+
"""Record the endpoint's completed projection and move
|
|
366
|
+
``dispatched -> accepted`` atomically with the acceptance receipt
|
|
367
|
+
(I2/I8). Requires a live lease."""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
def verify(
|
|
371
|
+
self,
|
|
372
|
+
action_id: str,
|
|
373
|
+
owner: str,
|
|
374
|
+
receipt_key: str,
|
|
375
|
+
evidence: Any,
|
|
376
|
+
*,
|
|
377
|
+
qualifying_receipt_keys: Sequence[str],
|
|
378
|
+
) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
|
|
379
|
+
"""Move ``accepted -> verified`` only if every qualifying receipt
|
|
380
|
+
already exists durably on this action (I9). Requires a live
|
|
381
|
+
lease."""
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
def complete(
|
|
385
|
+
self, action_id: str, owner: str, result: Any
|
|
386
|
+
) -> Mapping[str, Any]:
|
|
387
|
+
"""Move ``verified -> completed`` with the terminal result, clearing
|
|
388
|
+
the lease (I8). Requires a live lease."""
|
|
389
|
+
...
|
|
390
|
+
|
|
391
|
+
def fail_attempt(
|
|
392
|
+
self, action_id: str, owner: str, error: str, retryable: bool
|
|
393
|
+
) -> Mapping[str, Any]:
|
|
394
|
+
"""Terminalize as ``failed`` with the error, dead-lettering in the
|
|
395
|
+
same transaction (I10). ``retryable`` is recorded but NEVER
|
|
396
|
+
re-opens a dispatched action. Requires a live lease."""
|
|
397
|
+
...
|
|
398
|
+
|
|
399
|
+
def defer_leased(
|
|
400
|
+
self,
|
|
401
|
+
action_id: str,
|
|
402
|
+
owner: str,
|
|
403
|
+
reason: str,
|
|
404
|
+
delay_seconds: float,
|
|
405
|
+
*,
|
|
406
|
+
event_type: str = "deferred",
|
|
407
|
+
) -> Mapping[str, Any]:
|
|
408
|
+
"""Release the lease without changing state or attempt count, for
|
|
409
|
+
work safe to repeat in its current state (``gated`` before the gate
|
|
410
|
+
is consumed, ``accepted``/``verified`` read-only verification).
|
|
411
|
+
Refuses for ``dispatched`` — that path is
|
|
412
|
+
:meth:`defer_dispatched_observation` (I6)."""
|
|
413
|
+
...
|
|
414
|
+
|
|
415
|
+
def list_receipts(self, action_id: str) -> Sequence[Mapping[str, Any]]:
|
|
416
|
+
"""Return all receipts for the action in insertion order."""
|
|
417
|
+
...
|
|
418
|
+
|
|
419
|
+
def get_action(self, action_id: str) -> Mapping[str, Any]:
|
|
420
|
+
"""Return the action, re-verifying its payload digest (I1). Raises
|
|
421
|
+
:class:`ActionNotFound` if absent."""
|
|
422
|
+
...
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
__all__ = (
|
|
426
|
+
"ACTION_STATES",
|
|
427
|
+
"ALLOWED_TRANSITIONS",
|
|
428
|
+
"ActionIdempotencyConflict",
|
|
429
|
+
"ActionLeaseConflict",
|
|
430
|
+
"ActionNotFound",
|
|
431
|
+
"ActionStore",
|
|
432
|
+
"ActionStoreError",
|
|
433
|
+
"ActionTransitionError",
|
|
434
|
+
"GATE_RECEIPT_KIND",
|
|
435
|
+
"GateValidator",
|
|
436
|
+
"OBSERVATION_RECEIPT_KIND",
|
|
437
|
+
"RECOVERY_RECEIPT_KIND",
|
|
438
|
+
"STATE_ACCEPTED",
|
|
439
|
+
"STATE_COMPLETED",
|
|
440
|
+
"STATE_DISPATCHED",
|
|
441
|
+
"STATE_FAILED",
|
|
442
|
+
"STATE_GATED",
|
|
443
|
+
"STATE_PROPOSED",
|
|
444
|
+
"STATE_VERIFIED",
|
|
445
|
+
"TERMINAL_STATES",
|
|
446
|
+
)
|