Skip to content

fix(video-streamer): handle channel close during header write and emit a single terminal frame#1825

Draft
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 1 commit into
masterfrom
fix/webm-streamer-impl-hardening
Draft

fix(video-streamer): handle channel close during header write and emit a single terminal frame#1825
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 1 commit into
masterfrom
fix/webm-streamer-impl-hardening

Conversation

@irvingoujAtDevolution

Copy link
Copy Markdown
Contributor

Summary

Implementation-level hardening of the WebM "shadow" live-streamer (video-streamer crate). The streaming architecture is unchanged; these are correctness fixes to its termination/error handling, surfaced and verified with the crate's (ignored, XMF-gated) integration harness.

Fixes

#0 — spurious server_error on early disconnect (header phase)

A client that disconnected while webm_stream was still writing the initial WebM headers caused webm_stream to return Err. The header-write loop propagated the closed-channel error with a bare ?, while the main encode loop already treated a closed channel as a clean shutdown. The caller (devolutions-gateway/src/streaming.rs) turns that Err into a server_error sent to the client — so any shadow session closed during the brief header phase got a bogus error. The header path now goes through the same closed-channel handling and exits Ok.

This was intermittent (a scheduling race between the disconnect and the header writes); the existing client_disconnect_exits_cleanly test caught it only ~1-in-3 runs.

#5 — duplicate terminal frame on shutdown

In spawn_sending_task, both the message-handler task and the control task independently emitted a terminal frame (End/Error) on most termination paths (external shutdown, client disconnect, error), so the client could receive two terminal frames. A shared AtomicBool now arbitrates: whoever flips it first sends the single terminal frame; the other skips. The clean producer-EOF path still emits exactly one End.

#8 — consolidate closed-channel detection

The closed-channel check was duplicated as a fragile triple downcast in two places. It is now a single ChannelWriterError::is_in_chain. The classifier walks the anyhow error chain and peeks inside io::Error via get_ref() — necessary because io::Error::source() exposes the inner error's source (here, nothing), not the ChannelWriterError itself.

Tests

Two new #[ignore], XMF-gated regression tests in webm_stream_correctness.rs:

  • early_disconnect_during_headers_exits_ok — disconnect during the header phase must exit Ok (red→green against this fix).
  • external_shutdown_emits_single_terminal_frame — at most one terminal frame on shutdown.

Verification:

  • cargo clippy -p video-streamer --tests and --lib --features perf-diagnostics: clean.
  • Full webm_stream_correctness suite (9 tests) passes serialized.
  • An independent code review confirmed the production changes are correct (is_in_chain reasoning, the AtomicBool gate with no zero-frame regression, and the header early-return contract were all verified).

Notes / out of scope

  • The XMF-gated tests need DGATEWAY_LIB_XMF_PATH and a local WebM asset under crates/video-streamer/testing-assets/ (not committed; new crate-level .gitignore keeps it out).
  • Pre-existing, untouched: --features bench fails to compile (bench_support.rs:74 calls WebmPositionedIterator::new with 1 arg vs 3). Not addressed here.
  • Larger items flagged during diagnosis but intentionally left out (not reproduced as production bugs): the byte-offset iterator tracking, and making the spawn_blocking decode path interruptible on shutdown.

…t a single terminal frame

Hardening of the WebM shadow streamer surfaced by the integration harness.

- Header-write path now treats a closed destination channel as a clean shutdown
  (Ok), matching the main encode loop. Previously a client that disconnected while
  the initial WebM headers were still being written caused `webm_stream` to return
  Err, which the caller turned into a spurious server_error to the client.
- spawn_sending_task now arbitrates the terminal frame with a shared AtomicBool so
  the client receives at most one End/Error frame. Both the message-handler and the
  control task could previously emit one on the same termination path.
- Consolidate the closed-channel detection (previously a fragile triple downcast in
  two places) into ChannelWriterError::is_in_chain. The classifier walks the anyhow
  chain and peeks inside io::Error via get_ref(), since io::Error::source() exposes
  the inner error's source rather than the ChannelWriterError itself.

Adds two ignored, XMF-gated regression tests covering both fixes.
@github-actions

Copy link
Copy Markdown

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant