GeezerVison is showing github.com — quic: add initial RTT option to session options · nodejs/node@84d3100
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/quic.md +17Lines changed: 17 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2723,6 +2723,23 @@ added: v23.8.0 | 2723 | 2723 | Specifies the maximum number of milliseconds a TLS handshake is permitted to take | 2724 | 2724 | to complete before timing out. | 2725 | 2725 |
| | 2726 | +#### `sessionOptions.initialRtt` | | 2727 | + | | 2728 | +<!-- YAML | | 2729 | +added: REPLACEME | | 2730 | +--> | | 2731 | + | | 2732 | +* Type: {bigint|number} | | 2733 | +* **Default:** `0` (use ngtcp2 default of 333ms) | | 2734 | + | | 2735 | +Specifies the initial round-trip time estimate in milliseconds. This value is | | 2736 | +used for probe timeout (PTO) computation, initial pacing, and early loss | | 2737 | +detection before the first actual RTT sample is collected from the connection. | | 2738 | +The default of 333ms is appropriate for the general internet. For low-latency | | 2739 | +environments such as loopback or same-rack deployments, setting a value closer | | 2740 | +to the actual RTT (e.g., `1`) avoids unnecessarily conservative initial | | 2741 | +behavior. | | 2742 | + | 2726 | 2743 | #### `sessionOptions.keepAlive` | 2727 | 2744 |
| 2728 | 2745 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/quic.js +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -404,6 +404,9 @@ const endpointRegistry = new SafeSet(); | 404 | 404 | * @property {ArrayBufferView} [token] An opaque address validation token | 405 | 405 | * previously received from the server via `onnewtoken` (client only). | 406 | 406 | * @property {bigint|number} [handshakeTimeout] The handshake timeout | | 407 | + * @property {bigint|number} [initialRtt] The initial round-trip time estimate in milliseconds. | | 408 | + * Used for PTO computation and initial pacing before the first RTT sample. Default uses | | 409 | + * ngtcp2's built-in default of 333ms. Set lower for low-latency environments. | 407 | 410 | * @property {bigint|number} [keepAlive] The keep-alive timeout in milliseconds. When set, | 408 | 411 | * PING frames will be sent automatically to prevent idle timeout. | 409 | 412 | * @property {bigint|number} [maxStreamWindow] The maximum stream window | @@ -4875,6 +4878,7 @@ function processSessionOptions(options, config = kEmptyObject) { | 4875 | 4878 | maxPayloadSize, | 4876 | 4879 | unacknowledgedPacketThreshold = 0, | 4877 | 4880 | handshakeTimeout, | | 4881 | + initialRtt, | 4878 | 4882 | keepAlive, | 4879 | 4883 | maxStreamWindow, | 4880 | 4884 | maxWindow, | @@ -4982,6 +4986,7 @@ function processSessionOptions(options, config = kEmptyObject) { | 4982 | 4986 | maxPayloadSize, | 4983 | 4987 | unacknowledgedPacketThreshold, | 4984 | 4988 | handshakeTimeout, | | 4989 | + initialRtt, | 4985 | 4990 | keepAlive, | 4986 | 4991 | maxStreamWindow, | 4987 | 4992 | maxWindow, | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/bindingdata.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -93,6 +93,7 @@ class SessionManager; | 93 | 93 | V(groups, "groups") \ | 94 | 94 | V(handshake_timeout, "handshakeTimeout") \ | 95 | 95 | V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ | | 96 | + V(initial_rtt, "initialRtt") \ | 96 | 97 | V(keep_alive_timeout, "keepAlive") \ | 97 | 98 | V(initial_max_data, "initialMaxData") \ | 98 | 99 | V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/session.cc +17-4Lines changed: 17 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -513,6 +513,12 @@ Session::Config::Config(Environment* env, | 513 | 513 | options.handshake_timeout == UINT64_MAX | 514 | 514 | ? UINT64_MAX | 515 | 515 | : options.handshake_timeout * NGTCP2_MILLISECONDS; | | 516 | + | | 517 | + // The initial_rtt option is in milliseconds; ngtcp2 expects nanoseconds. | | 518 | + // A value of 0 leaves the ngtcp2 default (333ms) unchanged. | | 519 | + if (options.initial_rtt > 0) | | 520 | + settings.initial_rtt = options.initial_rtt * NGTCP2_MILLISECONDS; | | 521 | + | 516 | 522 | settings.max_stream_window = options.max_stream_window; | 517 | 523 | settings.max_window = options.max_window; | 518 | 524 | settings.ack_thresh = options.unacknowledged_packet_threshold; | @@ -604,10 +610,11 @@ Maybe<Session::Options> Session::Options::From(Environment* env, | 604 | 610 |
| 605 | 611 | if (!SET(version) || !SET(min_version) || !SET(preferred_address_strategy) || | 606 | 612 | !SET(transport_params) || !SET(tls_options) || !SET(qlog) || | 607 | | - !SET(handshake_timeout) || !SET(keep_alive_timeout) || | 608 | | - !SET(max_stream_window) || !SET(max_window) || !SET(max_payload_size) || | 609 | | - !SET(unacknowledged_packet_threshold) || !SET(cc_algorithm) || | 610 | | - !SET(draining_period_multiplier) || !SET(max_datagram_send_attempts)) { | | 613 | + !SET(handshake_timeout) || !SET(initial_rtt) || | | 614 | + !SET(keep_alive_timeout) || !SET(max_stream_window) || !SET(max_window) || | | 615 | + !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || | | 616 | + !SET(cc_algorithm) || !SET(draining_period_multiplier) || | | 617 | + !SET(max_datagram_send_attempts)) { | 611 | 618 | return Nothing<Options>(); | 612 | 619 | } | 613 | 620 |
| @@ -726,6 +733,12 @@ std::string Session::Options::ToString() const { | 726 | 733 | res += prefix + "handshake timeout: " + std::to_string(handshake_timeout) + | 727 | 734 | " nanoseconds"; | 728 | 735 | } | | 736 | + if (initial_rtt > 0) { | | 737 | + res += prefix + "initial rtt: " + std::to_string(initial_rtt) + | | 738 | + " milliseconds"; | | 739 | + } else { | | 740 | + res += prefix + "initial rtt: <default>"; | | 741 | + } | 729 | 742 | res += prefix + "max stream window: " + std::to_string(max_stream_window); | 730 | 743 | res += prefix + "max window: " + std::to_string(max_window); | 731 | 744 | res += prefix + "max payload size: " + std::to_string(max_payload_size); | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/session.h +9Lines changed: 9 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -163,6 +163,15 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { | 163 | 163 | static constexpr uint64_t DEFAULT_HANDSHAKE_TIMEOUT = 10'000; | 164 | 164 | uint64_t handshake_timeout = DEFAULT_HANDSHAKE_TIMEOUT; | 165 | 165 |
| | 166 | + // The initial round-trip time estimate in milliseconds. ngtcp2 uses this | | 167 | + // for PTO computation, initial pacing, and early loss detection before | | 168 | + // the first RTT sample is collected. The default of 0 uses ngtcp2's | | 169 | + // built-in default of 333ms, which is appropriate for the general | | 170 | + // internet. For low-latency environments (e.g., loopback or same-rack | | 171 | + // deployments), setting a value closer to the actual RTT avoids | | 172 | + // unnecessarily conservative initial behavior. | | 173 | + uint64_t initial_rtt = 0; | | 174 | + | 166 | 175 | // The keep-alive timeout in milliseconds. When set to a non-zero value, | 167 | 176 | // ngtcp2 will automatically send PING frames to keep the connection alive | 168 | 177 | // before the idle timeout fires. Set to 0 to disable (default). | |
Collapse file Copy file name to clipboard +60Lines changed: 60 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Flags: --experimental-quic --experimental-stream-iter --no-warnings | | 2 | + | | 3 | +// Test: initialRtt session option is accepted and the session functions | | 4 | +// correctly with a custom initial RTT estimate. | | 5 | + | | 6 | +import { hasQuic, skip, mustCall } from '../common/index.mjs'; | | 7 | +import assert from 'node:assert'; | | 8 | + | | 9 | +const { ok } = assert; | | 10 | + | | 11 | +if (!hasQuic) { | | 12 | + skip('QUIC is not enabled'); | | 13 | +} | | 14 | + | | 15 | +const { listen, connect } = await import('../common/quic.mjs'); | | 16 | +const { bytes } = await import('stream/iter'); | | 17 | + | | 18 | +const encoder = new TextEncoder(); | | 19 | +const payload = encoder.encode('hello rtt'); | | 20 | +const serverDone = Promise.withResolvers(); | | 21 | + | | 22 | +// Use a low initialRtt (1ms) to simulate a low-latency environment. | | 23 | +// The session should complete successfully and the smoothed RTT in | | 24 | +// stats should converge to a value well below the default 333ms. | | 25 | +const serverEndpoint = await listen(mustCall((serverSession) => { | | 26 | + serverSession.onstream = mustCall(async (stream) => { | | 27 | + const data = await bytes(stream); | | 28 | + ok(data.byteLength > 0); | | 29 | + stream.writer.endSync(); | | 30 | + await stream.closed; | | 31 | + serverSession.close(); | | 32 | + serverDone.resolve(); | | 33 | + }); | | 34 | +}), { | | 35 | + initialRtt: 1, // 1ms | | 36 | +}); | | 37 | + | | 38 | +const clientSession = await connect(serverEndpoint.address, { | | 39 | + initialRtt: 1, // 1ms | | 40 | +}); | | 41 | +await clientSession.opened; | | 42 | + | | 43 | +const stream = await clientSession.createBidirectionalStream({ | | 44 | + body: payload, | | 45 | +}); | | 46 | + | | 47 | +for await (const _ of stream) { /* drain */ } // eslint-disable-line no-unused-vars | | 48 | +await stream.closed; | | 49 | +await serverDone.promise; | | 50 | + | | 51 | +// After data exchange, the smoothed RTT should have converged to a | | 52 | +// realistic value. On loopback it should be well under 10ms (10,000,000ns). | | 53 | +// The stat is in nanoseconds. | | 54 | +const smoothedRtt = clientSession.stats.smoothedRtt; | | 55 | +ok(smoothedRtt > 0n, 'smoothedRtt should be non-zero after data exchange'); | | 56 | +ok(smoothedRtt < 10_000_000n, | | 57 | + `smoothedRtt should be under 10ms on loopback, got ${smoothedRtt}ns`); | | 58 | + | | 59 | +await clientSession.close(); | | 60 | +await serverEndpoint.close(); |
|