fix(issue-50): the registry wait admits one job per session and holds it (review aa1bff92 F1, F2)
F1: `clear` is now an admission persisted on the released record - a merge through the engine whose new `guard` re-reads the session's open jobs under the registry lock, so two waiters cannot both be admitted. It is scoped to the attempt by the dispatch token (a redispatch voids it) and reported as `admitted` to every other waiter, which holds the session as a running job does, whatever its FIFO place: a lower id in the same second or a redispatched older loop job now waits. F2: a pending record's liveness window is the dispatcher's signal cadence times three misses (10 s x 3 = 30 s), no longer ack_timeout_sec, which the agent's budget takes down to 1 s. After `clear` a detached hold renews the admitted record's signal until the record leaves pending, its attempt changes, the wrapper is gone, or the wall clock ends. Tests: u1 boundaries, u1d/u2e/u2f/u2g/u3d/u3e for the admission, u4c/u4d for the hold, s1 pins --hold; real herdr: C3/C7/C8 adjusted, C9 is C8's live-owner counterpart with short ack windows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e78ff3ba3
commit
9cbe08e8de
5 files changed
+363
-41
No files matched your search
@@ -284,11 +284,16 @@ def _notification(job_id, kind, **fields) -> Dict[str, Any]:
|
||||
def transition(job_id, request="merge", *, registry_dir=None, expected_status=None,
|
||||
expected_attempt=None, fields=None, source="unknown",
|
||||
notify=None, expected_notification=_UNSET,
|
||||
expected_dispatch_token=None) -> TransitionResult:
|
||||
expected_dispatch_token=None, guard=None) -> TransitionResult:
|
||||
"""Decide and commit one state transition. The only writer of job status.
|
||||
|
||||
Returns a result whose `notifications` have ALREADY been flushed; the field
|
||||
is kept so callers and tests can see what the transition emitted.
|
||||
|
||||
`guard(record)`, when given, runs under the registry lock before the
|
||||
decision and refuses the commit by returning false: a precondition that
|
||||
spans other records (Issue #50 dispatch admission), which the per-record
|
||||
expectations cannot express. A refusal by the guard is not a conflict.
|
||||
"""
|
||||
registry_dir = registry_dir or mqtt_common.DEFAULT_REGISTRY_DIR
|
||||
fields = dict(fields or {})
|
||||
@@ -297,10 +302,13 @@ def transition(job_id, request="merge", *, registry_dir=None, expected_status=No
|
||||
with mqtt_common.registry_lock(registry_dir):
|
||||
record = mqtt_common.load_job(job_id, registry_dir)
|
||||
from_status = record.get("status")
|
||||
decision = _decide(record, request, expected_status, expected_attempt,
|
||||
source=source, fields=fields,
|
||||
expected_notification=expected_notification,
|
||||
expected_dispatch_token=expected_dispatch_token)
|
||||
if guard is not None and not guard(record):
|
||||
decision = _Decision(False, from_status, "guard refused")
|
||||
else:
|
||||
decision = _decide(record, request, expected_status, expected_attempt,
|
||||
source=source, fields=fields,
|
||||
expected_notification=expected_notification,
|
||||
expected_dispatch_token=expected_dispatch_token)
|
||||
|
||||
notifications: List[Dict[str, Any]] = []
|
||||
if decision.commit:
|
||||
|
||||
Reference in new issue
Block a user