Skip to content

fix(net): sample a closing subscription's lag once more - #4451

Merged
kixelated merged 4 commits into
quest/m1/qos/READMEfrom
quest/m1/qos/final-lag-sample
Sep 29, 2026
Merged

kixelated merged 4 commits into
quest/m1/qos/READMEfrom
quest/m1/qos/final-lag-sample

Conversation

@kixelated

@kixelated kixelated commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Counters::sample (rs/moq-net/src/stats.rs) samples lag only on Registry::report ticks and prunes dead FrontierInner weak references without sampling them. When an egress subscription closed, the bytes its track produced since the last tick never reached the lag histogram, and a subscription that opened and closed between two ticks was never sampled.

Approach

FrontierInner now takes one last sample(now) into the same counters in Drop. The subscription guard and every in-flight group Delivery hold a strong reference, so it fires once the last of them is gone. sample advances each source's sampled bytes under the frontier lock, so a tick racing the drop finds nothing left to count again. The frontier holds an Arc to its TierCounters until after the sample, so the report's entry pruning cannot discard it first. Each source holds its track's Production strongly (the Weak to the track now only reports liveness), so the final sample still reads a track that died first, whichever of the subscriber's state, its guard, a serve task's group, or its Delivery drops last. Registry::report now decides a tier's counters are retired before reading them out, so a last bump racing a report (this final sample, or any guard's) lands in that readout or the next instead of being retired unreported.

Tests (each fails without the fix): opens and closes between ticks lands its bytes once; closes mid-interval adds exactly the bytes since its last sample; an in-flight Delivery defers the final sample to its own drop; the last subscriber, or a Delivery outliving it, still samples a track the producer already closed. doc/concept/stats.md notes the behaviour. The quest is deleted and its entry removed from the QoS README.

Ordering with #4381 (lag-splice, same base): the drop reuses FrontierInner::sample, so whatever weight that PR defers (unsampled, retired sources) is kept by the final sample automatically, with no separate copy of the logic. The two touch different hunks (this adds an impl Drop, #4381 edits sample/unwatch), so they should merge in either order; if #4381 lands first, the existing tests here still apply unchanged. Until #4381 lands, the pending-weight loss it describes (sample taking unsampled before a source supplies newest) also applies at drop time.

Impact

  • Public API: none.
  • Wire: none; only the lag histogram's values change (a closing subscription's last interval is now counted).

Alternatives

  • Reordering track::Subscriber fields so stats_sub drops before inner: the same hazard exists in the serve tasks (group drops before delivery), so field order would have to be kept right in every holder.
  • Sampling from Subscription::drop: misses bytes still owed while a Delivery outlives the guard, and does not cover a frontier dropped by another path.

Follow-ups

None.

(Written by Claude Sonnet 5.5, updated by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits September 28, 2026 19:36
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome: quest implemented and checked locally (cargo test -p moq-net, clippy, rustdoc). Draft left for maintainer. Open decision: none blocking; landing order against #4381 is free (different hunks), recommend either order.

(Written by Claude Sonnet 5.5)

@kixelated
kixelated marked this pull request as ready for review September 29, 2026 13:21
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T18:56:51.416341Z 13facea New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cff4c75e3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-net/src/stats.rs
/// is gone, so no tick will find this frontier again: record the bytes produced
/// since the last sample now, at the lag they last had.
fn drop(&mut self) {
if let Some((lag, weight)) = self.sample(crate::model::clock::now()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sample before releasing the last source handle

If the producer has already ended and this is the last subscriber, dropping track::Subscriber destroys its inner field before stats_sub because of their declaration order in model/track.rs; that can destroy the cache track before this destructor runs. Since FrontierState stores only a Weak, sample() then fails the upgrade at lines 1921-1922 and discards the source without recording any bytes produced since the previous tick, so the documented final sample is still lost on this common end-of-track path. Ensure a sample occurs while the subscriber still keeps its source alive, and add a regression case where the producer closes before the subscriber. The repository's handle model makes last-handle teardown an expected closure path.

AGENTS.md reference: rs/AGENTS.md:L13-L15

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 5b98e2b at the source rather than by field order: the same drop-order hazard also exists in GroupServe (group drops before delivery), so reordering one struct would not cover it. Each frontier source now holds the track's Production strongly (Arc) and keeps the Weak only for liveness, so the final sample reads a dead track's last bytes regardless of which handle drops last. Regression tests: the_last_subscriber_samples_a_track_its_own_drop_releases and an_in_flight_delivery_samples_a_track_that_already_closed (both fail without the fix).

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Recommendation: MERGE

Positive stats correctness fix: lag was only sampled on registry ticks, so bytes after the last tick (or a sub that opened and closed between ticks) never hit the histogram. Sampling once more in FrontierInner::Drop, deferred until the last strong ref (guard or in-flight Delivery) is gone, and advancing sampled under the frontier lock so a racing tick does not double-count, is the right design. Focused tests cover between-ticks close, mid-interval residual, and Delivery-deferred drop. Complexity is small and worth accurate QoS histograms.

This is an automated review, not the maintainer's decision
(Written by Grok)

The frontier held each source only by a Weak to its track, so when the
track died first (the subscriber's state dropping before its stats guard,
or a group serve dropping its group before its Delivery), the final sample
failed the upgrade and discarded the bytes since the last tick. Sources now
hold the track's Production strongly and use the Weak only for liveness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b98e2bdb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-net/src/stats.rs
Comment on lines +1957 to +1958
if let Some((lag, weight)) = self.sample(crate::model::clock::now()) {
self.counters.publisher.lag.record(lag, weight);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve final samples across report pruning

When the last producer and subscriber handles disappear concurrently with Registry::report, this record can still be lost from the per-broadcast report. report samples and snapshots TierCounters before its later entries.retain; if this destructor runs after that snapshot but finishes before retain checks Arc::strong_count(counters), the lag is updated here and the counters are then retired and removed without refreshing the already-built Report, so no later tick emits the final sample. The new Production Arc fixes the earlier dead-track case, but this snapshot/prune interleaving is fresh evidence that the final-sample guarantee still fails; retain such late updates for the next report and cover synchronized last-handle teardown.

AGENTS.md reference: rs/AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 13facea. The window was general to Registry::report (any guard's last bump, not only lag, could land between the readout and the prune), so the fix is there: each tier now decides it is done (orphaned entry, counters held only by the map) before its readout, with an acquire fence pairing the last holder's decrement. A bump racing the report then lands in this readout, or the counters survive to the next one, which retires them with it. No deterministic regression test: the old window sits inside one lock scope with no hook to interleave a drop into, so reproducing it would need a thread race; the ordering is now structural instead.

(Written by Claude Opus 5.5)

Registry::report read every entry out, then pruned those only the map
still held. A guard whose last bump (such as a frontier's final lag
sample) landed between the two was retired without that bump reaching
any per-broadcast report. Each tier now decides it is done before its
readout, so a racing bump lands in this readout or the next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Landing summary:

  • Fixed the Codex P2 on field order at the source: each frontier source holds its track's Production strongly and uses the Weak only for liveness, so the final sample reads a dead track's last bytes whichever handle drops last (the same hazard existed in the serve tasks, where group drops before delivery). Regression tests fail without it.
  • Fixed the follow-up Codex P2: Registry::report decides a tier is retired before reading it out, so a last bump racing a report lands in that readout or the next.
  • Decision: fixed both inside this PR rather than splitting, since each is small and the PR's guarantee depends on them. No public API or wire change.

Merging on the maintainer's go-ahead, CI green, Codex thumbs-up on 13facea.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit be233e6 into quest/m1/qos/README Sep 29, 2026
9 checks passed
@kixelated
kixelated deleted the quest/m1/qos/final-lag-sample branch September 29, 2026 20:41
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