feat(quic): priority-aware stream scheduling for moq-lite groups

Bias the QUIC connection writer's drain loop toward higher-priority
streams so moq-lite group streams with newer (higher) sequence numbers
drain ahead of older ones under congestion. Implements the T11.3
follow-up flagged in nestsClient/plans/2026-05-06-stream-priority-
followup.md (now removed).

QuicStream gets a `@Volatile var priority: Int = 0`. The writer's
streamsView iteration is replaced by a stable sortedByDescending pass
so same-priority streams keep their existing rotating-start round
robin while higher-priority tiers always drain first.

WebTransportWriteStream gains a `setPriority(Int)` hook; the QUIC-
backed adapter forwards to the underlying QuicStream, while the
in-memory test fakes treat it as a no-op. MoqLiteSession.openGroupStream
calls `uni.setPriority(sequence)` (saturating to Int.MAX_VALUE) to
mirror moq-rs's `Publisher::serve_group`.

Tests: a new InMemoryQuicPipe.decryptClientApplicationFrames helper
walks past coalesced long-header packets to surface 1-RTT frames,
which lets QuicConnectionWriterTest assert that the higher-priority
stream's StreamFrame lands first inside a single drained packet.

https://claude.ai/code/session_01KWdr4RjVvyYZfEuPVaQfUa
This commit is contained in:
Claude
2026-05-06 20:25:01 +00:00
parent e144226eee
commit f1034b1f53
9 changed files with 165 additions and 154 deletions
@@ -1037,6 +1037,13 @@ class MoqLiteSession internal constructor(
// 150 ms late still falls inside hang's default ~200 ms
// jitter buffer) and avoids the dropout entirely.
val uni = transport.openUniStream()
// Mirror moq-rs `Publisher::serve_group`
// (`rs/moq-lite/src/lite/publisher.rs`): newer groups get higher
// priority so the QUIC writer drains fresh audio first when
// retransmits queue up under loss. Saturating cast guards a
// theoretical broadcast that runs long enough for `sequence` to
// exceed Int.MAX_VALUE (≈ 71 years at 1 group/sec); defensive only.
uni.setPriority(sequence.coerceAtMost(Int.MAX_VALUE.toLong()).toInt())
uni.write(Varint.encode(MoqLiteDataType.Group.code))
uni.write(MoqLiteCodec.encodeGroupHeader(MoqLiteGroupHeader(subscribeId, sequence)))
return uni
@@ -162,6 +162,8 @@ class FakeBidiStream internal constructor(
override suspend fun finish() {
write.close()
}
override fun setPriority(priority: Int) = Unit
}
class FakeReadStream internal constructor(
@@ -186,4 +188,6 @@ private class ChannelWriteStream(
override suspend fun finish() {
channel.close()
}
override fun setPriority(priority: Int) = Unit
}
@@ -112,6 +112,22 @@ interface WebTransportWriteStream {
/** Half-close the write side (FIN). No further writes after this call. */
suspend fun finish()
/**
* Hint to the transport about this stream's drain priority relative
* to other streams on the same session. Higher value drains first
* under congestion; same-priority streams keep round-robin order.
* Default 0 = unchanged round-robin behaviour.
*
* moq-lite uses this to bias the writer toward newer group streams
* (sequence-numbered, fresher audio) so a backlog of retransmits on
* an older group doesn't starve the listener of fresh frames. See
* `Publisher::serve_group` in `rs/moq-lite/src/lite/publisher.rs`.
*
* Implementations that don't model priority (e.g. the in-memory
* fake) MAY treat this as a no-op.
*/
fun setPriority(priority: Int)
}
/**
@@ -365,6 +365,10 @@ private class QuicBidiStreamAdapter(
stream.send.finish()
driver.wakeup()
}
override fun setPriority(priority: Int) {
stream.priority = priority
}
}
private class QuicReadStreamAdapter(
@@ -392,6 +396,10 @@ private class QuicUniWriteStreamAdapter(
stream.send.finish()
driver.wakeup()
}
override fun setPriority(priority: Int) {
stream.priority = priority
}
}
/** Adapter for a WT peer-initiated uni stream whose prefix has been stripped. */
@@ -431,4 +439,14 @@ private class StrippedWtBidiStreamAdapter(
?: error("peer-initiated bidi stream has no finish — demux didn't wire one")
finish()
}
/**
* No-op: peer-initiated bidi streams arrive through the demux as a
* [com.vitorpamplona.quic.webtransport.StrippedWtStream] which exposes
* only `send`/`finish` closures, not the underlying [QuicStream]. The
* moq-lite priority use case targets locally-opened uni group streams
* only, so this path doesn't need to model priority see the
* [WebTransportWriteStream.setPriority] contract.
*/
override fun setPriority(priority: Int) = Unit
}