Skip to content

feat(media): hold a listener pull until the publisher track is active, behind a flag - #794

Draft
realhasanshoaib wants to merge 1 commit into
masterfrom
feat/pull-hold-until-active
Draft

realhasanshoaib wants to merge 1 commit into
masterfrom
feat/pull-hold-until-active

Conversation

@realhasanshoaib

@realhasanshoaib realhasanshoaib commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

A listener's pull that reaches Cloudflare before the speaker's connection is up poisons that track: it returns not_found_track_error, and retries after the speaker connects fail too (11 of 11 such taps in the rig; pulls made after connect almost never failed). SDK 5.2.6 cannot publish again by itself, so the speaker stays silent until they toggle the mic.

Fix

  • The add-tracks endpoint now holds a listener's remote pull while the publication is under 10 s old and Cloudflare does not yet list its track as active. It reads the publisher session (the same call liveness uses) every 200 ms, then forwards the pull either way. The budget is 4 s by default, set with CHALK_MEDIA_PULL_HOLD_BUDGET_MS (200 to 5000), which stays well under the 5.2.6 client's pull timeout of about 8 s.
  • Off by default. CHALK_MEDIA_PULL_HOLD_ENABLED=true turns it on. With it off, the API keeps no state and makes no extra call.
  • A clean pull costs nothing: unknown or older publications skip the hold, and a track seen active is cached. A young track costs one session read, and listeners pulling the same publisher at once share one read per poll.
  • A failed session read or a missing session forwards the pull at once, so the hold never fails a pull.
  • Metrics chalk.media.pull_hold (by outcome) and chalk.media.pull_hold_wait (ms), and an media_publication.pull_hold log with outcome, wait, budget and counts. It logs no participant or connection identifiers.

Limit: Cloudflare marks a published track active when the publish is answered, not when the publisher's ICE/DTLS connects. The rig saw 12 of 29 new publisher connections already listed active before connect, and the first status read returned active for 27 of 29. So this hold most often releases on the first read and may not prevent poisoning on its own; the budget and flag are the part that matter once a real connect signal exists. The code keeps the signal in one place (pullHold.markActive), so a connect signal can replace "active" without touching the endpoint.

What changed

Changed: SFU add-tracks endpoint, publication service, API config, API env example.

Untouched: Sync, clients and SDKs, database, publication liveness and closing rules.

Proof

  • New tests: the pull is held until active and then forwarded; forwarded after the budget; an active track adds no Cloudflare call; flag off changes nothing. They run against a fake clock and fake session reader in mediapublications, and through the real endpoint with a fake Cloudflare plane in httpapi.
  • go vet, gofmt and the touched packages' tests pass locally.
  • Not checked: a rig run with the flag on (this lane did not run the rig), the Postgres integration tests (no database here, so CI covers them), and multi-instance behavior: publishes are remembered in memory, so a pull served by an instance that did not see the publish is forwarded without a hold.

Turn it off by unsetting CHALK_MEDIA_PULL_HOLD_ENABLED; the change is inert while the flag is off.

Created with Claude Sonnet 5.5 in Claude Code.


Devin Review

…, behind a flag

When a listener pulls a track just after its publisher published, the API
now waits (about 200 ms polls, 4 s budget) for Cloudflare to list the
track as active, then forwards the pull either way. Off by default with
CHALK_MEDIA_PULL_HOLD_ENABLED; budget in CHALK_MEDIA_PULL_HOLD_BUDGET_MS.
Only publications younger than 10 s are checked, and an active track is
cached, so clean pulls add no Cloudflare call and no wait.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@realhasanshoaib realhasanshoaib added risk:low Low risk: an agent may merge it once every check and review passes. size:L 100-499 changed lines, not counting tests, lockfiles or generated files. labels Nov 2, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 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-11-02T03:52:11.074143Z 1e157e3 PR opened
ℹ️ 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.

@realhasanshoaib
realhasanshoaib marked this pull request as draft November 2, 2026 03:51
@realhasanshoaib

Copy link
Copy Markdown
Contributor Author

Parked as a draft. Rig notes show Cloudflare lists a track as active before the publisher connects (12 of 29), so holding pulls until active probably doesn't prevent the pre-connect track errors this targets. Waiting on the rig run that measures that before deciding whether to rework or close this.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Devin Review

Comment on lines +216 to +225
if err != nil {
if ctx.Err() != nil {
return pending, ""
}
return pending, pullHoldOutcomeReadFailed
}
if !session.Found {
return pending, pullHoldOutcomeSessionMissing
}
h.markActive(connectionID, session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Missing publisher releases every listener pull

When a pull names multiple young publishers, poll releases every track if any session is missing or its read fails. Other pending tracks reach Cloudflare before becoming active, risking a stuck listener track.

Learn more

An add-tracks request can pull up to 16 remote tracks. hold passes all pending identities into poll, which groups them by publisher connection. If one session is missing or its read fails, poll exits without waiting on the other connections. The endpoint then forwards the whole request through signalSFUTracks. The client supports partial remote responses, so the missing track does not necessarily prevent the remaining tracks from being attempted.

Example: A listener requests Alice's newly published microphone and Bob's departed camera in one request. Bob's session lookup returns Found: false; Alice's microphone is still inactive. The hold ends immediately, and the request can reach Cloudflare while Alice's microphone is unavailable instead of waiting for it.

Recommended fix: Remove only identities belonging to a missing or unreadable publisher from the pending set. Continue polling other publishers until they become active or the shared budget expires. Add a multi-publisher endpoint test where one session is missing and the other becomes active later.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +35 to +47
func (f *pullHoldFixture) read(_ context.Context, connectionID string) (PublisherSession, error) {
f.t.Helper()
if connectionID != f.identity.ConnectionID {
f.t.Fatalf("session read for %q, want %q", connectionID, f.identity.ConnectionID)
}
f.reads++
if f.readErr != nil {
return PublisherSession{}, f.readErr
}
if f.notFound {
return PublisherSession{}, nil
}
return PublisherSession{Found: true, Tracks: []PublisherTrack{{MID: "camera-mid", TrackName: f.identity.TrackName, Active: f.reads > f.activeAfter}}}, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Observed pull failure lacks regression proof

The fake read response invents track status and timing instead of replaying an observed failed pull. The production failure therefore has no regression test grounded in its retained evidence.

Learn more

The repository's review standard requires a production-failure fix to have a test that fails without the fix and uses an observed sanitized fixture, logged request, or retained trace. This fixture synthesizes publisher status from a read count, while the endpoint test likewise synthesizes the response. Neither checks the timing or response shape of the reported failure.

Example: If Cloudflare reports active before the publisher connects, this fake can be configured to turn active later and pass even though the real pull still fails.

Recommended fix: Build a regression from a sanitized captured failed request and provider/session responses, replay the observed ordering, and demonstrate that it fails on the base implementation. If the active signal cannot prevent the observed failure, make that limitation explicit in the proof rather than using fabricated status transitions.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +249 to +254
for _, track := range session.Tracks {
if !track.Active {
continue
}
if entry, ok := h.entries[RemoteTrackIdentity{ConnectionID: connectionID, TrackName: track.TrackName}]; ok {
entry.active = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Active status does not prove connection

InspectSession treats every status except inactive as active. The reported rig observed active tracks before publisher connection, so the hold often releases before the track is pullable.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@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: 1e157e3a57

ℹ️ 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".

func (h *pullHold) releasePoll(connectionID string) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.polling, connectionID)

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 Deduplicate publisher reads for the full polling interval

With the flag enabled, multiple listeners pulling the same inactive publication only share a read while that request is in flight. Deleting the claim immediately afterward lets another listener issue a fresh session read within the same 200 ms interval, so staggered listeners each run their own polling loop instead of sharing one poll per publisher. This amplifies Cloudflare requests during publication fan-out precisely while the hold is needed. Retain a per-publisher next-poll time or share the latest poll result for the interval, including failed or missing-session outcomes.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:low Low risk: an agent may merge it once every check and review passes. size:L 100-499 changed lines, not counting tests, lockfiles or generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant