Skip to content

Commit 5bf8103

Browse files
committed
fix: close QUIC streams after receiving FIN
1 parent 0797689 commit 5bf8103

4 files changed

Lines changed: 84 additions & 18 deletions

File tree

lsquic.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
packageName = "lsquic"
2-
version = "0.5.5"
2+
version = "0.6.0"
33
author = "Status Research & Development GmbH"
44
description = "Nim wrapper around the lsquic library"
55
license = "MIT"

lsquic/context/stream.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ proc onRead*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl.
104104

105105
streamCtx.toRead = Opt.none(ReadTask)
106106

107+
if n == 0 and not streamCtx.closeIfDone():
108+
error "could not close stream after EOF", streamId = lsquic_stream_id(stream)
109+
streamCtx.abort()
110+
107111
proc onWrite*(stream: ptr lsquic_stream_t, ctx: ptr lsquic_stream_ctx_t) {.cdecl.} =
108112
trace "onWrite"
109113

lsquic/stream.nim

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Stream* = ref object
2121
quicStream*: ptr lsquic_stream_t
2222
closedByEngine*: bool
2323
closeWrite*: bool
24+
closeRequested: bool
2425
# This is called when on_close callback is executed
2526
closed*: AsyncEvent
2627
# Reuse a single closed-event waiter to minimize allocations on hot paths.
@@ -132,23 +133,39 @@ template raiseIfWriteReset(stream: Stream) =
132133
if stream.writeResetByPeer():
133134
raise stream.newStreamResetError("stream write")
134135

135-
proc abort*(stream: Stream) =
136+
proc requestClose(stream: Stream): bool {.raises: [].} =
137+
if stream.closedByEngine or stream.quicStream.isNil or stream.closeRequested:
138+
return true
139+
140+
stream.closeRequested = true
141+
let ret = lsquic_stream_close(stream.quicStream)
142+
if ret != 0:
143+
let closeErrno = errno
144+
if closeErrno == EBADF:
145+
stream.doProcess()
146+
return true
147+
148+
stream.closeRequested = false
149+
trace "could not close stream",
150+
streamId = lsquic_stream_id(stream.quicStream), errno = closeErrno
151+
return false
152+
153+
stream.doProcess()
154+
true
155+
156+
proc closeIfDone*(stream: Stream): bool {.raises: [].} =
136157
if stream.closeWrite and stream.isEof:
137-
if not stream.closed.isSet():
138-
stream.closed.fire()
139-
stream.abortPendingWrites("stream aborted")
140-
return
158+
return stream.requestClose()
141159

142-
if not stream.closedByEngine:
143-
let ret = lsquic_stream_close(stream.quicStream)
144-
if ret != 0:
145-
trace "could not abort stream", streamId = lsquic_stream_id(stream.quicStream)
146-
stream.doProcess()
160+
true
147161

162+
proc abort*(stream: Stream) =
148163
stream.closeWrite = true
149164
stream.isEof = true
165+
discard stream.requestClose()
150166
stream.abortPendingWrites("stream aborted")
151-
stream.closed.fire()
167+
if not stream.closed.isSet():
168+
stream.closed.fire()
152169

153170
proc close*(stream: Stream) {.async: (raises: [StreamError, CancelledError]).} =
154171
if stream.closeWrite or stream.closedByEngine:
@@ -157,14 +174,13 @@ proc close*(stream: Stream) {.async: (raises: [StreamError, CancelledError]).} =
157174
# Closing only the write side
158175
let ret = lsquic_stream_shutdown(stream.quicStream, 1)
159176
if ret == 0:
160-
if stream.isEof:
161-
if lsquic_stream_close(stream.quicStream) != 0:
162-
stream.abort()
163-
raise newException(StreamError, "could not close the stream")
164-
165177
stream.abortPendingWrites("stream closed")
166178
stream.closeWrite = true
167-
stream.doProcess()
179+
if not stream.closeIfDone():
180+
stream.abort()
181+
raise newException(StreamError, "could not close the stream")
182+
if not stream.isEof:
183+
stream.doProcess()
168184
else:
169185
raise newException(StreamError, "could not close the stream")
170186

@@ -200,6 +216,9 @@ proc readOnce*(
200216

201217
if n == 0:
202218
stream.isEof = true
219+
if not stream.closeIfDone():
220+
stream.abort()
221+
raise newException(StreamError, "could not close the stream")
203222
return 0
204223
elif n > 0:
205224
return n
@@ -231,6 +250,7 @@ proc readOnce*(
231250
raiseIfReadReset(stream)
232251
stream.isEof = true
233252
stream.closeWrite = true
253+
discard stream.closeIfDone()
234254
return 0
235255

236256
return await doneFut

tests/test_lifecycle.nim

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,48 @@ suite "lifecycle":
267267
check (await reading) == 0
268268
await incomingStream.close()
269269

270+
asyncTest "close then EOF retires peer-initiated stream credit":
271+
const StreamCount = 120
272+
const StreamCreditTimeout = 30.seconds
273+
let peers = await connectPeers()
274+
defer:
275+
await stopPeers(peers)
276+
277+
proc openStreamWithTimeout(conn: Connection): Future[Stream] {.async:(raises:[CancelledError, ConnectionError]).} =
278+
let opening = conn.openStream()
279+
if not await opening.withTimeout(StreamCreditTimeout):
280+
await opening.cancelAndWait()
281+
doAssert false, "timed out opening stream; close+EOF should retire stream credit"
282+
await opening
283+
284+
proc serve() {.async.} =
285+
for i in 0 ..< StreamCount:
286+
let stream = await peers.incoming.incomingStream()
287+
var buf = newSeq[byte](1)
288+
check (await stream.readOnce(buf)) == 1
289+
check buf[0] == byte(i mod 256)
290+
291+
await stream.write(@[byte((i + 1) mod 256)])
292+
await stream.close()
293+
check (await stream.readOnce(buf)) == 0
294+
295+
let serving = serve()
296+
297+
for i in 0 ..< StreamCount:
298+
let stream = await peers.outgoing.openStreamWithTimeout()
299+
await stream.write(@[byte(i mod 256)])
300+
await stream.close()
301+
302+
var buf = newSeq[byte](1)
303+
check (await stream.readOnce(buf)) == 1
304+
check buf[0] == byte((i + 1) mod 256)
305+
check (await stream.readOnce(buf)) == 0
306+
307+
if not await serving.withTimeout(StreamCreditTimeout):
308+
await serving.cancelAndWait()
309+
doAssert false, "timed out serving streams; close+EOF should not leave stream handlers blocked"
310+
await serving
311+
270312
asyncTest "cancelled blocked read clears pending read":
271313
let peers = await connectPeers()
272314
defer:

0 commit comments

Comments
 (0)