nestsClient: revert framesPerGroup default to 1 now that the QUIC fix lands frames
Production sweep_frames_200 confirms received=200/200 missing=[] after the :quic MAX_STREAMS_UNI extension landed ind391ae1d. The framesPerGroup=5 mitigation in NestMoqLiteBroadcaster (commit10ad69f1) is no longer needed and was a workaround for a real QUIC bug; reverting to 1 brings the wire shape in line with the JS reference broadcaster (one Opus frame per moq-lite group → late-join initial gap drops from ≤100ms back to ≤20ms). Tests that still want one-frame-per-group semantics keep their explicit framesPerGroup=1 overrides — they pin the assumption independent of any future default change. The broadcaster's framesPerGroup field kdoc is updated to explain the history (mitigation → root-caused fix → revert). All :quic and :nestsClient JVM tests pass. Plan doc: nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md status flipped to CLOSED with the production-validation numbers.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# QUIC stream cliff against nostrnests.com — investigation plan
|
# QUIC stream cliff against nostrnests.com — investigation plan
|
||||||
|
|
||||||
**Status: ROOT-CAUSED + FIXED.** Our `:quic` client never emitted
|
**Status: CLOSED.** Our `:quic` client never emitted `MAX_STREAMS_*`
|
||||||
`MAX_STREAMS_*` frames to extend the peer-initiated stream-id cap.
|
frames to extend the peer-initiated stream-id cap.
|
||||||
[QuicConnectionConfig.initialMaxStreamsUni] (default `100`) was the
|
[QuicConnectionConfig.initialMaxStreamsUni] (default `100`) was the
|
||||||
*lifetime* maximum the peer could ever open. The relay forwards each
|
*lifetime* maximum the peer could ever open. The relay forwards each
|
||||||
broadcast group as a fresh peer-initiated uni stream, so any
|
broadcast group as a fresh peer-initiated uni stream, so any
|
||||||
@@ -11,9 +11,13 @@ Fixed by tracking [QuicConnection.peerInitiatedUniCount] and emitting
|
|||||||
peer's usage crosses the half-window threshold — same pattern as the
|
peer's usage crosses the half-window threshold — same pattern as the
|
||||||
existing `MAX_DATA` / `MAX_STREAM_DATA` extension.
|
existing `MAX_DATA` / `MAX_STREAM_DATA` extension.
|
||||||
|
|
||||||
The `framesPerGroup = 5` mitigation in [NestMoqLiteBroadcaster] can
|
Production validation: `sweep_frames_200` against `nostrnests.com`
|
||||||
be reverted to `1` to match the JS reference broadcaster's wire
|
went from `received=99/200 missing=[99-199]` (pre-fix) to
|
||||||
shape now that the underlying QUIC bug is gone.
|
`received=200/200 missing=[]` (post-fix). The
|
||||||
|
[NestMoqLiteBroadcaster.DEFAULT_FRAMES_PER_GROUP] mitigation has
|
||||||
|
been reverted from `5` back to `1` so the broadcaster matches the
|
||||||
|
JS reference's wire shape (one Opus frame per moq-lite group → ≤ 20 ms
|
||||||
|
late-join initial gap).
|
||||||
|
|
||||||
**Reproduces against:** `https://moq.nostrnests.com:4443` (production
|
**Reproduces against:** `https://moq.nostrnests.com:4443` (production
|
||||||
relay). Not consistently reproducible against the local
|
relay). Not consistently reproducible against the local
|
||||||
|
|||||||
+23
-24
@@ -49,26 +49,27 @@ class NestMoqLiteBroadcaster(
|
|||||||
* varint-prefixed frames; [MoqLitePublisherHandle.endGroup] FINs
|
* varint-prefixed frames; [MoqLitePublisherHandle.endGroup] FINs
|
||||||
* the stream and the next [send] starts a fresh group.
|
* the stream and the next [send] starts a fresh group.
|
||||||
*
|
*
|
||||||
* **Default = 5 (≈ 100 ms of audio per group).** This is a
|
* **Default = 1 (one Opus frame per group).** Matches the JS
|
||||||
* mitigation for a production loss cliff observed against
|
* reference broadcaster's wire shape and gives any late-joining
|
||||||
* `nostrnests.com`: at one-frame-per-group and 20 ms cadence the
|
* subscriber a sub-20 ms initial audio gap (moq-lite "from-latest"
|
||||||
* client opens 50 uni streams/second, and somewhere around the
|
* semantics — new subscribers pick up at the next group boundary).
|
||||||
* 99th stream the connection enters a state where new uni streams'
|
*
|
||||||
* data stops being delivered to the listener, even though
|
* Earlier versions defaulted to 5 as a mitigation for a production
|
||||||
* `publisher.send` still returns true. Packing 5 frames per group
|
* cliff at frame ~99: the relay would only forward the first ~100
|
||||||
* reduces the stream-creation rate to 10/second, which the prod
|
* uni streams to a listener for the lifetime of the connection.
|
||||||
* sweep (`NostrnestsProdAudioTransmissionTest.sweep_frames_per_group_5`)
|
* The actual root cause was on the listener's *receive* side —
|
||||||
* shows delivers 100/100 frames cleanly. See
|
* our `:quic` never emitted `MAX_STREAMS_UNI` frames to extend
|
||||||
|
* the peer-initiated stream-id cap, so the relay's initial 100
|
||||||
|
* uni-stream allowance was the lifetime maximum. Once
|
||||||
|
* [com.vitorpamplona.quic.connection.QuicConnectionWriter]
|
||||||
|
* started emitting periodic `MAX_STREAMS_*` extensions, packing
|
||||||
|
* stopped being necessary. See
|
||||||
* `nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md`.
|
* `nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md`.
|
||||||
*
|
*
|
||||||
* Trade-off: a brand-new subscriber that attaches mid-broadcast
|
* Set to a higher value to amortise per-stream overhead (CPU,
|
||||||
* picks up at the next group boundary per moq-lite "from-latest"
|
* relay bookkeeping) at the cost of late-join latency: with
|
||||||
* semantics. With 5 frames per group, late joiners miss up to
|
* `framesPerGroup = 5` the late-join initial gap is ≤ 100 ms
|
||||||
* 100 ms of audio. With the original 1 frame per group it was
|
* instead of ≤ 20 ms.
|
||||||
* up to 20 ms. Both are imperceptible for live audio rooms.
|
|
||||||
*
|
|
||||||
* Set to 1 to restore the original behaviour for testing or for
|
|
||||||
* deployments to relays that don't exhibit the stream-cliff bug.
|
|
||||||
*/
|
*/
|
||||||
private val framesPerGroup: Int = DEFAULT_FRAMES_PER_GROUP,
|
private val framesPerGroup: Int = DEFAULT_FRAMES_PER_GROUP,
|
||||||
) {
|
) {
|
||||||
@@ -197,12 +198,10 @@ class NestMoqLiteBroadcaster(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Default moq-lite group size = 5 Opus frames ≈ 100 ms of audio.
|
* Default moq-lite group size = 1 Opus frame per group, matching
|
||||||
* Picked to keep the QUIC uni-stream creation rate under the
|
* the JS reference broadcaster's wire shape. See [framesPerGroup]
|
||||||
* production cliff observed against `nostrnests.com` while
|
* kdoc for the full rationale + history.
|
||||||
* still giving late-joining subscribers a sub-100 ms initial
|
|
||||||
* audio gap. See [framesPerGroup] kdoc for the full rationale.
|
|
||||||
*/
|
*/
|
||||||
const val DEFAULT_FRAMES_PER_GROUP: Int = 5
|
const val DEFAULT_FRAMES_PER_GROUP: Int = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user