Merge branch 'worktree-agent-acb67f8575e4086eb' into claude/research-quic-libraries-hH1Dc
This commit is contained in:
+8
-8
@@ -64,7 +64,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
|
||||
// Peer ACKs the packet that carried our outbound ACK
|
||||
// covering up to PN 4.
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.onTokensAcked(
|
||||
listOf(
|
||||
@@ -75,7 +75,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
// Tracker is now empty: peer has confirmed receipt of our
|
||||
// ACK that covered everything up to PN 4. Re-advertising
|
||||
@@ -96,7 +96,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
}
|
||||
|
||||
// Peer ACKs our Initial-level outbound ACK.
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.onTokensAcked(
|
||||
listOf(
|
||||
@@ -104,7 +104,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
// Initial tracker drained; Application tracker untouched.
|
||||
assertTrue(conn.initial.ackTracker.isEmpty())
|
||||
@@ -120,7 +120,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
}
|
||||
// Peer ACKs our outbound ACK that covered up to PN 4 only;
|
||||
// the tracker's higher-PN ranges (5..9) must survive.
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.onTokensAcked(
|
||||
listOf(
|
||||
@@ -128,7 +128,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertFalse(conn.application.ackTracker.isEmpty())
|
||||
assertEquals(9L, conn.application.ackTracker.largestReceived())
|
||||
@@ -145,7 +145,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
for (pn in 0L..9L) {
|
||||
conn.application.ackTracker.receivedPacket(pn, ackEliciting = true, receivedAtMillis = 1L)
|
||||
}
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.onTokensAcked(
|
||||
listOf(
|
||||
@@ -160,7 +160,7 @@ class AckTrackerPurgeOnAckOfAckTest {
|
||||
)
|
||||
assertTrue(conn.application.ackTracker.isEmpty())
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -81,12 +81,12 @@ class CryptoRetransmitTest {
|
||||
.single()
|
||||
|
||||
// Simulate loss via direct dispatch.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensLost(listOf(cryptoToken))
|
||||
client.initial.sentPackets.remove(firstPn)
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
|
||||
// Initial-level cryptoSend should now have re-queued bytes
|
||||
@@ -126,11 +126,11 @@ class CryptoRetransmitTest {
|
||||
client.initial.sentPackets.entries
|
||||
.first { it.value.tokens.any { t -> t is RecoveryToken.Crypto } }
|
||||
// ACK via direct dispatch.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensAcked(packet.value.tokens)
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
// After ACK the Initial-level cryptoSend's flushedFloor should
|
||||
// have advanced — we check by observing that another takeChunk
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quic.connection
|
||||
|
||||
import com.vitorpamplona.quic.tls.InProcessTlsServer
|
||||
import com.vitorpamplona.quic.tls.PermissiveCertificateValidator
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Throughput contract for the lock-split refactor (2026-05-08): opening
|
||||
* many parallel bidi streams + queueing requests must not be serialised
|
||||
* by a single connection-wide mutex. Phase 1 of the split (separate
|
||||
* `streamsLock` / `lifecycleLock` / per-level `levelLock`) targets the
|
||||
* multiplexing testcase that drove this refactor — see
|
||||
* `quic/plans/2026-05-08-lock-split-design.md`.
|
||||
*
|
||||
* The test stands up an in-memory client (no socket I/O), opens 1000
|
||||
* client-bidi streams concurrently, enqueues a small request body + FIN
|
||||
* on each, and asserts the operation completes within a generous wall-
|
||||
* clock budget. The number is deliberately loose: this is a contract
|
||||
* for "lock contention isn't pathological", not a microbenchmark.
|
||||
*
|
||||
* NOTE: the in-memory pipe doesn't drive a concurrent send loop, so
|
||||
* this test exercises the lock-acquisition cost of `openBidiStream`
|
||||
* itself rather than full multiplexing throughput. The interop runner
|
||||
* provides the end-to-end measurement.
|
||||
*/
|
||||
class MultiplexingThroughputTest {
|
||||
@Test
|
||||
fun open_1000_bidi_streams_completes_quickly() {
|
||||
runBlocking {
|
||||
val client =
|
||||
QuicConnection(
|
||||
serverName = "example.test",
|
||||
config =
|
||||
QuicConnectionConfig(
|
||||
initialMaxStreamsBidi = 2_000,
|
||||
initialMaxStreamsUni = 2_000,
|
||||
initialMaxData = 100_000_000,
|
||||
initialMaxStreamDataBidiLocal = 100_000,
|
||||
initialMaxStreamDataBidiRemote = 100_000,
|
||||
initialMaxStreamDataUni = 100_000,
|
||||
),
|
||||
tlsCertificateValidator = PermissiveCertificateValidator(),
|
||||
)
|
||||
val serverScid = ConnectionId.random(8)
|
||||
val tlsServer =
|
||||
InProcessTlsServer(
|
||||
transportParameters =
|
||||
TransportParameters(
|
||||
initialMaxData = 100_000_000,
|
||||
initialMaxStreamDataBidiLocal = 100_000,
|
||||
initialMaxStreamDataBidiRemote = 100_000,
|
||||
initialMaxStreamDataUni = 100_000,
|
||||
initialMaxStreamsBidi = 2_000,
|
||||
initialMaxStreamsUni = 2_000,
|
||||
initialSourceConnectionId = serverScid.bytes,
|
||||
originalDestinationConnectionId = client.destinationConnectionId.bytes,
|
||||
).encode(),
|
||||
)
|
||||
val pipe =
|
||||
InMemoryQuicPipe(
|
||||
client = client,
|
||||
initialDcid = client.destinationConnectionId.bytes,
|
||||
serverScid = serverScid,
|
||||
tlsServer = tlsServer,
|
||||
)
|
||||
client.start()
|
||||
pipe.drive(maxRounds = 16)
|
||||
assertEquals(QuicConnection.Status.CONNECTED, client.status)
|
||||
|
||||
val request = ByteArray(50) { it.toByte() }
|
||||
val streamCount = 1_000
|
||||
|
||||
// Open all streams in parallel — each launch contends for
|
||||
// streamsLock briefly. Pre-refactor this serialised against
|
||||
// any in-flight drainOutbound call; phase 1 keeps openBidi
|
||||
// contention scoped to streamsLock-only.
|
||||
val started =
|
||||
kotlin.time.TimeSource.Monotonic
|
||||
.markNow()
|
||||
val opens =
|
||||
(0 until streamCount).map {
|
||||
async {
|
||||
val stream = client.openBidiStream()
|
||||
stream.send.enqueue(request)
|
||||
stream.send.finish()
|
||||
stream.streamId
|
||||
}
|
||||
}
|
||||
val ids = opens.awaitAll()
|
||||
val elapsed = started.elapsedNow()
|
||||
|
||||
// Useful diagnostic for measuring future regressions: stdout
|
||||
// shows up in the test report so phase-1 vs phase-2 can be
|
||||
// compared against the same test.
|
||||
println(
|
||||
"[MultiplexingThroughputTest] opened $streamCount bidi streams in " +
|
||||
"${elapsed.inWholeMilliseconds}ms " +
|
||||
"(${(streamCount * 1000.0 / elapsed.inWholeMilliseconds.coerceAtLeast(1L)).toLong()} streams/sec)",
|
||||
)
|
||||
assertEquals(streamCount, ids.size)
|
||||
assertEquals(streamCount, ids.toSet().size, "stream ids must be unique")
|
||||
// Generous bound; in-process opens of 1000 streams should
|
||||
// complete in well under half a second on any developer
|
||||
// machine — pre-refactor this was minutes due to lock
|
||||
// contention against the (idle) send-loop drain. The looser
|
||||
// 2-second bound is still 100x what's expected on actual
|
||||
// hardware while accounting for slow CI workers.
|
||||
assertTrue(
|
||||
elapsed.inWholeMilliseconds < 2_000L,
|
||||
"1000 parallel openBidiStream calls took ${elapsed.inWholeMilliseconds}ms; expected <2000ms",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,11 @@ class OnTokensLostTest {
|
||||
fun ackToken_doesNotPopulateAnyPending() =
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.onTokensLost(listOf(RecoveryToken.Ack(level = EncryptionLevel.APPLICATION, largestAcked = 0L)))
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertNull(conn.pendingMaxStreamsUni)
|
||||
assertNull(conn.pendingMaxStreamsBidi)
|
||||
@@ -64,12 +64,12 @@ class OnTokensLostTest {
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
// Simulate the writer having advertised a higher cap.
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxStreamsUni = 150L
|
||||
conn.onTokensLost(listOf(RecoveryToken.MaxStreamsUni(maxStreams = 150L)))
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(150L, conn.pendingMaxStreamsUni)
|
||||
}
|
||||
@@ -82,12 +82,12 @@ class OnTokensLostTest {
|
||||
// the value carried by the lost token (150). The lost
|
||||
// frame is irrelevant — re-emitting 150 would not extend
|
||||
// the cap. neqo's fc.rs line 322 supersede check.
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxStreamsUni = 200L
|
||||
conn.onTokensLost(listOf(RecoveryToken.MaxStreamsUni(maxStreams = 150L)))
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertNull(conn.pendingMaxStreamsUni, "stale lost extension must not be re-emitted")
|
||||
}
|
||||
@@ -96,12 +96,12 @@ class OnTokensLostTest {
|
||||
fun lostMaxStreamsBidi_matchingAdvertised_setsPending() =
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxStreamsBidi = 200L
|
||||
conn.onTokensLost(listOf(RecoveryToken.MaxStreamsBidi(maxStreams = 200L)))
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(200L, conn.pendingMaxStreamsBidi)
|
||||
}
|
||||
@@ -110,12 +110,12 @@ class OnTokensLostTest {
|
||||
fun lostMaxData_matchingAdvertised_setsPending() =
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxData = 1_000_000L
|
||||
conn.onTokensLost(listOf(RecoveryToken.MaxData(maxData = 1_000_000L)))
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(1_000_000L, conn.pendingMaxData)
|
||||
}
|
||||
@@ -124,12 +124,12 @@ class OnTokensLostTest {
|
||||
fun lostMaxData_supersededIsDropped() =
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxData = 2_000_000L
|
||||
conn.onTokensLost(listOf(RecoveryToken.MaxData(maxData = 1_000_000L)))
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertNull(conn.pendingMaxData)
|
||||
}
|
||||
@@ -138,13 +138,13 @@ class OnTokensLostTest {
|
||||
fun lostMaxStreamData_unknownStream_dropped() =
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.onTokensLost(
|
||||
listOf(RecoveryToken.MaxStreamData(streamId = 999L, maxData = 1024L)),
|
||||
)
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
// No stream with id 999 exists ⇒ token is dropped silently.
|
||||
assertEquals(emptyMap<Long, Long>(), conn.pendingMaxStreamData)
|
||||
@@ -154,7 +154,7 @@ class OnTokensLostTest {
|
||||
fun multipleLostTokens_dispatchAll() =
|
||||
runBlocking {
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxStreamsUni = 150L
|
||||
conn.advertisedMaxStreamsBidi = 200L
|
||||
@@ -168,7 +168,7 @@ class OnTokensLostTest {
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(150L, conn.pendingMaxStreamsUni)
|
||||
assertEquals(200L, conn.pendingMaxStreamsBidi)
|
||||
@@ -183,7 +183,7 @@ class OnTokensLostTest {
|
||||
// most one value (the last setter wins; the supersede
|
||||
// check filters older losses).
|
||||
val conn = newConn()
|
||||
conn.lock.lock()
|
||||
conn.streamsLock.lock()
|
||||
try {
|
||||
conn.advertisedMaxStreamsUni = 200L
|
||||
// First lost packet had MaxStreamsUni(150) — stale, dropped.
|
||||
@@ -193,7 +193,7 @@ class OnTokensLostTest {
|
||||
conn.onTokensLost(listOf(RecoveryToken.MaxStreamsUni(maxStreams = 200L)))
|
||||
assertEquals(200L, conn.pendingMaxStreamsUni)
|
||||
} finally {
|
||||
conn.lock.unlock()
|
||||
conn.streamsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -94,7 +94,7 @@ class PeerStreamCreditExtensionTest {
|
||||
// Simulate the relay opening uni streams to us. SERVER_UNI
|
||||
// stream IDs use the encoding `index << 2 | 0x3`. Two streams
|
||||
// (cap=4, half-window=2) is the threshold for a refresh.
|
||||
client.lock
|
||||
client.streamsLock
|
||||
.let {
|
||||
// Acquire under lock since getOrCreatePeerStreamLocked requires it.
|
||||
it
|
||||
@@ -103,7 +103,7 @@ class PeerStreamCreditExtensionTest {
|
||||
kotlinx.coroutines.sync
|
||||
.Mutex()
|
||||
.let { /* noop: silence unused-import linter */ }
|
||||
client.lock.let { l ->
|
||||
client.streamsLock.let { l ->
|
||||
kotlinx.coroutines.runBlocking {
|
||||
l.lock()
|
||||
try {
|
||||
@@ -179,7 +179,7 @@ class PeerStreamCreditExtensionTest {
|
||||
|
||||
// Open 10 peer streams — half-window for cap=100 is 50, so
|
||||
// we're well below the threshold.
|
||||
client.lock.let { l ->
|
||||
client.streamsLock.let { l ->
|
||||
kotlinx.coroutines.runBlocking {
|
||||
l.lock()
|
||||
try {
|
||||
|
||||
+14
-14
@@ -46,11 +46,11 @@ class PendingFlowControlEmitTest {
|
||||
fun pendingMaxStreamsUni_drainEmitsFrameAndToken() =
|
||||
runBlocking {
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxStreamsUni = 150L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
|
||||
val sizeBefore = client.application.sentPackets.size
|
||||
@@ -79,11 +79,11 @@ class PendingFlowControlEmitTest {
|
||||
fun pendingMaxStreamsBidi_drainEmitsFrameAndToken(): Unit =
|
||||
runBlocking {
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxStreamsBidi = 200L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
val sizeBefore = client.application.sentPackets.size
|
||||
runCatching { drainOutbound(client, nowMillis = 1L) }
|
||||
@@ -103,11 +103,11 @@ class PendingFlowControlEmitTest {
|
||||
fun pendingMaxData_drainEmitsFrameAndToken(): Unit =
|
||||
runBlocking {
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxData = 5_000_000L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
val sizeBefore = client.application.sentPackets.size
|
||||
runCatching { drainOutbound(client, nowMillis = 1L) }
|
||||
@@ -127,12 +127,12 @@ class PendingFlowControlEmitTest {
|
||||
fun pendingMaxStreamData_perStreamDrain() =
|
||||
runBlocking {
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxStreamData[3L] = 1_024L
|
||||
client.pendingMaxStreamData[7L] = 2_048L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
val sizeBefore = client.application.sentPackets.size
|
||||
runCatching { drainOutbound(client, nowMillis = 1L) }
|
||||
@@ -159,13 +159,13 @@ class PendingFlowControlEmitTest {
|
||||
fun multiplePending_drainEmitsAllInOnePacket(): Unit =
|
||||
runBlocking {
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxStreamsUni = 150L
|
||||
client.pendingMaxStreamsBidi = 200L
|
||||
client.pendingMaxData = 1_000_000L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
val sizeBefore = client.application.sentPackets.size
|
||||
runCatching { drainOutbound(client, nowMillis = 1L) }
|
||||
@@ -216,11 +216,11 @@ class PendingFlowControlEmitTest {
|
||||
// advertised cap. The writer drains it as-is — supersede check
|
||||
// is in step 6 (the setter side), not here.
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxStreamsUni = 50L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
val sizeBefore = client.application.sentPackets.size
|
||||
runCatching { drainOutbound(client, nowMillis = 1L) }
|
||||
@@ -242,11 +242,11 @@ class PendingFlowControlEmitTest {
|
||||
fun pendingClearedAcrossDrains() =
|
||||
runBlocking {
|
||||
val client = handshakedClient()
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.pendingMaxStreamsUni = 150L
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
// Drain once: pending consumed.
|
||||
runCatching { drainOutbound(client, nowMillis = 1L) }
|
||||
|
||||
+8
-8
@@ -93,12 +93,12 @@ class ResetStopSendingEmitTest {
|
||||
.single()
|
||||
|
||||
// Simulate loss.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensLost(listOf(token))
|
||||
client.application.sentPackets.remove(firstEntry.key)
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
// Per-stream emit-pending should be re-flagged.
|
||||
assertTrue(stream.resetEmitPending, "loss must re-flag resetEmitPending")
|
||||
@@ -137,21 +137,21 @@ class ResetStopSendingEmitTest {
|
||||
.single()
|
||||
|
||||
// ACK first.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensAcked(listOf(token))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(true, stream.resetAcked)
|
||||
assertEquals(false, stream.resetEmitPending)
|
||||
|
||||
// Now a stale loss notification arrives. Defensive: drop.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensLost(listOf(token))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(false, stream.resetEmitPending, "stale loss after ACK must not re-flag emit-pending")
|
||||
}
|
||||
@@ -248,11 +248,11 @@ class ResetStopSendingEmitTest {
|
||||
connectionId = byteArrayOf(1, 2, 3, 4),
|
||||
statelessResetToken = ByteArray(16) { it.toByte() },
|
||||
)
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensLost(listOf(token))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(token, client.pendingNewConnectionId[1L])
|
||||
|
||||
|
||||
+8
-8
@@ -69,7 +69,7 @@ class RetransmitIntegrationTest {
|
||||
// ACK'd by reordering — its PN < largestAckedPn -
|
||||
// PACKET_THRESHOLD ⇒ declared lost.
|
||||
val futurePn = msuPn + 4L
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
// Inject a phantom SentPacket at futurePn so the loss
|
||||
// detector has a credible "newly acked" reference, then
|
||||
@@ -102,7 +102,7 @@ class RetransmitIntegrationTest {
|
||||
// 5. Dispatch lost tokens — pendingMaxStreamsUni gets set.
|
||||
client.onTokensLost(lostMsuPacket.tokens)
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
assertEquals(
|
||||
capAfterFirstDrain,
|
||||
@@ -148,24 +148,24 @@ class RetransmitIntegrationTest {
|
||||
|
||||
// Second bump: open more peer-uni streams to cross the
|
||||
// (already extended) threshold again.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 2))
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 3))
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 4))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
runCatching { drainOutbound(client, nowMillis = 2L) }
|
||||
val secondCap = client.advertisedMaxStreamsUni
|
||||
assertTrue(secondCap > firstCap, "second drain must advertise a still-higher cap; saw $firstCap → $secondCap")
|
||||
|
||||
// Now declare the FIRST emit lost via direct dispatch.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensLost(listOf(RecoveryToken.MaxStreamsUni(maxStreams = firstCap)))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
// Supersede check: firstCap != advertisedMaxStreamsUni (now == secondCap),
|
||||
// so pending must remain null.
|
||||
@@ -216,12 +216,12 @@ class RetransmitIntegrationTest {
|
||||
|
||||
private fun crossPeerUniHalfWindow(client: QuicConnection) =
|
||||
runBlocking {
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 0))
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 1))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -200,12 +200,12 @@ class SentPacketTrackingTest {
|
||||
/** Cross the half-window threshold (cap=4, two peer-uni streams ⇒ count >= cap-half=2). */
|
||||
private fun crossPeerUniHalfWindow(client: QuicConnection) =
|
||||
runBlocking {
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 0))
|
||||
client.getOrCreatePeerStreamLocked(StreamId.build(StreamId.Kind.SERVER_UNI, 1))
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -82,7 +82,7 @@ class StreamRetransmitTest {
|
||||
val firstPn = firstPacketEntry.key
|
||||
|
||||
// Simulate loss via direct dispatch.
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
val streamToken =
|
||||
firstPacketEntry.value.tokens
|
||||
@@ -93,7 +93,7 @@ class StreamRetransmitTest {
|
||||
// detector would have done this).
|
||||
client.application.sentPackets.remove(firstPn)
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
|
||||
// SendBuffer should have re-queued the bytes for retransmit.
|
||||
@@ -133,11 +133,11 @@ class StreamRetransmitTest {
|
||||
val packet =
|
||||
client.application.sentPackets.entries
|
||||
.first { it.value.tokens.any { t -> t is RecoveryToken.Stream } }
|
||||
client.lock.lock()
|
||||
client.streamsLock.lock()
|
||||
try {
|
||||
client.onTokensAcked(packet.value.tokens)
|
||||
} finally {
|
||||
client.lock.unlock()
|
||||
client.streamsLock.unlock()
|
||||
}
|
||||
|
||||
// After ACK: enqueue more, observe that the buffer
|
||||
|
||||
Reference in New Issue
Block a user