Skip to content

Stability & latency hardening: bounded stalls, MoQ stale-reset, truthful keyframes - #17

Merged
rldyourmnd merged 24 commits into
mainfrom
improve/stability-latency
Sep 22, 2026
Merged

rldyourmnd merged 24 commits into
mainfrom
improve/stability-latency

Conversation

@rldyourmnd

Copy link
Copy Markdown
Contributor

Summary

Stability/latency hardening pass over the whole workspace, built on the
review-hardening base (#15) with the CI/CD branch (#16) merged in —
this PR contains both, so landing it lands all three.

Transport (rds-net) — the uni demux read each UniHello tag
inline: a peer that opened a stream and never tagged it stalled routing
of every stream behind it. Each accepted stream now gets its own
tag-read task under a 10s bound; failed-send route removal is
channel-identity checked so a stale sender can't clobber a re-claimed
inbox. Regression test stalled_tag_does_not_block_routing.

Desktop media — the doc claimed freshness was enforced "rather than
by stream reset"; now it actually resets: a frame that goes stale while
its stream is still sending is abandoned mid-write (MoQ-style), the
producer's IDR flag re-arms, and undecodable queued deltas are drained.
Keyframes still always finish. Frame streams carry a constant priority
below control (escalating per-frame priorities, which starved in-flight
sends, stay removed). The decoder moved from a function-static Mutex —
which cross-contaminated reference chains between sessions — to
per-session state, and decode failure auto-requests an IDR, rate-limited
against storms. Session ack + frame header/body reads bounded at 30s,
payloads at 32 MiB.

Codec — the keyframe flag was seq % 240 while
intra_frame_period defaulted to auto: the flag could lie to the
collapse that trusts it. It is now read off the emitted Annex-B NALs;
the period is pinned at 240 (~8s at 30fps) as a bound on undecodable
time. set_bitrate was a silent no-op — OpenH264 has no live rate
setter — so changes past a 15% deadband now lazily rebuild the encoder
(whose first frame is a real IDR).

Sync — manifests stream through StreamCDC (memory bounded at one
max-size chunk; proven identical cut points to slice chunking); manifest
scans, journal open/verify and assembly run on the blocking pool;
verified chunks go through a bounded blocking-pool sink; every protocol
read/chunk body is stall-bounded at 300s. Completion counts chunks on
the wire rather than the sink's lagging counter — the old condition
parked the receive loop 300s after the last chunk arrived.

Agent / discovery / relay / CLI — stream hello 15s, registry PUT
verify+store under one write lock (concurrent-PUT regression test),
service locks recover from poisoning, request paths refuse with
HelloAck::Error instead of unreachable!, relay register 15s, CLI
connect 30s + acks 15s.

Test plan

  • cargo fmt --check — clean
  • cargo build --locked --workspace --all-targets — clean
  • cargo clippy --workspace --all-targets --locked -D warnings — all 4 lanes (default, noq/owned-relay, metrics, x11)
  • cargo test --workspace --locked — all targets green, incl. 12/12 sync e2e in ~25s (previously 7×300s stalls)
  • cargo deny check / cargo audit / cargo machete — clean
  • New regression tests: stalled_tag_does_not_block_routing, concurrent_registry_puts_cannot_regress, streaming_manifest_matches_slice, keyframe_flag_matches_emitted_nals, bitrate_change_rebuilds_with_idr

Generated with Devin

rldyourmnd and others added 24 commits September 22, 2026 18:28
…n demux

Every uni stream now opens with a UniHello tag frame (Desktop/Sync/
Audio). The accepting side runs one accept_uni owner per connection
that reads the tag and routes the stream to the consumer registered
for it via Connection::uni_streams — desktop and sync can share a
connection without racing the accept queue. PROTOCOL_VERSION bumps to
3; v2 peers won't interop on uni streams.
…roll; scope input display

- mailbox::Sender notifies on drop — a consumer parked in recv() used
  to sleep forever after the producer's task ended (verified hang).
  Regression test: parked_recv_wakes_when_last_sender_drops.
- X11 scroll clamped to 32 clicks/event: remote deltas are unbounded
  f64s; a huge one looped billions of paired XTEST calls, wedging the
  injector.
- Input events naming a display other than the session's are dropped
  un-acked — the grant scope checked the hello's display but per-event
  display_id was never enforced.
- Frame streams write the UniHello::Desktop tag; the client claims
  tagged streams via conn.uni_streams instead of accept_uni.
…nk streams

- resolve_under(): the canonicalized deepest-existing ancestor of
  every destination must stay under the canonical sync root — a
  symlinked component (managed dotfiles, nix dirs) can no longer
  redirect pulls, the .rds-sync journal, or assembly outside the root.
  Offers are refused up front; the journal dir resolves the same way.
- check_rel_path refuses .rds-sync as a first component — the journal
  namespace is not a peer-writable path.
- Assembly's temp file is dest + '.rds-part' (appended suffix, no
  aliasing a peer's literal *.rds-part name) created exclusively after
  unlinking any stale/planted one.
- ChunkHdr.len is validated against the manifest before sizing the
  receive buffer — a forged u32 no longer forces a multi-GiB alloc.
- Chunk streams carry the UniHello::Sync tag; the receiver consumes
  them via the connection demux.
- E2E: symlink_escape_refused (pull/push/journal-redirect),
  forged_chunk_len_rejected, .rds-sync namespace refusal.
The freshness check ran under a read lock that was dropped before the
store — two concurrent valid PUTs could regress the snapshot. The
write lock now covers verify and store.
Both consume uni streams; the v3 demux routes Desktop- and Sync-tagged
streams to their own consumer. Proves the fix for the pre-v3 race
where two accept_uni callers could swallow each other's streams.
…og; C5/C6 checklists

- architecture.md documents the UniHello tag + uni_streams demux and
  the two-phase (lexical + resolved) sync confinement.
- deployment.md and the agent unit note that PrivateTmp hides
  /tmp/.X11-unix — desktop capture needs the abstract socket, a
  user-level agent, or PrivateTmp dropped.
- CHANGELOG records protocol v3, the confinement/alloc/scroll/display
  fixes and the mailbox liveness fix.
- C5/C6 manual checklists filled honestly: the review they anticipated
  happened and found the bugs these commits fix.
- uni_demux now drops every registered sender on exit — a dead
  connection ends its UniStreams inboxes (recv → None) instead of
  leaving consumers parked forever. rds-sync's receive loop and the
  desktop frame task both rely on None as end-of-stream.
- mailbox::Sender::drop used to notify while its own Arc still
  counted, so a receiver woken cross-thread could read a stale
  strong_count, re-park, and hang — the same bug the wake was meant
  to close. An explicit AtomicUsize sender count now falls before
  notify_one; recv checks it instead of strong_count.
- x11 scroll comment corrected: f64::min ignores NaN, so a NaN delta
  caps at MAX_SCROLL_CLICKS rather than producing zero clicks.
- Regression tests: uni_demux.rs (recv → None on conn death, claim on
  a dead conn ends immediately) and parked_recv_survives_drop_wake_race
  (multi-thread drop/wake stress).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…deny policy

cargo-machete flagged 13 declared dependencies that nothing imports:
serde (agent/desktop/relay), bytes (cli/relay), rand (relay),
tracing (discovery, bench), proptest (sync dev-dep), rds-core (bench),
async-trait (desktop), thiserror and tracing-subscriber (net).
All removed; the workspace still builds and tests green under
--locked.

deny.toml gains the two licenses the iroh transitive tree actually
ships (Unlicense, CDLA-Permissive-2.0) and ignores the two
unmaintained advisories that have no upgrade path (atomic-polyfill
via rustls, paste via iroh-quinn) — with a comment explaining the
bounds so the ignore can't silently absorb future advisories.

`cargo deny check bans licenses advisories sources` and
`cargo audit` now pass locally; `cargo machete` is clean.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ning

Replaces the hand-rolled workflow with callers of
NDDev-OpenNetwork/ci-workflows @ a7b90bd (release 0.1.26):

- ci.yml → rust-ci: locked build, fmt --check, clippy across five
  feature lanes (default, noq/owned-relay, metrics, x11, desktop),
  and a ubuntu+macos test matrix running the workspace suite plus
  feature tests.
- supply-chain.yml → rust-supply-chain: cargo-deny, cargo-audit and
  cargo-machete on every PR/push and weekly, closing the gap where
  deny.toml documented a check no workflow ran.
- codeql.yml → public-codeql: rust (build-mode none) + actions
  analysis; enables code scanning, which default setup reports as
  not-configured.

Top-level permissions remain {}; caller jobs declare only the
scopes their callee needs; ubuntu-latest everywhere (public repo).
actionlint clean; zizmor 1.26.1 pedantic clean.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Tagging X.Y.Z runs resolve → authorize (release environment) →
release-supply-chain, which validates the release contract
(VERSION file == tag, CHANGELOG heading) and publishes the source
archive, SPDX SBOM, SHA256SUMS and SLSA/SBOM attestations.

Adds VERSION (0.1.0, matching Cargo.toml) and a tag ruleset
(refs/tags/X.Y.Z: creation open, deletion + rewrite denied),
applied to the repo as ruleset 23828256.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mirrors the estate convention: grouped minor/patch cargo updates
and action pinning updates with a 7-day cooldown so fresh releases
get a bake-in window.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
YAML `>-` folding only joins lines at the scalar's own indentation;
the extra-indented `--features`/`-- -D warnings` continuations kept
their line breaks, so bash executed `--features` as a command and the
clippy job exited 127. Flatten every continuation to one line.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… test

Path pinning caps concurrent multipath paths at one, but it does not
suppress iroh's direct-path probes at candidates learned through
in-band address exchange. Whether probes land inside the test window
is platform timing — macOS runners observed 4 direct datagrams, Linux
none. The counter is correct to count them; asserting zero tested a
transport-timing property, not counter accuracy.

The C7 gate stands on the relay side: payload datagrams, sent and
received bytes and seen paths must all register via=relay.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
New improvement work stands on both pending PRs: the v3 demux +
confinement hardening and the ci-workflows adoption. CHANGELOG keeps
both Unreleased entries; the merged lock re-adds rds-sync as an
rds-desktop dev-dep (the coexistence test needs it) while keeping
the machete removals.

Generated with Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The demux read each UniHello tag inline, so a peer that opened a uni
stream and never tagged it blocked routing of every stream queued
behind it for the connection's lifetime. Each accepted stream now gets
its own tag-read task bounded by a 10s timeout — a stalled tag costs
one task seconds, not the connection's routing.

Route removal on a failed send is also channel-identity checked now:
a stale sender's failure cannot clobber a kind that was re-claimed by
a new uni_streams() call.

Regression test: stalled_tag_does_not_block_routing.
The decoder lived in a function-static Mutex shared by every session —
two concurrent viewers would cross-contaminate each other's H.264
reference chains. Decode state now lives in a per-session Delivery
owned by the connection's frame task.

A decode failure now requests an IDR (rate-limited at 500ms) so a
corrupt stretch resyncs instead of decoding deltas against a broken
reference until the next periodic keyframe — without letting the
stretch turn into an IDR storm.

Session ack and per-frame header/body reads are bounded at 30s and
frame payloads at 32 MiB, so a peer that opens a tagged stream and
drips bytes parks seconds of budget, not a task forever.
…ority

A frame that went stale while its stream was still sending used to
finish on the wire — bytes the client would drop anyway, consuming
path capacity the fresher frame needs. The writer now selects between
the payload send and the next queued frame: a stale delta in flight is
reset mid-write (MoQ-style), an in-flight keyframe always finishes
(the chain behind it depends on it), and the producer's IDR flag is
re-armed after a reset since the client's delta chain just broke.

The collapse is extracted into a decode-aware helper applied after
dequeue AND after the pacing wait, and empty encode-failure
placeholders are skipped instead of sent as undecodable payloads.

Frame streams now carry an explicit constant priority (i32::MAX/2 —
above QUIC's default, strictly below control). Escalating per-frame
priorities, which starved in-flight sends, are not reintroduced.
The keyframe flag was seq % 240, but intra_frame_period defaulted to
auto — periodic IDRs never fired on schedule, so the flag could mark
deltas as keyframes. The collapse trusts that flag to decide what may
be superseded, so a wrong flag breaks decode chains. The flag is now
read off the emitted Annex-B NAL units (type 5), and the intra period
is pinned at 240 frames (~8s at 30fps) as a bound on undecodable time.

set_bitrate was a silent no-op: OpenH264 has no live rate setter, so
the adaptive controller's decisions never reached the encoder. Changes
past a 15% deadband now queue a lazy rebuild whose first frame is a
real IDR with fresh SPS/PPS — exactly the resync a rate shift wants;
smaller steps ride the token-bucket pacing alone.
manifest_of() loaded whole files into memory; manifest_of_reader /
manifest_of_path stream through StreamCDC instead — memory bounded at
one max-size chunk, with a regression test proving identical cut
points to the slice chunker. Manifest scans, journal open/verify and
assembly now run on spawn_blocking; chunk reads use tokio::fs.

Verified chunks are stored by a dedicated blocking-pool sink behind a
bounded queue, so disk latency never parks an async worker or the wire
pipeline. Completion is counted on the wire (the peer sends exactly
the Need set) instead of the sink's asynchronously lagging counter,
which used to leave the receive loop parked in a 300s stall after the
last chunk arrived. Every protocol read, uni-stream wait and chunk
body is bounded by a 300s stall.

store() returns whether the chunk was newly present and skips the
tmp-write+rename for duplicates; destination seeding gates on
metadata before scanning and hashes streamed.
…anic

A peer that opened a bidi stream and never wrote its StreamHello
parked a task per stream for the connection's lifetime — the read is
now bounded at 15s.

Every state mutex (authz state, watcher, grant id, active grants) now
recovers from poisoning via into_inner — the guarded data is plain
state whose invariants a panic cannot tear, so one panicked holder can
no longer deny service to every later connection.

Both unreachable!() arms on the request path now write an explicit
HelloAck::Error refusal before bailing — a request path must never
panic if its coupling assumption breaks.
/v1/registry PUT did read-verify-drop-write: two racing valid PUTs
could interleave so the older snapshot won. verify_fresh and the store
now run under one write lock, matching /v1/revocations. Regression
test concurrent_registry_puts_cannot_regress races eight snapshots and
asserts the newest lands.

All service locks (registry, revocations, rate limiter, metrics) now
recover from poisoning — they guard plain data a panic cannot tear,
so one panicked holder must not 500 every later directory request.
Relay: a connection that never opened its control stream or sent
Register parked a task per connection; both waits are bounded at 15s.

CLI: endpoint.connect could stall inside hole punching/relay fallback
— now bounded at 30s; every HelloAck wait and the ping echo bounded
at 15s, so a silent peer fails fast instead of hanging the command.
The desktop media section claimed freshness was enforced "rather than
by stream reset" and that frame priorities were removed — both changed
this pass: stale frames reset mid-send, frame streams carry a constant
priority below control (escalating per-frame priorities that starved
in-flight sends stay removed). The sync and stability sections now
describe streamed manifests, the journal sink, wire-counted
completion, and the per-stage stall bounds.
@rldyourmnd
rldyourmnd merged commit 393b84b into main Sep 22, 2026
12 checks passed
@rldyourmnd
rldyourmnd deleted the improve/stability-latency branch September 22, 2026 16:39
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.

1 participant