Skip to content

Stop drain_proxy on subscriber arrival to prevent relay queue leak#233

Merged
aliev merged 3 commits intomainfrom
fix/drain-proxy-cleanup
Apr 3, 2026
Merged

Stop drain_proxy on subscriber arrival to prevent relay queue leak#233
aliev merged 3 commits intomainfrom
fix/drain-proxy-cleanup

Conversation

@aliev
Copy link
Copy Markdown
Member

@aliev aliev commented Apr 2, 2026

Why

After #232, add_track_subscriber calls blackhole.stop() to clean up the drain. But blackhole.stop() only cancels the internal recv task - it doesn't remove the drain_proxy from relay.__proxies. The relay worker continues putting frames into the dead proxy's unbounded queue, causing OOM for agents with video processors.

Changes

  • Store drain_proxy alongside blackhole and drain_task in _video_drains tuple
  • Call drain_proxy.stop() in add_track_subscriber to remove proxy from relay __proxies
  • Keep blackhole.stop() to cancel internal task blocked on queue.get()
  • Keep drain_task.cancel() as safety net
  • Update tests to verify drain_proxy.stop() is called

How the cleanup works

drain_proxy.stop()     → removes from relay.__proxies, no more frames queued
blackhole.stop()       → cancels internal task, unblocks queue.get()
drain_task.cancel()    → safety net

Companion PR

Summary by CodeRabbit

  • Bug Fixes

    • Improved video cleanup when removing subscribers: all video drain components are now stopped/canceled and any remaining drain state is removed when tracks end, reducing lingering media resources and improving stability.
  • Tests

    • Updated unit tests to validate the new drain-stop behavior and the updated drain registry handling.

blackhole.stop() alone doesn't remove the drain_proxy from
relay.__proxies. Relay continues putting frames into the dead
proxy's queue, causing unbounded memory growth.

Now add_track_subscriber calls drain_proxy.stop() (removes from
relay) + blackhole.stop() (cancels internal recv task) + drain_task.cancel()
(safety net).
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57ff1f26-500b-4ce8-addc-2262d6bc041f

📥 Commits

Reviewing files that changed from the base of the PR and between 2b14f8a and d7f50c6.

📒 Files selected for processing (1)
  • getstream/video/rtc/pc.py

📝 Walkthrough

Walkthrough

Refactors SubscriberPeerConnection's incoming-video draining: _video_blackholes is replaced with _video_drains storing (MediaBlackhole, asyncio.Task, drain_proxy). Drain proxies are created at on_track time and explicitly stopped during subscriber teardown; ended tracks remove remaining drain state.

Changes

Cohort / File(s) Summary
Drain Registry Refactor
getstream/video/rtc/pc.py
Replaced _video_blackholes with _video_drains to store (MediaBlackhole, asyncio.Task, MediaStreamTrack). Create and store drain proxy at on_track when _drain_video_frames is enabled. add_track_subscriber now pops the triple, calls blackhole.stop(), drain_proxy.stop(), and cancels the drain task. handle_track_ended removes any remaining drain state for the ended track.id.
Test Adjustments
tests/rtc/test_subscriber_drain.py
Tests initialize pc._video_drains = {} and update mocked entries to (blackhole, Mock(), drain_proxy). Assertions now verify drain_proxy.stop() was called and that the track_id is removed from _video_drains. The "no drain" case remains unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble code in tidy rows,
Three friends tucked where the river flows,
When tracks depart I hop and stop —
blackhole, proxy, task all drop,
A clean small patch — behold my nose!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Stop drain_proxy on subscriber arrival to prevent relay queue leak' directly and accurately summarizes the main change: explicitly stopping the drain proxy when a subscriber is added to prevent a relay queue leak.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/drain-proxy-cleanup

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.

@aliev aliev changed the title fix: stop drain_proxy on subscriber arrival to prevent relay queue leak Stop drain_proxy on subscriber arrival to prevent relay queue leak Apr 2, 2026
@aliev aliev marked this pull request as ready for review April 3, 2026 07:21
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/rtc/test_subscriber_drain.py (1)

25-42: Test uses mocks, which the coding guidelines discourage.

The guidelines state: "Do not use mocks or mock objects in tests unless directly requested." However, this test is verifying internal cleanup mechanics (drain_proxy.stop() being called) that would be difficult to test via integration testing without setting up full WebRTC connections.

If mock usage was an intentional exception for this specific scenario, please disregard. Otherwise, consider whether an integration-style test could verify the memory leak fix by checking proxy counts on the relay.

As per coding guidelines: "Do not use mocks or mock objects in tests unless directly requested"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/rtc/test_subscriber_drain.py` around lines 25 - 42, The test
TestAddTrackSubscriberStopsDrain uses Mock/AsyncMock for internal objects
(_video_drains entries and blackhole/drain_proxy) which violates the "no mocks
in tests" guideline; either convert the test to an integration-style assertion
that exercises add_track_subscriber and verifies cleanup via observable behavior
(e.g., create a real MediaRelay and real Track-like objects and assert the
relay/proxy counts decrease or that the track_id is removed from
subscriber_pc._video_drains after add_track_subscriber), or explicitly mark this
test as an allowed exception with a short comment explaining why mocks are
necessary; update references to add_track_subscriber, MediaRelay,
subscriber_pc._video_drains, blackhole.stop and drain_proxy.stop to use real
implementations or document the exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/rtc/test_subscriber_drain.py`:
- Around line 25-42: The test TestAddTrackSubscriberStopsDrain uses
Mock/AsyncMock for internal objects (_video_drains entries and
blackhole/drain_proxy) which violates the "no mocks in tests" guideline; either
convert the test to an integration-style assertion that exercises
add_track_subscriber and verifies cleanup via observable behavior (e.g., create
a real MediaRelay and real Track-like objects and assert the relay/proxy counts
decrease or that the track_id is removed from subscriber_pc._video_drains after
add_track_subscriber), or explicitly mark this test as an allowed exception with
a short comment explaining why mocks are necessary; update references to
add_track_subscriber, MediaRelay, subscriber_pc._video_drains, blackhole.stop
and drain_proxy.stop to use real implementations or document the exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 851c8717-fc5a-4e42-8561-3654685272d5

📥 Commits

Reviewing files that changed from the base of the PR and between 4e2e147 and 2d07fff.

📒 Files selected for processing (2)
  • getstream/video/rtc/pc.py
  • tests/rtc/test_subscriber_drain.py

Single None check on the tuple instead of checking each element.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@getstream/video/rtc/pc.py`:
- Around line 211-217: handle_track_ended() currently clears track_map and
video_frame_trackers but does not clean up entries in _video_drains, leaking
(blackhole, drain_task, drain_proxy) for tracks that end before
add_track_subscriber() is called; update handle_track_ended() to mirror the
cleanup in the other branch by popping the track_id from _video_drains, and if
present call blackhole.stop() (schedule with asyncio.create_task if needed),
call drain_proxy.stop(), and cancel drain_task to fully release resources
associated with that track. Ensure you reference the same tuple semantics used
where video_drain is unpacked into blackhole, drain_task, drain_proxy so
behavior matches add_track_subscriber() cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1d3e18f-8755-4e5e-8647-fa971f6b814c

📥 Commits

Reviewing files that changed from the base of the PR and between 2d07fff and 2b14f8a.

📒 Files selected for processing (1)
  • getstream/video/rtc/pc.py

If a participant leaves before anyone subscribes to their video,
the drain entry stays pinned in _video_drains for the lifetime
of the peer connection.
@aliev aliev merged commit f3972a6 into main Apr 3, 2026
30 of 31 checks passed
@aliev aliev deleted the fix/drain-proxy-cleanup branch April 3, 2026 19:35
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