Skip to content

fix(moq-net): release cached state when a producer is aborted or dropped#1715

Merged
kixelated merged 2 commits into
mainfrom
claude/moq-net-producer-abort-cleanup-nursuv
Jun 14, 2026
Merged

fix(moq-net): release cached state when a producer is aborted or dropped#1715
kixelated merged 2 commits into
mainfrom
claude/moq-net-producer-abort-cleanup-nursuv

Conversation

@kixelated

@kixelated kixelated commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Tearing down a moq-net producer left its cached state intact, so a stale Consumer could pin the buffers in memory forever.

kio keeps the shared state value alive as long as any handle exists, and that includes kio::Weak (it skips bumping the producer/consumer counts but still holds an Arc clone of the state lock, so it pins the value). A late/abandoned TrackConsumer/GroupConsumer, or a Weak held in the broadcast track lookup, kept the whole cached tree alive:

Track.State.groups -> GroupProducer -> Group.State.frames -> FrameProducer -> FrameBuf (heap buffer)

This happened on both teardown paths:

  • Explicit abort (abort()) only flipped kio's close flag and stored the error; it never dropped the cache.
  • Plain drop of all producers (e.g. a publisher task ending without an explicit abort) only flipped kio's close flag too. There were no Drop impls doing any moq-net-level cleanup.

Fix

Release the cache on teardown, treating an abrupt drop like an abort while preserving a clean finish:

On abort():

  • TrackProducer::abort clears groups + duplicates.
  • GroupProducer::abort clears frames + resets cache.
  • BroadcastProducer / BroadcastDynamic::abort clear the track lookup (on top of draining owned dynamic requests).

On drop of the last producer, when not cleanly finished:

  • TrackProducer::drop clears groups + duplicates (kept if finish()/finish_at() was called).
  • GroupProducer::drop clears frames + resets cache (kept if finish() was called).
  • BroadcastProducer::drop / BroadcastDynamic::drop clear the track lookup.

A new kio::Producer::is_last() gates the drop cleanup so it only runs for the final producer, not a dropped clone.

The frame layer needs no change: its payload lives in an Arc<FrameBuf> held directly by the producer/consumer handles, not in the shared kio state, so it is released by refcount when the handles drop.

Behavior

After abort, or after the last producer drops without a clean finish, a consumer that hasn't pulled yet surfaces the terminal error (the abort error, or Error::Dropped) instead of draining the leftover cache. This matches teardown semantics and avoids serving a truncated stream as if complete. A cleanly finished producer keeps its cache so consumers can still drain it (e.g. a relay forwarding a finished track), and child consumers that already pulled a handle (GroupConsumer/FrameConsumer) own independent state and finish reading what they hold.

Public API signatures and wire behavior are unchanged; kio::Producer::is_last() is additive. Targeting main; happy to retarget dev if reviewers consider the drop/abort drain behavior change disruptive.

Test plan

  • cargo test -p kio -p moq-net (347 passed)
  • cargo clippy -p kio -p moq-net --all-targets clean
  • cargo test -p hang (unaffected)
  • New regression tests:
    • kio: producer::is_last_tracks_producer_count
    • abort: track::abort_clears_cached_groups, group::abort_clears_cached_frames, broadcast::abort_clears_track_lookup
    • drop: track::{drop_unfinished_clears_cached_groups, drop_finished_keeps_cached_groups}, group::{drop_unfinished_clears_cached_frames, drop_finished_keeps_cached_frames}, broadcast::drop_clears_track_lookup

Note: moq-relay requires rustc 1.95 and didn't build on the local 1.94.1 toolchain; verified via CI.

(Written by Claude)

Aborting a producer only flipped the kio close flag and set the abort
error; the cached collections in the shared state were left intact. Since
kio keeps the state value alive as long as any handle exists, a stale
Consumer that never drained would pin the entire cache forever:
Track.groups -> GroupProducer -> Group.frames -> FrameProducer -> the
heap-allocated frame buffers.

Drop the cache as part of abort so the buffers are freed immediately
regardless of lingering consumers:

- TrackProducer::abort clears groups and duplicates.
- GroupProducer::abort clears frames and resets cache.
- BroadcastProducer / BroadcastDynamic::abort clear the track lookup
  (in addition to draining the owned dynamic requests).

Consumers that haven't pulled yet now surface the abort error instead of
the leftover cache. Child consumers that already pulled a handle
(GroupConsumer / FrameConsumer) own independent state and can still
finish reading.
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00ea7da7-3f2d-4e04-8f3c-3552f7892ce9

📥 Commits

Reviewing files that changed from the base of the PR and between 8bb944d and cc13b9a.

📒 Files selected for processing (4)
  • rs/kio/src/producer.rs
  • rs/moq-net/src/model/broadcast.rs
  • rs/moq-net/src/model/group.rs
  • rs/moq-net/src/model/track.rs

Walkthrough

This PR extends cache clearing across the model hierarchy by adding it to both abort and drop paths. A new is_last() method is added to Producer<T> to detect when a producer is the final handle remaining. Using this method, GroupProducer, TrackProducer, and BroadcastProducer each gain Drop implementations that clear their cached state (frames, groups/duplicates, and track lookups respectively) when the last producer is dropped without clean finish. All three abort methods are similarly extended to clear their caches before aborting. Tests verify cache clearing on both abort paths and drop paths, including conditional behavior for finished versus unfinished producers.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: fixing memory leaks by releasing cached state when producers are aborted or dropped.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about the problem, solution, behavior changes, and testing approach for the cache release fixes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/moq-net-producer-abort-cleanup-nursuv

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…pped

The abort() cleanup only ran on an explicit abort. Dropping all producers
without aborting (e.g. a publisher task ending) just flipped kio's closed
flag and left the cache intact, so a stale Consumer (or Weak, which pins
the state via an Arc clone) kept the buffers alive forever, the same leak.

Add Drop impls that treat the last producer dropping without a clean
finish like an abort: clear the cache. A cleanly finished producer keeps
its cache so consumers can still drain it.

- TrackProducer::drop clears groups + duplicates when not finished.
- GroupProducer::drop clears frames + resets cache when not finished.
- BroadcastProducer::drop / BroadcastDynamic::drop clear the track lookup.

Gated on a new kio Producer::is_last() so cleanup only runs for the final
producer, not a dropped clone.
@kixelated kixelated changed the title fix(moq-net): release cached state when a producer is aborted fix(moq-net): release cached state when a producer is aborted or dropped Jun 14, 2026
@kixelated
kixelated merged commit 025093e into main Jun 14, 2026
1 check passed
@kixelated
kixelated deleted the claude/moq-net-producer-abort-cleanup-nursuv branch June 14, 2026 04:48
This was referenced Jun 14, 2026
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.

2 participants