fix(moq-lite): exit run_send_bandwidth when the session closes#1286
Conversation
The bandwidth polling task spawned in Session::new held a clone of the underlying web_transport_trait::Session, but the select! arms inside run_send_bandwidth only waited on producer.closed() / producer.used() / producer.unused(). None of those fire when the wrapping Session drops: the task is the sole Producer, so producer.closed() can never transition, and an unused channel will never become used again. As a result, once the moq_lite::Session dropped (e.g. on connection close), the background task got stuck forever in the outer "wait for a consumer" loop, permanently pinning the cloned session and, transitively, the underlying quinn::Connection and its StreamsState allocations. On relays this manifested as ~1.5-3 MB of live heap per accepted connection growing unboundedly. Fix by adding a top-level select! on session.closed() so the task exits as soon as the transport dies, and extract the polling logic into run_send_bandwidth_inner so the termination conditions sit in one place. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
WalkthroughThe pull request includes a patch version increment for the 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
Summary
run_send_bandwidth(introduced in Add bandwidth estimation for adaptive bitrate control #1208) spawns a detached task that clones the underlyingweb_transport_trait::Session, but itsselect!arms only wait onproducer.closed()/producer.used()/producer.unused(). None of those fire when the wrappingmoq_lite::Sessiondrops — the task is the soleProducer, soproducer.closed()can never transition, and the outer loop'sproducer.used()sits forever waiting for a new consumer on a dead session.quinn::Connection, and itsStreamsState(HashMaps sized tomax_concurrent_*_streams). Onmoq-relaywithDEFAULT_MAX_STREAMS = 10_000, that's ~1.5–3 MB of live heap per connection accumulating unboundedly.use.cdn.moq.dev: ~484 MB retained after 53 min / 328 accepted connections, 79% of it inquinn_proto::endpoint::Endpoint::accept → hashbrown::HashMap::insert → RawTable::reserve_rehashinsideStreamsState::new.Fix
Wrap the existing polling logic in an outer
select!that also listens onsession.closed(), so the task exits as soon as the transport dies. Extract the polling loop intorun_send_bandwidth_innerso all termination conditions live in one place.Bumps
moq-liteto0.15.11.Test plan
just checkpasses locallyquinn_proto::Connection::newis no longer dominant🤖 Generated with Claude Code