Skip to content

live tap: do not stall the capture pipe on the input log or tap I/O - #147

Merged
ConWan30 merged 1 commit into
mainfrom
fix/livetap-no-capture-stall
Nov 11, 2026
Merged

ConWan30 merged 1 commit into
mainfrom
fix/livetap-no-capture-stall

Conversation

@ConWan30

Copy link
Copy Markdown
Owner

Draft — do not merge without Con's word. Practice-only; live tap stays off by default. REAL MATCH stays on v0.3.38. Recording path, receipt.v0, contracts/ and schema/ untouched.

Problem

Practice match match-20261110-185256 (v0.3.43, tap on, pad connected): 1,802 "frame dropped" warnings, 37 CFR dups, ~1.07 s lost on the file side (weave 1465/1465 matched). Match 184049 (tap on, no pad) and non-live practices: 0 drops.

Cause (source-confirmed): the capture stdout thread (ingest → live_tap.observe) took input.lock() for cue context, while the segment-reader thread held that lock across on_segment → seal() → append_bytes (sync_all, plus up to ~1 s of Windows sharing-violation retries). The tap also did UDP sends and live_weave.jsonl writes on the capture thread.

Change

  • livetap: lock-free cue-context snapshot; UDP + live_weave.jsonl moved to a worker via try_send; atomic queue_drops (new /status field live_tap.queue_drops). Frame/PTS counted on the submitting thread so cue f stays correct after drops. Snapshot tracks pad unplug/missing/stall; only attached when the tap is on.
  • inputs: seal() split into prepare / write / finish; engine uses seal_shared() so fsync runs outside the input mutex. Dedicated seal mutex serializes seals; chunk bytes fixed under the lock; chain advances only after a durable write; failure restores record order. on_segment still writes the checkpoint only after the chunk is durable.
  • Bytes identical to v0.3.43: golden test (4 segments, 234,476 bytes, sha256 e577d5af…7fc2) reproduced by both seal() and seal_shared().

Tests (Linux box)

  • cargo build --locked / --release --locked: OK, no warnings.
  • QORTIME=<qortime @ 0204e6d> cargo test --locked: lib 196 (base 189 + 7 new), main 1, contracts 2, haptic_replay 1, redteam 2 — all pass.
  • cargo clippy --all-targets --locked: 0 errors (lib warnings 28 → 29, one chunks_exact style lint matching existing tap code).
  • cargo fmt --check: repo isn't rustfmt-formatted on base either; not reformatted.

Not yet proven

  • Not run on Windows; whether the drops disappear needs a MyTurn practice with the pad connected.
  • Segment-reader thread still fsyncs inputs/manifest/checkpoints (needed for durability); a slow disk can still backpressure.
  • Up to 8 queued tap slices can be lost at exit (practice stream only, never the recording).

v0.3.43 practice live tap, with a pad connected, dropped frames (1,802 dshow
"frame dropped" warnings in match-20261110-185256; 0 without a pad).

Cause: Engine::ingest (capture stdout thread) called LiveTap::observe, which
took the InputLog mutex for weave_ctx() whenever a cue was due, and did the tap
UDP send + live_weave.jsonl write inline. The segment reader thread held that
same mutex across InputLog::seal() -> append_bytes() -> sync_all() (plus the
Windows sharing-violation retry sleeps) on every ~2 s fragment.

Changes (based on QorStream's patch, reviewed and fixed):
- livetap: capture thread only submit()s (try_send into an 8-slot queue); a
  worker thread does UDP + live_weave.jsonl. queue_drops is an AtomicU64 (the
  patch counted drops under the state mutex the worker holds during I/O, which
  could block the capture thread again). Frame numbering is kept on the
  submitting thread so a dropped slice does not shift later cue `f` values off
  the frame_clock.jsonl rule. Worker spawn failure leaves the tap off.
- WeaveCtx snapshot (short mutex) published by InputLog, now also on pad status
  changes (the patch missed unplugged/stalled), and attached only when the tap
  is on, so a tap-off run does no extra work on the HID path.
- inputs: seal split into seal_prepare / SealJob::write / seal_finish.
  Engine::on_segment uses seal_shared: a seal_io mutex serializes seals; the
  chunk bytes, sha256, prev and head are fixed under the input-log lock; the
  append + fsync run with that lock released; the chain moves only after the
  write succeeded; on failure the records go back in front of anything that
  arrived meanwhile. on_segment still returns after the fsync, so the segment
  checkpoint is written after the chunk is durable. Bytes are identical to
  v0.3.43 (golden test captured from 36debdd).

Recording path, manifest/checkpoint chain, receipt.v0, contracts/ and schema/
are unchanged. Live tap stays off by default and practice-lane only.
@ConWan30
ConWan30 marked this pull request as ready for review November 11, 2026 01:44
@ConWan30
ConWan30 merged commit 506a3bc into main Nov 11, 2026
3 checks passed
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