review: phase-gate review 001 — spec conformance of the v1 implementation

Findings + review doc: docs/reviews/001-implementation-review.md (the
alkcall house pattern). Closes tunnels/review-core-crates and
tunnels/review-impl.

Findings:
- U-1 [major, NOT fixed] — the local UDP adapter is not the framed
  adapter ADR-003 mandates: codec-framed bytes reach real targets,
  empty datagrams are EOF-shaped at the adapter (tunnel teardown),
  >8 KiB datagrams split, unframed target-initiated datagrams are
  dropped. Executable-pinned by real-socket probes (temporary test,
  deleted after the run). Remediation task: tunnels/fix-udp-framed-adapter.
- C-1 [major, fixed] — AcceptQueue::pop lost wakeup in the
  check→register window (open op hangs to the establishment bound);
  fixed via the tokio notify_waiters contract (Notified created before
  the check each iteration) + multi-thread regression test.
- N-4/N-5/N-7/N-8/N-9/N-10/N-11 [minor, fixed] — serialize TunnelParams
  in open_reverse_channel; stale doc ref; broken doc link; root
  re-exports (TunnelParams/Substrate/tunnel_open_spec/OP_TUNNEL_OPEN);
  README (publish dry-run was failing on the missing readme); harness
  comment; pump_against UDP framing doc note.
- N-2 retracted by executable falsification (codec buffering is
  bounded by construction); N-6 non-finding with rationale.

Verified: 63 tests default / 70 with --features local, 3x repeat-run
clean both configs; clippy -D warnings (all-targets + wasm32); fmt;
doc warning-free; wasm32 check; cargo publish --dry-run passes.
This commit is contained in:
2026-09-08 10:58:11 +00:00
parent 2efac9b609
commit a0f939c6df
10 changed files with 975 additions and 21 deletions
+5
View File
@@ -225,6 +225,11 @@ pub async fn wire_with(
// registration timeout. The generic channel ops registered
// above on the SAME registry are unaffected (per-op
// timeout, ADR-049 §2).
// A fresh ChannelCore over the SAME manager + policy (both
// cores are cheap facades over the same pair — the second
// registration rides the same ledger/cap state; this arm
// is local to the probe so the other arms' `core` binding
// stays untouched).
let core = alkcall::channels::operations::ChannelCore::new(
producer_client.manager().clone(),
alkcall::channels::policy::default_policy(),
+43
View File
@@ -320,3 +320,46 @@ async fn accept_wait_resolves_when_push_arrives_late() {
let reaped = session.close().await;
assert!(reaped);
}
/// The lost-wakeup regression (review 001 C-1): a `push` landing in
/// the wall-clock window between a parked `pop`'s state check and its
/// `Notified` registration must still wake the pop. The window only
/// exists under true thread preemption (a single-threaded executor
/// runs the pop's check and the future's first poll back to back),
/// hence the multi-thread flavor. The fix creates the `Notified`
/// future BEFORE the state check on every iteration (tokio's
/// documented notify_waiters pattern — the counter comparison at
/// first poll captures a pre-registration broadcast); the naive
/// create-after-check shape loses it. Structural: no choreography can
/// force the window deterministically, so this test pins the pattern
/// by hammering it (each round the push lands somewhere in the
/// pop's window; a lost wakeup would surface as the 500ms timeout
/// flake).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pop_wakeup_survives_push_landing_in_the_registration_window() {
for _ in 0..64 {
let queue = AcceptQueue::new();
let queue_popper = queue.clone();
let popper = tokio::spawn(async move {
tokio::time::timeout(std::time::Duration::from_millis(500), queue_popper.pop())
.await
.expect("pop timed out — lost wakeup")
.expect("queue closed early")
});
// The push races the popper's check→register window on the
// second worker: no synchronization, by design.
let (consumer_side, target_side) = tokio::io::duplex(1024);
drop(target_side);
let (c_read, c_write) = tokio::io::split(consumer_side);
queue
.push(TargetHandle {
read: Box::new(c_read),
write: Box::new(c_write),
})
.await
.expect("push");
let handle = popper.await.expect("popper task");
drop(handle);
queue.close().await;
}
}