Skip to content

Commit 0d83b2d

Browse files
committed
quic: update writedesiredsize on maxdataframe
This commit will react on a callback from ngtcp2, when a maxdata frame arrives and update the scheduled streams accordingly. A PR to get this callback is pending. Fixes: #64835 Signed-off-by: Marten Richter <marten.richter@freenet.de>
1 parent 8812357 commit 0d83b2d

5 files changed

Lines changed: 117 additions & 1 deletion

File tree

‎src/quic/application.cc‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ class DefaultApplication final : public Session::Application {
412412
void BlockStream(stream_id id) override {
413413
if (auto stream = session().FindStream(id)) [[likely]] {
414414
// Remove the stream from the send queue. It will be re-scheduled
415-
// via ExtendMaxStreamData when the peer grants more flow control.
415+
// via ExtendMax(Stream)Data when the peer grants more flow control.
416416
// Without this, SendPendingData would repeatedly pop and retry
417417
// the same blocked stream in an infinite loop.
418418
stream->Unschedule();
@@ -428,6 +428,14 @@ class DefaultApplication final : public Session::Application {
428428
stream->Schedule(&stream_queue_);
429429
}
430430

431+
void ExtendMaxData(uint64_t max_data) override {
432+
// The peer granted more flow control for session. Re-schedule
433+
// all streams so SendPendingData will resume writing.
434+
for (auto& [id, stream] : session().streams()) {
435+
stream->Schedule(&stream_queue_);
436+
}
437+
}
438+
431439
bool StreamCommit(Session::StreamData* stream_data, size_t datalen) override {
432440
DCHECK_NOT_NULL(stream_data);
433441
CHECK(stream_data->stream);

‎src/quic/application.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ class Session::Application : public MemoryRetainer {
141141
// By default do nothing.
142142
}
143143

144+
// Called when the Session determines that the flow control window for the
145+
// session has been expanded. Not all Application types will require
146+
// this notification so the default is to do nothing.
147+
virtual void ExtendMaxData(uint64_t max_data) {
148+
Debug(session_, "Application extending max data");
149+
// By default do nothing.
150+
}
151+
144152
// Different Applications may wish to set some application data in the
145153
// session ticket (e.g. http/3 would set server settings in the application
146154
// data). The first byte written MUST be the Application::Type enum value.

‎src/quic/http3.cc‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,16 @@ class Http3ApplicationImpl final : public Session::Application {
380380
nghttp3_conn_unblock_stream(*this, stream->id());
381381
}
382382

383+
void ExtendMaxData(uint64_t max_data) override {
384+
Debug(&session(),
385+
"HTTP/3 application extending max data to %" PRIu64,
386+
max_data);
387+
for (auto& [id, stream] : session().streams()) {
388+
stream->UpdateWriteDesiredSize(); // the stream might be blocked
389+
// on js side, is unblock stream also required?
390+
}
391+
}
392+
383393
void CollectSessionTicketAppData(
384394
SessionTicket::AppData* app_data) const override {
385395
uint8_t buf[kSessionTicketAppDataSize];

‎src/quic/session.cc‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,14 @@ struct Session::Impl final : public MemoryRetainer {
13591359
return NGTCP2_SUCCESS;
13601360
}
13611361

1362+
static int on_extend_max_data(ngtcp2_conn* conn,
1363+
uint64_t max_data,
1364+
void* user_data) {
1365+
NGTCP2_CALLBACK_SCOPE(session)
1366+
session->application().ExtendMaxData(max_data);
1367+
return NGTCP2_SUCCESS;
1368+
}
1369+
13621370
static int on_get_new_cid(ngtcp2_conn* conn,
13631371
ngtcp2_cid* cid,
13641372
ngtcp2_stateless_reset_token* token,
@@ -1706,7 +1714,10 @@ struct Session::Impl final : public MemoryRetainer {
17061714
on_receive_stream_stop_sending,
17071715
#ifdef NGTCP2_CALLBACKS_V5
17081716
nullptr, // stream_close2
1717+
#ifdef NGTCP2_CALLBACKS_V6
1718+
on_extend_max_data,
17091719
#endif
1720+
#endif // NGTCP2_CALLBACKS_V5
17101721
};
17111722

17121723
static constexpr ngtcp2_callbacks SERVER = {
@@ -1758,7 +1769,10 @@ struct Session::Impl final : public MemoryRetainer {
17581769
on_receive_stream_stop_sending,
17591770
#ifdef NGTCP2_CALLBACKS_V5
17601771
nullptr, // stream_close2
1772+
#ifdef NGTCP2_CALLBACKS_V6
1773+
on_extend_max_data,
17611774
#endif
1775+
#endif // NGTCP2_CALLBACKS_V5
17621776
};
17631777
};
17641778

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Flags: --experimental-quic --experimental-stream-iter --no-warnings
2+
3+
// Test: Quic maxdata updates on http/3
4+
// Client sends a body that precisely fills the session window size,
5+
// and verifies that it is data transfer is not stalled.
6+
7+
import { hasQuic, skip } from '../common/index.mjs';
8+
import { readFile } from 'node:fs/promises';
9+
import { setTimeout as sleep } from 'node:timers/promises';
10+
11+
if (!hasQuic) {
12+
skip('QUIC is not enabled');
13+
}
14+
const { listen, connect } = await import('node:quic');
15+
const { createPrivateKey } = await import('node:crypto');
16+
const { drainableProtocol } = await import('stream/iter');
17+
18+
const keys = 'test/fixtures/keys';
19+
const key = createPrivateKey(await readFile(`${keys}/agent1-key.pem`));
20+
const cert = await readFile(`${keys}/agent1-cert.pem`);
21+
22+
const WINDOW = 4096;
23+
// Fills the window exactly:
24+
// considers all framing including some initial session capsules
25+
const BODY = WINDOW - 38;
26+
27+
let letServerRead;
28+
const serverMayRead = new Promise((resolve) => { letServerRead = resolve; });
29+
30+
const endpoint = await listen((session) => {
31+
session.onstream = async (stream) => {
32+
await serverMayRead;
33+
// eslint-disable-next-line no-unused-vars
34+
for await (const _ of stream) { /* reading extends the window */ }
35+
};
36+
}, {
37+
sni: { '*': { keys: [key], certs: [cert] } },
38+
transportParams: {
39+
initialMaxStreamDataBidiRemote: 1024 * 1024, // Make sure maxstreamdata does not block
40+
initialMaxData: WINDOW,
41+
},
42+
onheaders() { this.sendHeaders({ ':status': '200' }); },
43+
});
44+
45+
const session = await connect(endpoint.address, {
46+
servername: 'localhost',
47+
verifyPeer: 'manual',
48+
});
49+
await session.opened;
50+
51+
// Budget well above the window, so the window is what stops the writer.
52+
const stream = await session.createBidirectionalStream({ budget: 1024 * 1024 });
53+
stream.sendHeaders({
54+
':method': 'POST',
55+
':path': '/',
56+
':scheme': 'https',
57+
':authority': 'localhost',
58+
}, { terminal: false });
59+
60+
const writer = stream.writer;
61+
writer.writeSync(new Uint8Array(BODY));
62+
63+
// Long enough for every byte to be acked. The peer acks as data arrives,
64+
// whether or not its application has read any of it, so by now the window is
65+
// exhausted, the send buffer is empty, and no further ACK can arrive.
66+
await sleep(500);
67+
68+
const watchdog = setTimeout(() => {
69+
console.error('STALLED: no drain after MAX_STREAM_DATA');
70+
process.exit(1);
71+
}, 5000);
72+
letServerRead(); // Extend the window, with no ack attached
73+
await writer[drainableProtocol]();
74+
75+
clearTimeout(watchdog);
76+
process.exit(0);

0 commit comments

Comments
 (0)