Skip to content

fix(tracker): serve /tracker/* from tick-independent snapshot - #50

Merged
vernonstinebaker merged 1 commit into
nullclaw:mainfrom
addadi:fix/tracker-snapshot-endpoints
Sep 28, 2026
Merged

vernonstinebaker merged 1 commit into
nullclaw:mainfrom
addadi:fix/tracker-snapshot-endpoints

Conversation

@addadi

@addadi addadi commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #41

tick() holds state.mutex across synchronous dispatch and NullTickets HTTP calls (heartbeat, claim, reconcile), so /tracker/status, /tracker/tasks, /tracker/stats, and /tracker/task/{id} all blocked for the duration of the tick — easily >10s whenever a dispatch was in flight.

TrackerState now publishes a TrackerSnapshot (running-task JSON array, counts, task_id→detail map) at the end of every tick and once at run() startup, under a dedicated snapshot_mutex held only for pointer swaps — never across I/O. Handlers copy the view under that mutex (never state.mutex) and serve distinguishable errors: 404 tracker-not-configured, 503 no-snapshot-yet, 500 OOM. Wire format is unchanged (formatRunningTask moved to tracker.zig as runningTaskJson). The dispatch path is untouched.

Test proves the decoupling property: copySnapshotView succeeds while the tick mutex is held by another caller. Full suite green (zig build test, 357/357).

tick() holds state.mutex across synchronous dispatch and NullTickets
HTTP calls (heartbeat, claim, reconcile), so /tracker/status, /tasks,
/stats, and /task/{id} all blocked for the duration of the tick —
easily >10s whenever a dispatch was in flight.

TrackerState now publishes a TrackerSnapshot (running-task JSON array,
counts, task_id→detail map) at the end of every tick and once at run()
startup, under a dedicated snapshot_mutex held only for pointer swaps —
never across I/O. Handlers copy the view under that mutex (never
state.mutex) and serve distinguishable errors: 404 tracker-not-
configured, 503 no-snapshot-yet, 500 OOM. Wire format is unchanged
(formatRunningTask moved to tracker.zig as runningTaskJson). The
dispatch path is untouched.

Test proves the decoupling property: copySnapshotView succeeds while
the tick mutex is held by another caller.

Fixes nullclaw#41

@vernonstinebaker vernonstinebaker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes the #41 failure mode we hit firsthand during the #53 investigation: /tracker/status served tick-dependent state, so status reads stalled or lagged exactly when the tracker was mid-tick. This serves from a published snapshot (copy under a short lock, never the tick mutex), with clean 404 (tracker unconfigured) / 503 (no snapshot yet) / 500 semantics. CI green on all four targets. Merging.

@vernonstinebaker
vernonstinebaker merged commit 08d2c77 into nullclaw:main Sep 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: /tracker/* endpoints block for the duration of tick() network I/O

2 participants