django-logic 0.13.1
Five fixes from the 0.13.0 adoption review (raised by the gv consumer as
#194–#197) plus #192, the sync analog the 0.13.0 review deliberately deferred.
One is a 0.13.0 regression; the rest harden the release's new surfaces. An
adversarial review pass over this release's own diff then confirmed nine gaps
in the first cut — the largest being that the staleness horizon used the
wrong liveness signal — all fixed and folded into the bullets below. Every
fix is mutation-pinned: reverting it makes its tests fail.
Fixed
-
Regression:
Action.fail_transition's in-flight probe was unguarded
(#194, introduced in 0.13.0). The side-effect that brings the engine to the
failure path may have rollback-poisoned the connection (ATOMIC_REQUESTS,
any caller'satomic), in which case the probe itself raised
TransactionManagementError— replacing the original exception at the
caller and silently skipping both failure hook bundles. A probe failure now
logs, skips thefailed_statewrite (unknown means don't write), and lets
the original exception re-raise with both hook bundles running. -
The transient typing is bounded by one shared liveness classification
(#195,TransitionMessage.in_flight_liveness). A stranded row — one
nothing is driving — used to keep answering "retry shortly" forever:
0.13.0'sTransitionTemporarilyUnavailabletold generic handlers to retry
while hook-path logging sat demoted at WARNING. Liveness now reads the
watchdog's own signals first: an attempt inside its declared
started_at + timeout_secondsbudget (plus slack) is LIVE however old
modifiedis — a healthy 40-minute declared-budget attempt is never
called stranded at minute 16. Otherwise a row whose newest activity is
withinmax(RETRY_MINUTES × (MAX_ERRORS + 1), 15)minutes is live; past
that it is stranded and raises plainTransitionNotAllowed(paging at
ERROR), with likely causes in the message — unscheduled beats
(django_logic.W002), a queue backlog or worker outage longer than the
horizon, or a lost broker message. The classification covers both entry
points: the sync gate and phase 1's constraint rejection (a stranded row
no longer raisesAlreadyInProgresson a background re-drive — the most
likely consumer retry path). A row that completed in the race window keeps
the transient answer: it just finished, so retrying is exactly right. -
The sync
failed_statewriters get the #189 treatment (#192). Both
sync savepoints —Transition.fail_transitionand the Action's
write-under-lock — now passrequire_commit=True, so a silently discarded
write (the receiver-authored suppressed-database-error idiom at the one
spot with no query after it) takes the honest except-branch instead of
logging a falseSET_STATE. The original exception still re-raises
unchanged. The except-branches (all four terminal writers) also restore
the in-memory state attribute the discarded savepoint left refreshed, so
failure hooks and the sync caller never observe a state the database
never had. -
The
LEGACY_EXCEPTION_BASEsmoke probe is airtight (#196). It now
verifies the bridged class preserves the denial message —argsmust
survive exactly and the message must appear instr()(a fork__str__
that formats the preserved message is accepted; a message-eating
__init__that used to boot green and blank every denial, breaking
pickling too, is rejected) — and the__bases__unwind runs on
BaseException, so a fork__init__raisingSystemExit/
KeyboardInterruptduring boot cannot leave the class half-mutated.
Added
django_logic.background.in_flight(instance, process_name='process')
(#197) — a documented probe for shaping "busy, try again shortly" answers
at consumer API seams without poking engine internals or duplicating the
marker filter. It answers the busy question:Trueonly for a LIVE
uncompleted row (same classification as the gates),Falsefor a
stranded one — so a consumer answering 409 on this probe and 400 on plain
TransitionNotAllowedstays consistent with what the engine raises. Racy
by nature (the engine's own guards stay authoritative);Falsewhen the
background app is not installed, without touching the database. The
engine's failure-path write-skip deliberately stays bare existence — an
Action must never clobber an uncompleted row's instance, stranded or not.
The marker filter itself now lives in exactly one place
(TransitionMessage.in_flight_for), so the #184/#186 identity rework will
change it once.