Skip to content

Qlv2 alex 2 - #76

Merged
nicoburniske merged 8 commits into
ql-v2from
qlv2-alex-2
Aug 3, 2026
Merged

Qlv2 alex 2#76
nicoburniske merged 8 commits into
ql-v2from
qlv2-alex-2

Conversation

@badicsalex

Copy link
Copy Markdown

Second batch of suggested changes.

badicsalex and others added 5 commits July 30, 2026 12:45
The record seq is the GCM nonce, so no retransmission can reuse one:
every transmission carries a fresh seq. A repeated seq is therefore
network duplication or a replay, never evidence that our ack was lost,
which made the immediate ack it triggered pure reflection. Lost acks
already recover through restore_acked_ranges when our own record times
out.

A replay is also no proof the peer is alive, so it must not refresh the
liveness timers: one captured record replayed every 20s was enough to
hold a session open against a peer that had gone away.

Rejecting replays before decrypt_record saves spending an AES-GCM on
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016YFf732Q7Rq4bf2PJR83LN
The session_* fields were a field-for-field mirror of SessionConfig, so
every new knob had to be declared in five places and copied across by
hand at handshake time.

local_parity and the peer's initial stream receive window can't come
along: the handshake settles them per session, so a nested config would
let a caller set values we then silently overwrite. They move to
SessionParams instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016YFf732Q7Rq4bf2PJR83LN
TrackedRecord kept four parallel records of what a record carried, so
restoring one meant three ad-hoc blocks plus a loop, and asking whether
a record still referenced a stream meant two separate queries. Window
updates and pings are ack-eliciting like the other two, so they belong
in frames, and should_track is then just a non-empty check.

Acking a record no longer requires it to be confirmed sent. An ack that
overtook its own write completion was dropped on the floor, and the
record went on to be retransmitted at the RTO even though the peer
already had it.

The Writable emit in handle_stream_window could never fire, since
nothing between the two send_capacity calls changes the send buffer.

collect_timeouts drains into a Vec so the restore can hang off
SessionState. That costs an allocation, but only on the loss path:
collect sizes from a lower bound of zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016YFf732Q7Rq4bf2PJR83LN
The round-robin cursor was an index into the stream map, so every removal
had to shift it, and StreamOps had to carry the index alongside the id in
order to reap on drop. Keying the cursor on a stream id costs a lookup per
record and leaves reaping as a retain plus a shift_remove.

The drop reap goes with it: reset() sets pending_reset and commit_read()
sets pending_window, and either one disqualifies a stream, so it never
reaped anything. Asserting that in Drop held across the suite.

The reaps in handle_stream_data and handle_stream_reset stay. They look
redundant next to the sweep in collect_timeouts, but receive sweeps before
it processes frames, so a stream that one frame finishes is still around
for the next frame in the same record, and a stale frame there fails as a
protocol error instead of reaching classify_missing_stream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZEW7twTbm7F1dAvfESBNi
@nicoburniske
nicoburniske merged commit 002d9d3 into ql-v2 Aug 3, 2026
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.

2 participants