Skip to content

Commit 645d8dd

Browse files
committed
fix: fixed bug with stream clean up and added concurrency tests
* using a stop-gap measure where we add and remove data to the finish frame message. * Likely there is a bug in the `quiche` code where this packet is not being sent due to it being a 0-length message. It was fixed before in their code, but they may have missed an edge case where a large volume of data is being processed. * Fixed race condition with stream pulling data throwing error and cleaning up after stream completes. Somehow despite the code having no awaits, the readable stream was getting pulled and errored out the stream before cleaning up after the finish frame. * Added tests for concurrent servers and clients. * Cleaned up logging for streams. * Related #14 [ci skip]
1 parent af0bfbc commit 645d8dd

8 files changed

+804
-145
lines changed

src/QUICConnection.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class QUICConnection extends EventTarget {
4646
protected codeToReason: StreamCodeToReason;
4747
protected maxReadableStreamBytes: number | undefined;
4848
protected maxWritableStreamBytes: number | undefined;
49-
protected destroyingMap: Map<StreamId, QUICStream> = new Map();
5049

5150
// This basically allows one to await this promise
5251
// once resolved, always resolved...
@@ -421,7 +420,7 @@ class QUICConnection extends EventTarget {
421420
};
422421
try {
423422
this.conn.recv(data, recvInfo);
424-
this.logger.debug(`RECEIVED ${data.byteLength} of data`);
423+
this.logger.info(`RECEIVED ${data.byteLength} of data`);
425424
} catch (e) {
426425
this.logger.error(`recv error ${e.message}`);
427426
// Depending on the exception, the `this.conn.recv`
@@ -460,18 +459,15 @@ class QUICConnection extends EventTarget {
460459
if (this.resolveCloseP != null) this.resolveCloseP();
461460
return;
462461
}
463-
if (
464-
!this.conn.isDraining() &&
465-
(this.conn.isInEarlyData() || this.conn.isEstablished())
466-
) {
462+
if (this.conn.isInEarlyData() || this.conn.isEstablished()) {
463+
const readIds: Array<number> = [];
467464
for (const streamId of this.conn.readable() as Iterable<StreamId>) {
468465
let quicStream = this.streamMap.get(streamId);
469466
if (quicStream == null) {
470467
// The creation will set itself to the stream map
471468
quicStream = await QUICStream.createQUICStream({
472469
streamId,
473470
connection: this,
474-
destroyingMap: this.destroyingMap,
475471
codeToReason: this.codeToReason,
476472
reasonToCode: this.reasonToCode,
477473
maxReadableStreamBytes: this.maxReadableStreamBytes,
@@ -482,9 +478,14 @@ class QUICConnection extends EventTarget {
482478
new events.QUICConnectionStreamEvent({ detail: quicStream }),
483479
);
484480
}
481+
readIds.push(quicStream.streamId);
485482
quicStream.read();
486483
quicStream.dispatchEvent(new events.QUICStreamReadableEvent());
487484
}
485+
if (readIds.length > 0) {
486+
this.logger.info(`processed reads for ${readIds}`);
487+
}
488+
const writeIds: Array<number> = [];
488489
for (const streamId of this.conn.writable() as Iterable<StreamId>) {
489490
let quicStream = this.streamMap.get(streamId);
490491
if (quicStream == null) {
@@ -494,7 +495,6 @@ class QUICConnection extends EventTarget {
494495
connection: this,
495496
codeToReason: this.codeToReason,
496497
reasonToCode: this.reasonToCode,
497-
destroyingMap: this.destroyingMap,
498498
maxReadableStreamBytes: this.maxReadableStreamBytes,
499499
logger: this.logger.getChild(`${QUICStream.name} ${streamId}`),
500500
});
@@ -503,18 +503,15 @@ class QUICConnection extends EventTarget {
503503
);
504504
}
505505
quicStream.dispatchEvent(new events.QUICStreamWritableEvent());
506+
writeIds.push(quicStream.streamId);
506507
quicStream.write();
507508
}
508-
// Checking shortlist if streams have finished.
509-
for (const [streamId, stream] of this.destroyingMap) {
510-
if (stream.isFinished()) {
511-
// If it has finished, it will trigger its own clean up.
512-
// Remove the stream from the shortlist.
513-
this.destroyingMap.delete(streamId);
514-
}
509+
if (writeIds.length > 0) {
510+
this.logger.info(`processed writes for ${writeIds}`);
515511
}
516512
}
517513
} finally {
514+
this.garbageCollectStreams('recv');
518515
this.logger.debug('RECV FINALLY');
519516
// Set the timeout
520517
this.checkTimeout();
@@ -527,7 +524,7 @@ class QUICConnection extends EventTarget {
527524
) {
528525
this.logger.debug('CALLING DESTROY 2');
529526
// Destroy in the background, we still need to process packets
530-
void this.destroy();
527+
void this.destroy().catch(() => {});
531528
}
532529
}
533530
}
@@ -558,6 +555,7 @@ class QUICConnection extends EventTarget {
558555
} else if (this.conn.isDraining()) {
559556
return;
560557
}
558+
let numSent = 0;
561559
try {
562560
const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE);
563561
let sendLength: number;
@@ -630,8 +628,10 @@ class QUICConnection extends EventTarget {
630628
return;
631629
}
632630
this.dispatchEvent(new events.QUICConnectionSendEvent());
631+
numSent += 1;
633632
}
634633
} finally {
634+
if (numSent > 0) this.garbageCollectStreams('send');
635635
this.logger.debug('SEND FINALLY');
636636
this.checkTimeout();
637637
if (
@@ -689,7 +689,6 @@ class QUICConnection extends EventTarget {
689689
connection: this,
690690
codeToReason: this.codeToReason,
691691
reasonToCode: this.reasonToCode,
692-
destroyingMap: this.destroyingMap,
693692
maxReadableStreamBytes: this.maxReadableStreamBytes,
694693
maxWritableStreamBytes: this.maxWritableStreamBytes,
695694
logger: this.logger.getChild(`${QUICStream.name} ${streamId!}`),
@@ -743,7 +742,7 @@ class QUICConnection extends EventTarget {
743742
) {
744743
this.logger.debug('CALLING DESTROY 3');
745744
// Destroy in the background, we still need to process packets
746-
void this.destroy();
745+
void this.destroy().catch(() => {});
747746
}
748747
this.checkTimeout();
749748
};
@@ -795,6 +794,19 @@ class QUICConnection extends EventTarget {
795794
}
796795
}
797796
};
797+
798+
protected garbageCollectStreams(where: string) {
799+
const nums: Array<number> = [];
800+
// Only check if packets were sent
801+
for (const [streamId, quicStream] of this.streamMap) {
802+
// Stream sending can finish after a packet is sent
803+
nums.push(streamId);
804+
quicStream.read();
805+
}
806+
if (nums.length > 0) {
807+
this.logger.info(`checking read finally ${where} for ${nums}`);
808+
}
809+
}
798810
}
799811

800812
export default QUICConnection;

src/QUICServer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ class QUICServer extends EventTarget {
290290
this.logger.debug(
291291
`Accepting new connection from QUIC packet from ${remoteInfo.host}:${remoteInfo.port}`,
292292
);
293+
const clientConnRef = Buffer.from(header.scid).toString('hex').slice(32);
293294
const connection = await QUICConnection.acceptQUICConnection({
294295
scid,
295296
dcid: dcidOriginal,
@@ -301,7 +302,7 @@ class QUICServer extends EventTarget {
301302
maxReadableStreamBytes: this.maxReadableStreamBytes,
302303
maxWritableStreamBytes: this.maxWritableStreamBytes,
303304
logger: this.logger.getChild(
304-
`${QUICConnection.name} ${scid.toString().slice(32)}`,
305+
`${QUICConnection.name} ${scid.toString().slice(32)}-${clientConnRef}`,
305306
),
306307
});
307308
connection.setKeepAlive(this.keepaliveIntervalTime);

0 commit comments

Comments
 (0)