feat(review 006 Unit 3): teardown-race log, early-arrival bound docs, count accessor (E-03, E-04, N-2)
- E-03: the open wrapper's handler-exit teardown no longer discards UnknownChannel silently — debug log + benign-race pinning comment (ledger take is the atomic gate; no double-decrement) - E-04: consumer-facing doc note on the 64-parked-chunks observable bound (channel-client.md + EARLY_ARRIVAL_CAP const doc) for tunnel-style push-first producers - N-2: ChannelManager::early_arrival_count() accessor (the observability choice over removing the write-only counter), documented monotonic, with a park/adopt-drain monotonicity test - review 006: Unit 3 marked implemented in Status and remediation plan Verification: 617 tests pass; clippy -D warnings clean (host + wasm32 check); fmt clean; doc clean
This commit is contained in:
1 parent
f8dad9dbc8
commit
36e74cda11
4 files changed
+126
-8
No files matched your search
@@ -108,6 +108,16 @@ struct Inner {
|
||||
/// the open-op response/first-data race). A channel whose adopter never
|
||||
/// arrives leaks its parked chunks until `clear_all`; the cap bounds
|
||||
/// that leak per channel.
|
||||
///
|
||||
/// Consumer-facing bound (review 006 E-04): for a push-first producer
|
||||
/// (dial-then-pump is the common tunnel shape), up to `EARLY_ARRIVAL_CAP`
|
||||
/// chunks per channel can be parked before the consumer's
|
||||
/// `adopt_channel` runs — and chunks past the cap drop silently at the
|
||||
/// consumer (data loss presents as a truncated stream with clean
|
||||
/// framing elsewhere; `dropped_unknown_chunks` /
|
||||
/// `early_arrival_count` are the producer-side observability). A
|
||||
/// tunnel crate's sizing (e.g. a UDP POC's MTU-vs-buffer math) should
|
||||
/// treat 64 parked chunks as the observable bound and adopt promptly.
|
||||
const EARLY_ARRIVAL_CAP: usize = 64;
|
||||
|
||||
impl ChannelManager {
|
||||
@@ -192,6 +202,19 @@ impl ChannelManager {
|
||||
self.inner.dropped_unknown_chunks.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The number of chunks parked in the early-arrival buffer since
|
||||
/// the connection started (monotonic — parked chunks handed to the
|
||||
/// adopter are not subtracted). Together with
|
||||
/// [`Self::dropped_unknown_chunks`] this bounds the two observable
|
||||
/// halves of the open-op-response/first-data race: how many of a
|
||||
/// push-first producer's first chunks were buffered
|
||||
/// (`early_arrival_count`) versus lost to the per-channel cap
|
||||
/// (`dropped_unknown_chunks` — see `EARLY_ARRIVAL_CAP`, review 006
|
||||
/// E-04). Observability only — neither counter drives control flow.
|
||||
pub fn early_arrival_count(&self) -> u64 {
|
||||
self.inner.early_arrival_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The mux handle — for registering new channels' write halves.
|
||||
pub fn mux(&self) -> &MuxHandle {
|
||||
&self.inner.mux
|
||||
@@ -712,6 +735,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `early_arrival_count` observes parked chunks (review 006 N-2 —
|
||||
/// the counter was write-only before the accessor). Monotonic by
|
||||
/// design: it counts chunks parked since connection start; draining
|
||||
/// on adopt does not subtract (paired with `dropped_unknown_chunks`
|
||||
/// it bounds the open-response/first-data race's two halves).
|
||||
#[tokio::test]
|
||||
async fn early_arrival_count_tracks_parked_chunks_monotonically() {
|
||||
let manager = make_manager_with_runner().await;
|
||||
assert_eq!(manager.early_arrival_count(), 0);
|
||||
for _ in 0..3 {
|
||||
manager
|
||||
.route_payload(7, Bytes::from_static(b"parked"))
|
||||
.await;
|
||||
}
|
||||
assert_eq!(
|
||||
manager.early_arrival_count(),
|
||||
3,
|
||||
"each parked chunk increments the counter"
|
||||
);
|
||||
// Adoption drains the buffer into the receiver but does not
|
||||
// decrement (the accessor is an observability counter, not a
|
||||
// live-depth gauge).
|
||||
let (_send, mut recv) = manager
|
||||
.adopt_channel(7, "alk/tty", None)
|
||||
.await
|
||||
.expect("adopt");
|
||||
assert_eq!(
|
||||
manager.early_arrival_count(),
|
||||
3,
|
||||
"adopt-drain does not decrement the monotonic counter"
|
||||
);
|
||||
use tokio::io::AsyncReadExt;
|
||||
let mut buf = [0u8; 6 * 3];
|
||||
recv.read_exact(&mut buf).await.expect("read parked chunks");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn route_payload_to_known_channel_does_not_increment_dropped_counter() {
|
||||
let manager = make_manager_with_runner().await;
|
||||
|
||||
@@ -750,7 +750,23 @@ async fn run_open_wrapper(
|
||||
let teardown_policy = Arc::clone(policy);
|
||||
let task = tokio::spawn(async move {
|
||||
let _ = raw_task.await;
|
||||
let _ = teardown_manager.teardown_channel(id);
|
||||
// Benign-race pin (review 006 E-03): `channel/close`'s
|
||||
// awaited teardown and this handler-exit teardown both
|
||||
// gate on the atomic `opener_ledger().take(id)`, so the
|
||||
// loser of the race gets `UnknownChannel` here and skips
|
||||
// the ledger/policy half — no double-decrement. A
|
||||
// vanished-after-install channel (transport EOF
|
||||
// `clear_all`) is equally benign: `clear_all` drained
|
||||
// the ledger and decremented the policy itself. The
|
||||
// vanished-between-open-and-install case surfaces as the
|
||||
// `set_handler_task` warn below.
|
||||
if let Err(e) = teardown_manager.teardown_channel(id) {
|
||||
tracing::debug!(
|
||||
channel_id = id,
|
||||
error = %e,
|
||||
"open wrapper: handler-exit teardown found no channel to remove (benign when racing channel/close)"
|
||||
);
|
||||
}
|
||||
if let Some(opener_id) = teardown_manager.opener_ledger().take(id) {
|
||||
let opener = Identity {
|
||||
id: opener_id,
|
||||
|
||||
Reference in new issue
Block a user