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:
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user