feat: the bind-first establisher + accept dispatcher (shape 3, ADR-008 as amended)
- BindFn + BoundListener: the platform-boundary bind seam (the
protocol never binds, OQ-TN-04); the opaque listener rides the plan
- bindfirst_establisher: registry namespace gate -> token mint
(non-zero-seeded AtomicU32, per-open fresh, per-producer unique,
token 0 never minted — the wrap shape unit-pinned) -> bind ->
`bound` + `listen` reply fields -> the BindFirstPlan plan; the
typed errors pass through (bind failure dial_failed, exhaustion
resource_shortage, registry miss unknown_resource)
- BindFirstAcceptLoop: the type-erased accept closure (receives the
opaque listener; yields (peer, halves) per accept; Err is
loop-critical)
- ForwardedOpener: the per-accept forwarded-open caller — the call
plane (call_with_payload on channel 0 + manager.adopt_channel; the
reverse POC's open_reverse_channel shape)
- make_bindfirst_accept_handler: the accept dispatcher — accept_bi
first (R-02 telemetry), the idle listen-stream EOF sentinel cancels
the loop + tears the listener down (select!), per-accept pump_bidi
tasks tracked by the handler's JoinHandle (awaited on loop exit —
a forwarded pump never outlives its listener), per-accept failure
isolation (log + drop; v1 no retry), loop-critical accept errors
end the loop
- register_tunnel_bindfirst_openable: same spec + tunnel:open scope
gate (the per-registration establisher posture); the injected
seams ride BindFirstInjections {bind, accept_loop, open_forwarded}
(8-arg sketch -> the bundle; clippy too_many_arguments) + the
optional CF-006 identity witness (witness_establisher wrapper)
- harness: RegistrationMode::BindFirst + wire_bindfirst; the consumer
hook gained the accept-side identity propagation + a shared serving
registry (Topology.consumer_registry exposed); the forwarded-op
serving stub (make_forwarded_stub_handler — correlate by token,
accept the BiStream, pump_bidi against the correlated halves; the
opener-side registration lands with tunnels/forwarded-receipt)
- tests: 6 new integration tests (the full flow e2e — the acceptance
gate incl. the byte round-trip through the whole path, the
dial-open neither-field projection, EOF teardown, per-accept
isolation, typed establishment errors, token discipline) + 6 unit
tests (the mint discipline, the establisher flow at the
Establishment surface, the witness wrapper)
Verified: cargo test green (118 default; 138 --all-features), clippy
--all-targets -D warnings clean (native + wasm32), fmt clean,
wasm32-unknown-unknown check passes, cargo doc --no-deps clean.
This commit is contained in:
1 parent
d586a5d2b8
commit
7d23a89791
6 files changed
+1663
-22
No files matched your search
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: tunnels/bindfirst-producer
|
||||
name: Bind-first establisher + accept dispatcher + registration (ADR-008 shape 3)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [tunnels/bindfirst-params]
|
||||
scope: broad
|
||||
risk: high
|
||||
@@ -171,22 +171,22 @@ pub fn register_tunnel_bindfirst_openable(
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `bindfirst_establisher`: registry gate → mint token → bind →
|
||||
- [x] `bindfirst_establisher`: registry gate → mint token → bind →
|
||||
`bound`+`listen` reply fields → `BindFirstPlan` plan-flow
|
||||
- [ ] The accept dispatcher: `accept_bi` first (R-02 telemetry),
|
||||
- [x] The accept dispatcher: `accept_bi` first (R-02 telemetry),
|
||||
idle-stream EOF cancels the loop + tears the listener down,
|
||||
per-accept `pump_bidi` tasks tracked by the handler JoinHandle
|
||||
- [ ] The forwarded-open caller rides the call plane
|
||||
- [x] The forwarded-open caller rides the call plane
|
||||
(`call_with_payload` + adopt — the reverse POC's shape); the
|
||||
registration wiring documents where the call plane comes from
|
||||
- [ ] `register_tunnel_bindfirst_openable` wires spec + establisher +
|
||||
- [x] `register_tunnel_bindfirst_openable` wires spec + establisher +
|
||||
dispatcher + injections
|
||||
- [ ] Integration tests: the full flow e2e (the acceptance gate),
|
||||
- [x] Integration tests: the full flow e2e (the acceptance gate),
|
||||
reply-field projection, EOF teardown, per-accept isolation,
|
||||
typed establishment errors, token discipline
|
||||
- [ ] Clippy/fmt clean; wasm32 check passes (closures injected; no
|
||||
- [x] Clippy/fmt clean; wasm32 check passes (closures injected; no
|
||||
substrate code in the default crate)
|
||||
- [ ] `cargo test` green
|
||||
- [x] `cargo test` green
|
||||
|
||||
## References
|
||||
|
||||
@@ -205,8 +205,128 @@ pub fn register_tunnel_bindfirst_openable(
|
||||
|
||||
## Notes
|
||||
|
||||
> Agent fills during implementation.
|
||||
- **Closure signatures decided (the design pin):** `BindFn` /
|
||||
`BindFirstAcceptLoop` / `ForwardedOpener` follow the type-erased
|
||||
closure precedent:
|
||||
- `BindFn = Arc<dyn Fn(Substrate, &str) -> BoxFuture<'static,
|
||||
Result<BoundListener, TunnelEstablishError>> + Send + Sync>` —
|
||||
same `(substrate, backing)` form as `DialFn` (the registry
|
||||
backing string rides the same signature; the bind closure stays
|
||||
substrate-agnostic from the protocol's view).
|
||||
- `BindFirstAcceptLoop = Arc<dyn Fn(Arc<dyn Any + Send + Sync>) ->
|
||||
BoxFuture<'static, Result<(SubstrateAddr, TargetHandle),
|
||||
TunnelEstablishError>> + Send + Sync>` — the closure receives the
|
||||
plan's OPAQUE listener (`Arc<dyn Any + Send + Sync>`, no
|
||||
downcast in the protocol: the assembly downcasts to ITS concrete
|
||||
type inside the closure; the same injection pattern as
|
||||
`DialFn`/`AcceptFn`). A returned `Err` is LOOP-CRITICAL (the
|
||||
substrate accept source broke) — the dispatcher ends the loop;
|
||||
per-accept forwarded-open failures never come from this closure.
|
||||
- `ForwardedOpener = Arc<dyn Fn(TunnelForwardedParams) ->
|
||||
BoxFuture<'static, Result<(MpscSendStream, MpscRecvStream),
|
||||
TunnelEstablishError>> + Send + Sync>` — closes over the serving
|
||||
session's `Arc<CallConnection>` + the producer-side
|
||||
`ChannelManager` (the reverse POC's `open_reverse_channel`
|
||||
shape: `call_with_payload` on channel 0 → extract `channel_id` →
|
||||
`manager.adopt_channel`). **Where the call plane comes from:** the
|
||||
assembly layer holds it — it constructed the serving session
|
||||
(the harness's `wire_bindfirst` shows the wiring: the
|
||||
`ForwardedOpener` closure closes over the `Arc<ChannelClient>`
|
||||
and calls `call_open_op` + `adopt_channel`; in production the
|
||||
assembly hands its `CallConnection` the same way).
|
||||
- **Registration signature deviation (documented):** the task sketch
|
||||
had 8 parameters (clippy's `too_many_arguments`); the injected
|
||||
seams bundle into `BindFirstInjections {bind, accept_loop,
|
||||
open_forwarded}` — one coherent injection point passed beside the
|
||||
core pair + the optional witness. Same posture as `DialFn` riding
|
||||
the dial registration. The `identity_witness` seam is wired via a
|
||||
small `witness_establisher` wrapper (records the per-call opener
|
||||
identity, then delegates — the same CF-006 seam the dial/direct
|
||||
variants inline).
|
||||
- **The token mint** (`mint_token`, private): `AtomicU32` counter
|
||||
seeded at 1 inside the establisher's closure state; `fetch_add`
|
||||
returns the PREVIOUS value, so the minted stream is 1, 2, 3, … —
|
||||
token 0 (an illegal wire value) is never minted, and a u32 wrap
|
||||
that lands on 0 re-increments (the loop skips it). Per-open fresh,
|
||||
per-producer unique; unit-pinned including the wrap shape.
|
||||
- **The idle-stream EOF watch:** a 1-byte `read` on the split idle
|
||||
half inside a `select!` against the accept loop — `Ok(0)` (the
|
||||
zero-byte EOF sentinel), a read error, or (defensively) any
|
||||
non-empty read on an idle stream all cancel the loop. The listen
|
||||
channel's stream is never pumped (no `pump_bidi` on it).
|
||||
- **Per-accept failure posture (v1, documented):** a forwarded-open
|
||||
failure (scope-less opener → FORBIDDEN; old opener → NOT_FOUND;
|
||||
correlation miss → handler_error-class) logs a warning and drops
|
||||
that accepted connection — the loop continues; NO retry/backoff
|
||||
(v1 posture per the task).
|
||||
- **Lifetime (R-02):** the handler collects every per-accept
|
||||
`pump_bidi` `JoinHandle` in a `Vec` and awaits them all on loop
|
||||
exit (before the handler task returns) — a forwarded pump never
|
||||
outlives its listener. Handler exit → wrapper teardown → the listen
|
||||
channel reaps → the plan's listener drops with the loop.
|
||||
- **Harness changes (test infrastructure):** `wire_bindfirst`
|
||||
(RegistrationMode::BindFirst); the consumer's install hook gained a
|
||||
shared serving registry + the accept-side identity propagation
|
||||
(the transport identity attaches to channel 0 — the same posture
|
||||
`wire_forward` models; needed because the forwarded opens arrive
|
||||
ON the consumer's serving path); `Topology.consumer_registry`
|
||||
exposed; the forwarded-op serving stub
|
||||
(`register_forwarded_stub_on_consumer` /
|
||||
`make_forwarded_stub_handler` — correlate by `listen` token →
|
||||
accept the `BiStream` → `pump_bidi` against the correlated halves;
|
||||
the opener-side registration shape lands for real with
|
||||
`tunnels/forwarded-receipt`).
|
||||
- **Test-data-plane shape:** the e2e round-trip drives the local
|
||||
BIND client end directly (the correlation map's halves are one end
|
||||
of a raw duplex; the stub's pump bridges forwarded channel ↔ those
|
||||
halves; the accepted side's echo task closes the loop). The
|
||||
forwarded channel on the opener side is wrapper-managed (the
|
||||
serving wrapper allocated it — `adopt_channel` on it a second time
|
||||
fails `ChannelExists`), so the test drives the client end, not a
|
||||
re-adopt.
|
||||
- **EOF-teardown observability:** the test pins the listen channel
|
||||
reaping on the producer side after the opener drops its session
|
||||
(`has_channel` false) and the accept loop's death via the queue
|
||||
(a follow-on push is never consumed).
|
||||
|
||||
## Summary
|
||||
|
||||
> Agent fills this on completion.
|
||||
Implemented the bind-first producer (shape 3, ADR-008 as amended) per
|
||||
the task: `src/producer.rs` — `BindFn` + `BoundListener` (the
|
||||
platform-boundary bind seam); the token mint (non-zero-seeded
|
||||
`AtomicU32`, per-open fresh, per-producer unique, token 0 never
|
||||
minted — the wrap shape unit-pinned); `bindfirst_establisher`
|
||||
(registry namespace gate → mint → bind → `bound` + `listen` reply
|
||||
fields → the `BindFirstPlan` plan; the typed errors pass through —
|
||||
bind failure `dial_failed`, exhaustion `resource_shortage`, registry
|
||||
miss `unknown_resource`); `BindFirstAcceptLoop` (the type-erased
|
||||
accept closure receiving the opaque listener); `ForwardedOpener` (the
|
||||
call-plane forwarded-open caller: call + adopt);
|
||||
`make_bindfirst_accept_handler` (the accept dispatcher: `accept_bi`
|
||||
first, the idle-stream EOF sentinel cancels the loop via `select!`,
|
||||
per-accept `pump_bidi` in spawned tasks tracked by the handler's
|
||||
`JoinHandle`, per-accept failure isolation, loop-critical accept
|
||||
errors end the loop); `register_tunnel_bindfirst_openable` (same
|
||||
spec + scope gate, `BindFirstInjections` bundle + witness seam) —
|
||||
re-exported through `lib.rs`. `tests/harness.rs` —
|
||||
`RegistrationMode::BindFirst` + `wire_bindfirst` (the consumer hook
|
||||
gained the accept-side identity propagation + a shared serving
|
||||
registry, `Topology.consumer_registry` exposed) + the forwarded-op
|
||||
serving stub (`make_forwarded_stub_handler`). `tests/
|
||||
producer_bindfirst.rs` — 6 integration tests: the full flow e2e (the
|
||||
acceptance gate: reply projection → token correlation → forwarded
|
||||
open → byte round-trip through the whole path), the dial-open
|
||||
neither-field projection, EOF teardown (listen reaped, loop gone),
|
||||
per-accept isolation (scope-less opener; the loop serves the next
|
||||
accept), typed establishment errors, and the token discipline (two
|
||||
opens distinct tokens; each dispatcher stamps its own plan's token).
|
||||
Plus 6 unit tests in producer.rs (the mint discipline incl. the wrap
|
||||
shape, the establisher flow at the `Establishment` surface — plan
|
||||
token == reply token — distinct tokens per open, registry miss,
|
||||
bind-error passthrough, the witness wrapper).
|
||||
|
||||
Verified: `cargo test` green (118 default; 138 `--all-features`),
|
||||
clippy `--all-targets -D warnings` clean (native + wasm32), fmt
|
||||
clean, `wasm32-unknown-unknown` check passes (closures injected; the
|
||||
default crate stays wasm-clean), `cargo doc --no-deps` clean.
|
||||
CHANGELOG updated (Unreleased → Added).
|
||||
Reference in new issue
Block a user