Skip to content

feat(supervisor): hand over the tail with the subscription so first lines survive (T16c) - #53

Merged
haiquang9994 merged 1 commit into
masterfrom
feat/t16c-first-lines-reach-the-ring
Aug 29, 2026
Merged

feat(supervisor): hand over the tail with the subscription so first lines survive (T16c)#53
haiquang9994 merged 1 commit into
masterfrom
feat/t16c-first-lines-reach-the-ring

Conversation

@haiquang9994

Copy link
Copy Markdown
Collaborator

A service's first lines reached its file but not the ring a reader was given, because
Capture handed out a subscription and nothing else: a broadcast::Receiver gets nothing sent
before it existed, so everything the process said between spawn and relay was lost to the API.

Capture::read now hands over the ring's contents and the subscription under one lock, the
way the daemon's ServiceLog::read always has, and Runner::relay records the tail before it
starts pumping.

The half the task did not name

Taking both under one lock is only correct if nothing can sit in the ring and not yet on the
channel. Sink::accept pushed to the ring, released the lock, and only then broadcast — so
this change alone would have delivered such a line twice: once in the tail, once again a
moment later on the stream. accept now publishes while still holding the ring, which is what
ServiceLog::record has always done and what its own doc calls "the property the whole type
exists for".

The cost is a lock held across a broadcast::Sender::send, which never waits on a receiver — a
subscriber that has fallen behind is told it lagged. The file's lock, the only one that does
I/O, is unchanged and still the outer one.

Tests

Two, written first and watched to fail:

  • a reader takes the tail and the stream with no line falling between them
  • a reader that arrives first is given an empty tail and every line after it

What is not covered: the race across a real service and relay. Capture::record is
#[cfg(test)] and the daemon cannot reach it; opening it would put test-only code in a shipped
crate, which standards/rust.md forbids. The limit is written into the roadmap rather than left
implied.

The module doc's opening sentence — "Capture::subscribe is the whole of what they need from
here" — was the bug stated as documentation. It now says why it is not.

Checks

cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all --check, rustdoc with
-D warnings, supervisor 68 lib tests + 4 suites, daemon all 12 suites. CI green on all ten jobs
(the first bench (ubuntu) failure was the known M3 warm-start bimodality: a rerun of the same
commit moved the median from 11118 ms to 7037 ms with nothing changed).

Phase 1 is now 15/15.

…ines survive (T16c)

Capture::start puts the reader threads on the pipes before it returns, and
Runner::relay subscribed from inside a tokio::spawn the runtime is free not to
poll. A broadcast delivers nothing to a receiver that did not exist when the line
was sent, so every line printed in that window reached current.log and never the
daemon's ring — permanently, not until something caught up. Those are a service's
first lines, the ones that explain a start nobody was watching.

Capture::read hands over what the ring holds together with the subscription,
without releasing the ring, exactly as ServiceLog::read hands a client its tail
and its stream. relay takes it instead of subscribe and records what it was
given before it pumps anything.

The second half was not in the task and is what makes the first half true.
Sink::accept pushed to the ring, released the lock, and only then published, so
taking both under one lock would have handed a line over twice: once in the tail
and once again on the stream. ServiceLog::record has always published while
holding its ring — its doc calls that the property the whole type exists for —
and accept now does the same. The lock is held across a broadcast send, which
never waits for a receiver, so the reader thread pays a bounded push and a
wakeup.

Still no test for the race itself, as the task predicted. What is tested is that
it has nowhere left to happen: a line recorded before the handover is in the tail
and not on the stream, one recorded after is on the stream and not in the tail.
Capture::record is #[cfg(test)] and unreachable from the daemon crate, and making
it reachable would put test-only code in a shipped one.

The module note still said Capture::subscribe was the whole of what a forwarding
reader needs. That sentence was the bug, and now says so.
@haiquang9994
haiquang9994 merged commit 78088e6 into master Aug 29, 2026
19 of 20 checks passed
@haiquang9994
haiquang9994 deleted the feat/t16c-first-lines-reach-the-ring branch August 29, 2026 11:51
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.

1 participant