quicsilver 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -1
  3. data/.github/workflows/cibuildgem.yaml +29 -5
  4. data/.gitignore +5 -0
  5. data/CHANGELOG.md +50 -0
  6. data/Gemfile.lock +8 -1
  7. data/README.md +128 -15
  8. data/Rakefile +10 -3
  9. data/benchmarks/baseline.rb +175 -0
  10. data/benchmarks/compare.rb +398 -0
  11. data/benchmarks/helpers.rb +138 -64
  12. data/benchmarks/latency.rb +128 -0
  13. data/benchmarks/loss.rb +193 -0
  14. data/benchmarks/mobile.rb +125 -0
  15. data/docs/connection_identity.md +141 -0
  16. data/examples/benchmark.rb +80 -0
  17. data/examples/connection_pool_demo.rb +1 -1
  18. data/examples/example_helper.rb +51 -0
  19. data/examples/feature_demo.rb +7 -7
  20. data/examples/grpc_style.rb +2 -2
  21. data/examples/latency_aware_routing.rb +382 -0
  22. data/examples/lifecycle_drain.rb +264 -0
  23. data/examples/priorities.rb +2 -2
  24. data/examples/rails_feature_test.rb +92 -72
  25. data/examples/server_stats.rb +117 -0
  26. data/examples/simple_client_test.rb +3 -3
  27. data/examples/trailers.rb +23 -23
  28. data/examples/webtransport_smoke.rb +546 -0
  29. data/examples/worker_load_balancing.rb +425 -0
  30. data/ext/quicsilver/extconf.rb +100 -15
  31. data/ext/quicsilver/include/msquic.h +2226 -0
  32. data/ext/quicsilver/include/msquic_posix.h +696 -0
  33. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  34. data/ext/quicsilver/msquic_patch.rb +68 -0
  35. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  36. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  37. data/ext/quicsilver/quicsilver.c +1640 -222
  38. data/lib/quicsilver/client/client.rb +419 -60
  39. data/lib/quicsilver/client/connection_pool.rb +122 -26
  40. data/lib/quicsilver/client/request.rb +106 -14
  41. data/lib/quicsilver/errors.rb +39 -0
  42. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  43. data/lib/quicsilver/protocol/adapter.rb +41 -21
  44. data/lib/quicsilver/protocol/capsule.rb +38 -0
  45. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  46. data/lib/quicsilver/protocol/control_stream_parser.rb +12 -1
  47. data/lib/quicsilver/protocol/datagram.rb +40 -0
  48. data/lib/quicsilver/protocol/frame_parser.rb +5 -6
  49. data/lib/quicsilver/protocol/frame_reader.rb +31 -2
  50. data/lib/quicsilver/protocol/frames.rb +128 -12
  51. data/lib/quicsilver/protocol/priority.rb +9 -0
  52. data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
  53. data/lib/quicsilver/protocol/qpack/encoder.rb +10 -20
  54. data/lib/quicsilver/protocol/qpack/huffman.rb +4 -4
  55. data/lib/quicsilver/protocol/request_encoder.rb +6 -3
  56. data/lib/quicsilver/protocol/request_parser.rb +16 -2
  57. data/lib/quicsilver/protocol/response_encoder.rb +18 -8
  58. data/lib/quicsilver/protocol/response_parser.rb +6 -0
  59. data/lib/quicsilver/protocol/stream_input.rb +3 -3
  60. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  61. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  62. data/lib/quicsilver/rack/context.rb +33 -0
  63. data/lib/quicsilver/response.rb +40 -0
  64. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  65. data/lib/quicsilver/server/receive_queue.rb +106 -0
  66. data/lib/quicsilver/server/request_handler.rb +24 -13
  67. data/lib/quicsilver/server/request_registry.rb +18 -5
  68. data/lib/quicsilver/server/scheduler.rb +50 -0
  69. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  70. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  71. data/lib/quicsilver/server/server.rb +655 -185
  72. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  73. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  74. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  75. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  76. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  77. data/lib/quicsilver/transport/configuration.rb +103 -10
  78. data/lib/quicsilver/transport/connection.rb +302 -87
  79. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  80. data/lib/quicsilver/transport/event_loop.rb +36 -0
  81. data/lib/quicsilver/transport/inbound_stream.rb +21 -1
  82. data/lib/quicsilver/transport/stream.rb +18 -3
  83. data/lib/quicsilver/transport/stream_event.rb +25 -2
  84. data/lib/quicsilver/transport/stream_id.rb +26 -0
  85. data/lib/quicsilver/version.rb +1 -1
  86. data/lib/quicsilver.rb +22 -11
  87. data/lib/rackup/handler/quicsilver.rb +57 -38
  88. data/quicsilver.gemspec +10 -3
  89. metadata +54 -8
  90. data/benchmarks/concurrent.rb +0 -110
  91. data/benchmarks/quicsilver_server.rb +0 -46
  92. data/benchmarks/rails.rb +0 -170
  93. data/benchmarks/throughput.rb +0 -113
  94. data/lib/quicsilver/quicsilver.bundle +0 -0
@@ -1,9 +1,19 @@
1
1
  #include <ruby.h>
2
2
  #include <ruby/thread.h>
3
+ #include <ruby/st.h>
4
+ #include <pthread.h>
3
5
  #define QUIC_API_ENABLE_PREVIEW_FEATURES 1
4
6
  #include "msquic.h"
7
+
8
+ // A reset stream's final size arrives on the PEER_SEND_ABORTED event, added
9
+ // by patches/stream-final-size.patch. This is the value MsQuic uses while it
10
+ // is unsettled, and what we pass to Ruby as "not known".
11
+ #define QUICSILVER_FINAL_SIZE_UNKNOWN UINT64_MAX
12
+ #include <errno.h>
13
+ #include <limits.h>
5
14
  #include <stdlib.h>
6
15
  #include <string.h>
16
+ #include <time.h>
7
17
  #include <unistd.h>
8
18
 
9
19
  #if __linux__
@@ -57,6 +67,17 @@ typedef struct {
57
67
  QUIC_STATUS error_status;
58
68
  uint64_t error_code;
59
69
  VALUE client_obj; // Ruby client object (Qnil for server connections)
70
+ char remote_address[INET6_ADDRSTRLEN];
71
+ uint16_t remote_port;
72
+ int session_resumed;
73
+ int reliable_reset_negotiated;
74
+ // 0-RTT resumption ticket (client-side)
75
+ uint8_t* resumption_ticket;
76
+ uint32_t resumption_ticket_length;
77
+ // Datagrams queued for Ruby and dropped for lack of room, per connection.
78
+ // Both maintained under EventQueue.lock, not StateLock.
79
+ size_t queued_datagrams;
80
+ uint64_t dropped_datagrams;
60
81
  } ConnectionContext;
61
82
 
62
83
  // Listener state tracking
@@ -70,20 +91,387 @@ typedef struct {
70
91
 
71
92
  // Stream state tracking
72
93
  typedef struct {
94
+ HQUIC stream;
95
+ uint64_t token;
96
+ QUIC_STREAM_SHUTDOWN_FLAGS abort_flags;
97
+ uint32_t pending_priority;
98
+ uint64_t receive_credit;
99
+ uint64_t receive_chunks;
100
+ // Set under StateLock when a RECEIVE returned less than indicated, which
101
+ // makes MsQuic stop delivering. The next grant that leaves both credits
102
+ // positive clears it and calls StreamReceiveSetEnabled.
103
+ int receive_paused;
104
+ int receive_credit_enabled;
105
+ int receive_delivery_failed;
106
+ // Set when a RECEIVE_FIN has been queued for Ruby. Poll thread only.
107
+ int fin_delivered;
73
108
  HQUIC connection;
74
109
  void* connection_ctx; // ConnectionContext pointer (for building connection_data)
75
110
  VALUE client_obj; // Ruby client object (copied from connection context)
111
+ char remote_address[INET6_ADDRSTRLEN];
112
+ uint16_t remote_port;
113
+ int session_resumed;
114
+ uint64_t stream_id; // QUIC stream ID, cached once after StreamStart
76
115
  int started;
77
116
  int shutdown;
78
117
  int early_data; // Set when stream received 0-RTT data
79
118
  QUIC_STATUS error_status;
80
119
  } StreamContext;
81
120
 
82
- // Pending stream priorities — set from Ruby threads, applied on MsQuic event thread.
83
- // Simple array-based storage (max 256 pending). Key = stream handle, value = priority + 1.
84
- #define MAX_PENDING_PRIORITIES 256
85
- static struct { HQUIC stream; uint16_t priority_plus_one; } PendingPriorities[MAX_PENDING_PRIORITIES];
86
- static int PendingPriorityCount = 0;
121
+ // --- Threading model ---------------------------------------------------------
122
+ //
123
+ // Two threads touch this file's state, and neither holds the other's lock.
124
+ //
125
+ // The poll thread drives MsQuic (ExecutionPoll, kevent/epoll_wait, completion
126
+ // callbacks) with the GVL released for the whole loop. Nothing reachable from
127
+ // a StreamCallback or ConnectionCallback may call into Ruby. Callbacks push a
128
+ // RubyEvent onto EventQueue and return.
129
+ //
130
+ // The dispatcher thread, a Ruby Thread in Transport::EventLoop, waits on
131
+ // EventQueue without the GVL, then takes it to pop a batch and call
132
+ // Server.handle_stream / Client#handle_stream_event for each. It also does
133
+ // the releases that used to happen inside callbacks and either need the GVL
134
+ // (rb_gc_unregister_address) or free memory a Ruby thread could still be
135
+ // using (StreamContext, ConnectionContext, SendContext). Because every free
136
+ // happens on a thread holding the GVL, a Ruby thread that found a context
137
+ // cannot see it freed underneath it: the GVL is the lifetime guarantee, as it
138
+ // was before, just held by a different thread.
139
+ //
140
+ // StateLock covers LiveStreams and every context field one side writes and
141
+ // the other reads: receive credit and the paused flag, the lazily cached
142
+ // stream_id. Rule: never call MsQuic while holding StateLock. Callbacks run
143
+ // inside MsQuic's connection lock, so holding StateLock across an MsQuic call
144
+ // from a Ruby thread would invert the order and deadlock.
145
+ //
146
+ // Why the receive path can be asynchronous: MsQuic processes API calls on a
147
+ // stream (StreamReceiveSetEnabled, StreamSend) as queued operations on the
148
+ // connection's worker, which here is ExecutionPoll on the poll thread. A call
149
+ // made from a Ruby thread therefore runs after any callback in flight, never
150
+ // during it. So when a RECEIVE returns a partial length and MsQuic pauses the
151
+ // stream, a grant that observed receive_paused and queued SetEnabled is
152
+ // applied after the pause, not lost before it.
153
+ static pthread_mutex_t StateLock = PTHREAD_MUTEX_INITIALIZER;
154
+ #define STATE_LOCK() pthread_mutex_lock(&StateLock)
155
+ #define STATE_UNLOCK() pthread_mutex_unlock(&StateLock)
156
+
157
+ // Tokens are never reused. find_stream returns a pointer whose lifetime is
158
+ // guaranteed by the GVL: contexts are freed only on the dispatcher thread,
159
+ // which holds it. So a caller must hold the GVL from the lookup through the
160
+ // last use of the pointer and must not release it in between, including via
161
+ // rb_thread_call_without_gvl, rb_thread_schedule, or any Ruby call that can
162
+ // block. Inserting one of those between find_stream and the MsQuic call it
163
+ // guards is a use-after-free.
164
+ static st_table* LiveStreams;
165
+
166
+ typedef enum {
167
+ RELEASE_NONE = 0,
168
+ RELEASE_SEND, // SendContext*: drop the retained String
169
+ RELEASE_STREAM, // StreamContext*: StreamClose + free
170
+ RELEASE_STREAM_NOCLOSE, // StreamContext*: free only, app is closing
171
+ RELEASE_CONNECTION, // ConnectionContext*: unregister client_obj, free
172
+ } ReleaseKind;
173
+
174
+ // Most events carry a token or a token plus two integers; those fit inline
175
+ // and cost no malloc. RECEIVE payloads are handed over already allocated.
176
+ #define EVENT_INLINE_DATA 40
177
+
178
+ typedef struct {
179
+ HQUIC connection;
180
+ void* connection_ctx;
181
+ VALUE client_obj;
182
+ const char* event_type; // string literal, never freed
183
+ uint64_t stream_id;
184
+ char* data; // malloc'd when data_len > EVENT_INLINE_DATA
185
+ size_t data_len;
186
+ char inline_data[EVENT_INLINE_DATA];
187
+ int early_data;
188
+ int dropped; // superseded datagram: free, do not deliver
189
+ ReleaseKind release;
190
+ void* release_ptr;
191
+
192
+ // A pending receive: MsQuic owns the bytes until StreamReceiveComplete.
193
+ // Descriptors are copied (callback-scoped); data is NULL.
194
+ HQUIC stream;
195
+ uint64_t token;
196
+ uint64_t accepted;
197
+ QUIC_BUFFER* buffers; // inline_buffers, or malloc'd when more
198
+ uint32_t buffer_count;
199
+ QUIC_BUFFER inline_buffers[4];
200
+ } RubyEvent;
201
+
202
+ #define EVENT_INLINE_BUFFERS 4
203
+
204
+ static struct {
205
+ pthread_mutex_t lock;
206
+ pthread_cond_t ready;
207
+ RubyEvent* buf;
208
+ size_t cap, head, len;
209
+ int closed;
210
+ } EventQueue = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, NULL, 0, 0, 0, 0 };
211
+
212
+ // The queue is unbounded for stream events, which stream limits and receive
213
+ // credit already bound. Datagrams have no flow control (RFC 9221 §5.3) and
214
+ // MAY be dropped when the receiver cannot process them, so each connection
215
+ // may have at most this many queued ahead of the dispatcher. Per connection,
216
+ // not global: one flooding peer must not cost every other peer its datagrams.
217
+ // 128 is quic-go's per-connection receive queue; neqo uses 10, quiche makes
218
+ // the application choose. When full the oldest is dropped, not the newest:
219
+ // for the signals datagrams carry (presence, positions) the stale one is the
220
+ // one nobody wants, which is quiche's and neqo's policy too. The count is of
221
+ // datagrams, so a backlog of stream events does not cost datagrams.
222
+ #define DATAGRAM_QUEUE_LIMIT 128
223
+ static uint64_t DroppedDatagrams = 0;
224
+
225
+ // Under EventQueue.lock. Finds the oldest undropped datagram for the
226
+ // connection and marks it; the dispatcher frees it without delivering.
227
+ static void
228
+ drop_oldest_datagram(void* connection_ctx)
229
+ {
230
+ for (size_t i = 0; i < EventQueue.len; i++) {
231
+ RubyEvent* ev = &EventQueue.buf[(EventQueue.head + i) % EventQueue.cap];
232
+ if (ev->connection_ctx == connection_ctx && !ev->dropped &&
233
+ strcmp(ev->event_type, "DATAGRAM_RECEIVED") == 0) {
234
+ ev->dropped = 1;
235
+ return;
236
+ }
237
+ }
238
+ }
239
+
240
+ // Grow by doubling, re-linearising the ring. Unbounded on purpose: a bounded
241
+ // queue that blocks the poll thread would deadlock against a Ruby thread
242
+ // calling StreamSend (needs the connection lock the blocked callback holds),
243
+ // and one that drops would lose stream data. Per-stream receive credit is
244
+ // what bounds memory, as before.
245
+ static int
246
+ event_queue_grow(void)
247
+ {
248
+ size_t cap = EventQueue.cap ? EventQueue.cap * 2 : 256;
249
+ RubyEvent* buf = malloc(cap * sizeof(RubyEvent));
250
+ if (!buf) return 0;
251
+ for (size_t i = 0; i < EventQueue.len; i++) {
252
+ RubyEvent* from = &EventQueue.buf[(EventQueue.head + i) % EventQueue.cap];
253
+ buf[i] = *from;
254
+ // Inline descriptors must point at their own copy.
255
+ if (from->buffers == from->inline_buffers) buf[i].buffers = buf[i].inline_buffers;
256
+ }
257
+ free(EventQueue.buf);
258
+ EventQueue.buf = buf;
259
+ EventQueue.cap = cap;
260
+ EventQueue.head = 0;
261
+ return 1;
262
+ }
263
+
264
+ static int event_queue_push(RubyEvent* evp);
265
+
266
+ // `owned` is a malloc'd buffer the queue takes over; otherwise data is copied.
267
+ static int
268
+ queue_event_full(HQUIC connection, void* connection_ctx, VALUE client_obj,
269
+ const char* event_type, uint64_t stream_id,
270
+ const char* data, size_t data_len, int early_data,
271
+ char* owned, ReleaseKind release, void* release_ptr)
272
+ {
273
+ RubyEvent ev;
274
+ ev.connection = connection;
275
+ ev.connection_ctx = connection_ctx;
276
+ ev.client_obj = client_obj;
277
+ ev.event_type = event_type;
278
+ ev.stream_id = stream_id;
279
+ ev.data_len = data_len;
280
+ ev.early_data = early_data;
281
+ ev.release = release;
282
+ ev.release_ptr = release_ptr;
283
+ if (owned) {
284
+ ev.data = owned;
285
+ } else if (data_len <= EVENT_INLINE_DATA) {
286
+ ev.data = NULL;
287
+ if (data_len) memcpy(ev.inline_data, data, data_len);
288
+ } else {
289
+ ev.data = malloc(data_len);
290
+ if (!ev.data) return 0;
291
+ memcpy(ev.data, data, data_len);
292
+ }
293
+
294
+ ev.dropped = 0;
295
+ ev.stream = NULL;
296
+ ev.buffers = NULL;
297
+ ev.buffer_count = 0;
298
+ return event_queue_push(&ev);
299
+ }
300
+
301
+ // Copies the record into the ring; frees ev->data on failure.
302
+ static int
303
+ event_queue_push(RubyEvent* evp)
304
+ {
305
+ RubyEvent ev = *evp;
306
+ const char* event_type = ev.event_type;
307
+ void* connection_ctx = ev.connection_ctx;
308
+ int is_datagram = strcmp(event_type, "DATAGRAM_RECEIVED") == 0;
309
+ ConnectionContext* conn = (ConnectionContext*)connection_ctx;
310
+ pthread_mutex_lock(&EventQueue.lock);
311
+ if (is_datagram && conn && conn->queued_datagrams >= DATAGRAM_QUEUE_LIMIT) {
312
+ // Make room by dropping the oldest; the new one is queued below.
313
+ drop_oldest_datagram(conn);
314
+ conn->queued_datagrams--;
315
+ conn->dropped_datagrams++;
316
+ DroppedDatagrams++;
317
+ }
318
+ if (EventQueue.len == EventQueue.cap && !event_queue_grow()) {
319
+ pthread_mutex_unlock(&EventQueue.lock);
320
+ free(ev.data);
321
+ return 0;
322
+ }
323
+ RubyEvent* slot = &EventQueue.buf[(EventQueue.head + EventQueue.len) % EventQueue.cap];
324
+ *slot = ev;
325
+ if (ev.buffers == evp->inline_buffers) slot->buffers = slot->inline_buffers;
326
+ EventQueue.len++;
327
+ if (is_datagram && conn) conn->queued_datagrams++;
328
+ pthread_cond_signal(&EventQueue.ready);
329
+ pthread_mutex_unlock(&EventQueue.lock);
330
+ return 1;
331
+ }
332
+
333
+ static int
334
+ queue_event(HQUIC connection, void* connection_ctx, VALUE client_obj,
335
+ const char* event_type, uint64_t stream_id,
336
+ const char* data, size_t data_len, int early_data)
337
+ {
338
+ return queue_event_full(connection, connection_ctx, client_obj, event_type, stream_id,
339
+ data, data_len, early_data, NULL, RELEASE_NONE, NULL);
340
+ }
341
+
342
+ // --- Zero-copy sends ---------------------------------------------------------
343
+ //
344
+ // A send used to malloc a QUIC_BUFFER plus a copy of the body and memcpy the
345
+ // body into it, with the GVL held, then free it on SEND_COMPLETE. For a 12MB
346
+ // response that is a 12MB allocate-and-copy during which no other Ruby thread
347
+ // runs. Instead the Ruby String is frozen and retained, and QUIC_BUFFER points
348
+ // straight at its bytes. MsQuic owns the pointer until SEND_COMPLETE, so the
349
+ // String must not move or be collected until then: LiveSends is a GC root
350
+ // that marks every retained String, and SEND_COMPLETE removes the entry.
351
+ //
352
+ // Freezing is what makes the pointer stable. A frozen String cannot be
353
+ // resized or have its buffer replaced, and the encoder hands these Strings
354
+ // over without reusing them. Compaction could still move an unfrozen embedded
355
+ // String; rb_gc_mark (not rb_gc_mark_movable) pins what it marks, so the
356
+ // retained Strings stay put.
357
+ //
358
+ // Every mutation of LiveSends happens with the GVL held: quicsilver_send_stream
359
+ // is a Ruby method, and the release for SEND_COMPLETE runs on the dispatcher
360
+ // thread, not in the callback. GC runs with the GVL. So the table needs no
361
+ // lock.
362
+ typedef struct {
363
+ QUIC_BUFFER buffer;
364
+ VALUE data;
365
+ } SendContext;
366
+
367
+ static st_table* LiveSends;
368
+ static VALUE LiveSendsRoot = Qnil;
369
+ // gc_mark_children skips a T_DATA's dmark when its data pointer is NULL, so
370
+ // the root wraps this instead of NULL. Its value is never read.
371
+ static int LiveSendsRootData;
372
+
373
+ static int
374
+ mark_live_send(st_data_t key, st_data_t value, st_data_t arg)
375
+ {
376
+ (void)key; (void)arg;
377
+ rb_gc_mark(((SendContext*)value)->data);
378
+ return ST_CONTINUE;
379
+ }
380
+
381
+ static void
382
+ live_sends_mark(void* ptr)
383
+ {
384
+ (void)ptr;
385
+ if (LiveSends) st_foreach(LiveSends, mark_live_send, 0);
386
+ }
387
+
388
+ static const rb_data_type_t live_sends_type = {
389
+ "Quicsilver::LiveSends",
390
+ { live_sends_mark, NULL, NULL, },
391
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
392
+ };
393
+
394
+ static SendContext*
395
+ send_context_new(VALUE data)
396
+ {
397
+ SendContext* ctx = malloc(sizeof(SendContext));
398
+ if (!ctx) return NULL;
399
+ ctx->data = data;
400
+ ctx->buffer.Buffer = (uint8_t*)RSTRING_PTR(data);
401
+ ctx->buffer.Length = (uint32_t)RSTRING_LEN(data);
402
+ st_insert(LiveSends, (st_data_t)ctx, (st_data_t)ctx);
403
+ return ctx;
404
+ }
405
+
406
+ static void
407
+ send_context_release(void* raw)
408
+ {
409
+ if (!raw) return;
410
+ st_data_t key = (st_data_t)raw;
411
+ st_delete(LiveSends, &key, NULL);
412
+ free(raw);
413
+ }
414
+ static uint64_t NextStreamToken = 1;
415
+
416
+ static uint64_t
417
+ register_stream(StreamContext* ctx, HQUIC stream, QUIC_STREAM_SHUTDOWN_FLAGS flags)
418
+ {
419
+ ctx->stream = stream;
420
+ ctx->abort_flags = flags;
421
+ ctx->receive_credit = 0;
422
+ ctx->receive_chunks = 0;
423
+ ctx->receive_paused = 0;
424
+ ctx->receive_credit_enabled = 0;
425
+ ctx->receive_delivery_failed = 0;
426
+ ctx->fin_delivered = 0;
427
+ ctx->pending_priority = 0;
428
+ // The token is minted under the same lock that inserts it. Peer streams
429
+ // are registered on the poll thread and client streams on Ruby threads;
430
+ // with the poll thread no longer holding the GVL, a bare increment here
431
+ // handed two streams one token, and send_stream resolved to the wrong one.
432
+ // Found as a client receiving its own request frame as a response.
433
+ STATE_LOCK();
434
+ ctx->token = NextStreamToken++;
435
+ st_insert(LiveStreams, (st_data_t)ctx->token, (st_data_t)ctx);
436
+ STATE_UNLOCK();
437
+ return ctx->token;
438
+ }
439
+
440
+ static StreamContext*
441
+ find_stream(uint64_t token)
442
+ {
443
+ st_data_t entry;
444
+ STATE_LOCK();
445
+ int found = st_lookup(LiveStreams, (st_data_t)token, &entry);
446
+ STATE_UNLOCK();
447
+ return found ? (StreamContext*)entry : NULL;
448
+ }
449
+
450
+ static void
451
+ unregister_stream(uint64_t token)
452
+ {
453
+ st_data_t key = (st_data_t)token;
454
+ STATE_LOCK();
455
+ st_delete(LiveStreams, &key, NULL);
456
+ STATE_UNLOCK();
457
+ }
458
+
459
+ static int
460
+ forget_connection_stream(st_data_t key, st_data_t value, st_data_t connection)
461
+ {
462
+ StreamContext* ctx = (StreamContext*)value;
463
+ if (ctx->connection != (HQUIC)connection) return ST_CONTINUE;
464
+ return ST_DELETE;
465
+ }
466
+
467
+ static void
468
+ forget_connection_streams(HQUIC connection)
469
+ {
470
+ STATE_LOCK();
471
+ st_foreach(LiveStreams, forget_connection_stream, (st_data_t)connection);
472
+ STATE_UNLOCK();
473
+ }
474
+
87
475
 
88
476
  // rb_protect wrapper — catches Ruby exceptions so they don't longjmp
89
477
  // through MsQuic callback frames (which would corrupt MsQuic state).
@@ -92,6 +480,15 @@ struct dispatch_ruby_args {
92
480
  HQUIC connection;
93
481
  void* connection_ctx;
94
482
  VALUE client_obj;
483
+ // For a pending receive: gather these into the String after the token.
484
+ const QUIC_BUFFER* buffers;
485
+ uint32_t buffer_count;
486
+ uint64_t token;
487
+ uint64_t accepted;
488
+ VALUE payload; // Qundef, or a String built before dispatch
489
+ char remote_address[INET6_ADDRSTRLEN];
490
+ uint16_t remote_port;
491
+ int session_resumed;
95
492
  const char* event_type;
96
493
  uint64_t stream_id;
97
494
  const char* data;
@@ -99,22 +496,50 @@ struct dispatch_ruby_args {
99
496
  int early_data;
100
497
  };
101
498
 
499
+ // [token][bytes], gathered straight from MsQuic's buffers.
500
+ static VALUE
501
+ gather_receive_string(struct dispatch_ruby_args* a)
502
+ {
503
+ VALUE str = rb_str_new(NULL, (long)(sizeof(a->token) + a->accepted));
504
+ char* out = RSTRING_PTR(str);
505
+ memcpy(out, &a->token, sizeof(a->token));
506
+ out += sizeof(a->token);
507
+ uint64_t remaining = a->accepted;
508
+ for (uint32_t b = 0; b < a->buffer_count && remaining; b++) {
509
+ size_t len = a->buffers[b].Length;
510
+ if (len > remaining) len = (size_t)remaining;
511
+ memcpy(out, a->buffers[b].Buffer, len);
512
+ out += len;
513
+ remaining -= len;
514
+ }
515
+ return str;
516
+ }
517
+
518
+ static VALUE
519
+ gather_receive_protected(VALUE arg)
520
+ {
521
+ return gather_receive_string((struct dispatch_ruby_args*)arg);
522
+ }
523
+
102
524
  static VALUE
103
525
  dispatch_ruby_body(VALUE arg)
104
526
  {
105
527
  struct dispatch_ruby_args* a = (struct dispatch_ruby_args*)arg;
528
+ VALUE payload = a->payload != Qundef ? a->payload : rb_str_new(a->data, a->data_len);
106
529
 
107
530
  if (NIL_P(a->client_obj)) {
108
531
  VALUE server_class = rb_const_get_at(mQuicsilver, rb_intern("Server"));
109
532
  if (rb_class_real(CLASS_OF(server_class)) == rb_cClass) {
110
- VALUE connection_data = rb_ary_new2(2);
533
+ ConnectionContext* conn_ctx = (ConnectionContext*)a->connection_ctx;
534
+ VALUE connection_data = rb_ary_new2(3);
111
535
  rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection));
112
536
  rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection_ctx));
537
+ rb_ary_push(connection_data, conn_ctx && conn_ctx->session_resumed ? Qtrue : Qfalse);
113
538
  VALUE argv[5] = {
114
539
  connection_data,
115
540
  ULL2NUM(a->stream_id),
116
541
  rb_str_new_cstr(a->event_type),
117
- rb_str_new(a->data, a->data_len),
542
+ payload,
118
543
  a->early_data ? Qtrue : Qfalse
119
544
  };
120
545
  rb_funcallv(server_class, rb_intern("handle_stream"), 5, argv);
@@ -124,7 +549,7 @@ dispatch_ruby_body(VALUE arg)
124
549
  VALUE argv[4] = {
125
550
  ULL2NUM(a->stream_id),
126
551
  rb_str_new_cstr(a->event_type),
127
- rb_str_new(a->data, a->data_len),
552
+ payload,
128
553
  a->early_data ? Qtrue : Qfalse
129
554
  };
130
555
  rb_funcallv(a->client_obj, rb_intern("handle_stream_event"), 4, argv);
@@ -134,14 +559,40 @@ dispatch_ruby_body(VALUE arg)
134
559
  return Qnil;
135
560
  }
136
561
 
137
- // Dispatch event to Ruby — entire body wrapped in rb_protect so no Ruby call
138
- // (object construction or funcall) can longjmp through MsQuic callback frames.
139
- static void
562
+ static VALUE
563
+ format_dispatch_error(VALUE err)
564
+ {
565
+ VALUE klass = rb_class_name(rb_obj_class(err));
566
+ VALUE msg = rb_funcall(err, rb_intern("message"), 0);
567
+ VALUE bt = rb_funcall(err, rb_intern("backtrace"), 0);
568
+ fprintf(stderr, "Quicsilver: exception in callback: %s: %s\n",
569
+ StringValueCStr(klass), StringValueCStr(msg));
570
+ if (RB_TYPE_P(bt, T_ARRAY) && RARRAY_LEN(bt) > 0) {
571
+ long bt_len = RARRAY_LEN(bt) < 5 ? RARRAY_LEN(bt) : 5;
572
+ for (long i = 0; i < bt_len; i++) {
573
+ VALUE line = rb_ary_entry(bt, i);
574
+ fprintf(stderr, " %s\n", StringValueCStr(line));
575
+ }
576
+ }
577
+ return Qnil;
578
+ }
579
+
580
+ // Deliver one event to Ruby, on the dispatcher thread, with the GVL. The body
581
+ // is wrapped in rb_protect so an exception in the handler is logged rather
582
+ // than unwinding the dispatcher. Returns the protect state: nonzero means the
583
+ // handler raised.
584
+ static int
140
585
  dispatch_to_ruby(HQUIC connection, void* connection_ctx, VALUE client_obj,
141
586
  const char* event_type, uint64_t stream_id,
142
- const char* data, size_t data_len, int early_data)
587
+ const char* data, size_t data_len, int early_data,
588
+ VALUE prebuilt)
143
589
  {
144
590
  struct dispatch_ruby_args args;
591
+ args.buffers = NULL;
592
+ args.buffer_count = 0;
593
+ args.token = 0;
594
+ args.accepted = 0;
595
+ args.payload = prebuilt;
145
596
  args.connection = connection;
146
597
  args.connection_ctx = connection_ctx;
147
598
  args.client_obj = client_obj;
@@ -154,9 +605,129 @@ dispatch_to_ruby(HQUIC connection, void* connection_ctx, VALUE client_obj,
154
605
  int state = 0;
155
606
  rb_protect(dispatch_ruby_body, (VALUE)&args, &state);
156
607
  if (state) {
608
+ VALUE err = rb_errinfo();
157
609
  rb_set_errinfo(Qnil);
158
- fprintf(stderr, "Quicsilver: exception in callback\n");
610
+ if (!NIL_P(err)) {
611
+ // #message and #backtrace are Ruby methods and can raise. Protect
612
+ // them too, or an exception in the handler's exception takes the
613
+ // dispatcher thread down.
614
+ int format_state = 0;
615
+ rb_protect(format_dispatch_error, err, &format_state);
616
+ if (format_state) {
617
+ rb_set_errinfo(Qnil);
618
+ fprintf(stderr, "Quicsilver: exception in callback (unprintable)\n");
619
+ }
620
+ }
159
621
  }
622
+ return state;
623
+ }
624
+
625
+ // Unsigned credit argument: NUM2ULL wraps negatives, so check the sign first.
626
+ static uint64_t
627
+ credit_arg(VALUE value, const char* label)
628
+ {
629
+ if (!RB_INTEGER_TYPE_P(value)) rb_raise(rb_eTypeError, "%s must be an integer", label);
630
+ if (RTEST(rb_funcall(value, rb_intern("<"), 1, INT2FIX(0)))) {
631
+ rb_raise(rb_eArgError, "%s must not be negative", label);
632
+ }
633
+ return NUM2ULL(value);
634
+ }
635
+
636
+ static int
637
+ resume_receive(StreamContext* ctx)
638
+ {
639
+ QUIC_STATUS status = MsQuic->StreamReceiveSetEnabled(ctx->stream, TRUE);
640
+ if (QUIC_FAILED(status)) return 0;
641
+ return 1;
642
+ }
643
+
644
+ // Returns QUIC_STATUS_PENDING: MsQuic keeps the bytes until the dispatcher
645
+ // has copied them into the Ruby String and called StreamReceiveComplete. The
646
+ // bytes outlive this callback (freed at StreamClose, which the dispatcher does
647
+ // later); the QUIC_BUFFER descriptors do not and are copied. One receive per
648
+ // stream is pending at a time, so the queue holds at most one per stream.
649
+ //
650
+ // When credit enforcement is active (receive_credit_enabled, set by the first
651
+ // grant) we accept only what fits and complete with that length; MsQuic then
652
+ // disables further receives until StreamReceiveSetEnabled, which the next
653
+ // sufficient grant issues (see receive_paused). A partially accepted receive
654
+ // never carries FIN: it arrives with the suffix.
655
+ static QUIC_STATUS
656
+ receive_with_credit(StreamContext* ctx, QUIC_STREAM_EVENT* event)
657
+ {
658
+ uint64_t indicated = event->RECEIVE.TotalBufferLength;
659
+ uint64_t accepted = indicated;
660
+
661
+ // Decide and debit under one lock hold, so a concurrent grant sees either
662
+ // the state before this receive or after it, never the middle.
663
+ STATE_LOCK();
664
+ int failed_before = ctx->receive_delivery_failed;
665
+ if (ctx->receive_credit_enabled) {
666
+ if (accepted > ctx->receive_credit) accepted = ctx->receive_credit;
667
+ if (!ctx->receive_chunks) accepted = 0;
668
+ ctx->receive_credit -= accepted;
669
+ if (accepted) ctx->receive_chunks--;
670
+ if (accepted < indicated) ctx->receive_paused = 1;
671
+ }
672
+ STATE_UNLOCK();
673
+
674
+ int has_fin = (event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) && accepted == indicated;
675
+ if (failed_before || (!accepted && !has_fin)) {
676
+ event->RECEIVE.TotalBufferLength = 0;
677
+ return QUIC_STATUS_SUCCESS;
678
+ }
679
+ if (accepted > LONG_MAX - sizeof(uint64_t)) goto failed;
680
+
681
+ RubyEvent ev;
682
+ memset(&ev, 0, sizeof(ev));
683
+ ev.connection = ctx->connection;
684
+ ev.connection_ctx = ctx->connection_ctx;
685
+ ev.client_obj = ctx->client_obj;
686
+ ev.event_type = has_fin ? "RECEIVE_FIN" : "RECEIVE";
687
+ ev.stream_id = ctx->stream_id;
688
+ ev.early_data = has_fin ? ctx->early_data : 0;
689
+ ev.stream = ctx->stream;
690
+ ev.token = ctx->token;
691
+ ev.accepted = accepted;
692
+ ev.data = NULL;
693
+ ev.data_len = sizeof(uint64_t) + (size_t)accepted;
694
+
695
+ uint32_t count = 0;
696
+ uint64_t covered = 0;
697
+ while (count < event->RECEIVE.BufferCount && covered < accepted) {
698
+ covered += event->RECEIVE.Buffers[count].Length;
699
+ count++;
700
+ }
701
+ ev.buffer_count = count;
702
+ if (count <= EVENT_INLINE_BUFFERS) {
703
+ ev.buffers = ev.inline_buffers;
704
+ } else {
705
+ ev.buffers = malloc(count * sizeof(QUIC_BUFFER));
706
+ if (!ev.buffers) goto failed;
707
+ }
708
+ memcpy(ev.buffers, event->RECEIVE.Buffers, count * sizeof(QUIC_BUFFER));
709
+
710
+ if (!event_queue_push(&ev)) {
711
+ if (ev.buffers != ev.inline_buffers) free(ev.buffers);
712
+ goto failed;
713
+ }
714
+ if (has_fin) ctx->fin_delivered = 1;
715
+ return QUIC_STATUS_PENDING;
716
+
717
+ failed:
718
+ // Last resort: a receive we could not hand to Ruby is data the stream can
719
+ // never deliver in order, so the stream ends here.
720
+ fprintf(stderr, "Quicsilver: receive delivery failed on stream %llu; aborting\n",
721
+ (unsigned long long)ctx->stream_id);
722
+ STATE_LOCK();
723
+ ctx->receive_delivery_failed = 1;
724
+ ctx->receive_credit = 0;
725
+ STATE_UNLOCK();
726
+ // Already in a native callback; inline abort avoids allocating a queued
727
+ // operation on this failure path. It may retire ctx before returning.
728
+ MsQuic->StreamShutdown(ctx->stream, ctx->abort_flags | QUIC_STREAM_SHUTDOWN_FLAG_INLINE, 0);
729
+ event->RECEIVE.TotalBufferLength = 0;
730
+ return QUIC_STATUS_SUCCESS;
160
731
  }
161
732
 
162
733
  // Platform I/O wait — called without GVL so other Ruby threads can run
@@ -174,11 +745,13 @@ eventq_wait_nogvl(void* arg)
174
745
  struct poll_args* a = (struct poll_args*)arg;
175
746
  #if __linux__
176
747
  a->count = epoll_wait(a->eq, a->events, a->max_events, a->timeout_ms);
748
+ if (a->count < 0 && errno == EINTR) a->count = 0;
177
749
  #elif __APPLE__ || __FreeBSD__
178
750
  struct timespec ts;
179
751
  ts.tv_sec = a->timeout_ms / 1000;
180
752
  ts.tv_nsec = (a->timeout_ms % 1000) * 1000000;
181
753
  a->count = kevent(a->eq, NULL, 0, a->events, a->max_events, &ts);
754
+ if (a->count < 0 && errno == EINTR) a->count = 0;
182
755
  #endif
183
756
  return NULL;
184
757
  }
@@ -195,81 +768,237 @@ cqe_get_sqe(QUIC_CQE* cqe)
195
768
 
196
769
  // Drive MsQuic execution: poll internal timers, wait for I/O, fire completions.
197
770
  // Callbacks (StreamCallback, ConnectionCallback) fire HERE on the Ruby thread.
198
- static VALUE
199
- quicsilver_poll(VALUE self)
771
+ // One iteration of the MsQuic loop, with the GVL released for all of it.
772
+ //
773
+ // Before, only the kevent/epoll_wait sleep released the GVL; ExecutionPoll and
774
+ // the completion callbacks, which is where MsQuic decrypts packets, runs loss
775
+ // recovery and reassembles streams, ran with it held, because the callbacks
776
+ // called into Ruby. They no longer do, so none of this needs the lock, and
777
+ // the dispatcher and worker threads run in parallel with the datapath.
778
+ static void*
779
+ poll_nogvl(void* arg)
200
780
  {
201
- if (ExecContext == NULL) return INT2NUM(0);
781
+ struct poll_args* a = (struct poll_args*)arg;
202
782
 
203
- // 1. ExecutionPoll — process MsQuic timers/state, may fire callbacks (has GVL)
204
783
  uint32_t wait_ms = MsQuic->ExecutionPoll(ExecContext);
205
-
206
- // 2. Wait for I/O completions (releases GVL)
207
- struct poll_args args;
208
- args.eq = EventQ;
209
- args.max_events = 64;
210
784
  // With wake_event_loop(), Ruby threads instantly unblock us when work is
211
785
  // queued. Cap at 1s as a safety net for shutdown responsiveness.
212
- uint32_t actual_wait = (wait_ms == UINT32_MAX) ? 1000 : wait_ms;
213
- args.timeout_ms = (int)actual_wait;
214
- args.count = 0;
786
+ a->timeout_ms = (int)((wait_ms == UINT32_MAX) ? 1000 : wait_ms);
787
+ a->count = 0;
788
+ eventq_wait_nogvl(a);
215
789
 
216
- rb_thread_call_without_gvl(eventq_wait_nogvl, &args, RUBY_UBF_IO, NULL);
217
-
218
- // 3. Fire completions — MsQuic callbacks run here (has GVL)
219
- for (int i = 0; i < args.count; i++) {
790
+ for (int i = 0; i < a->count; i++) {
220
791
  #if __linux__
221
- if (args.events[i].data.ptr == NULL) {
792
+ if (a->events[i].data.ptr == NULL) {
222
793
  uint64_t val;
223
794
  read(WakeFd, &val, sizeof(val)); // drain eventfd
224
795
  continue;
225
796
  }
226
797
  #elif __APPLE__ || __FreeBSD__
227
- if (args.events[i].filter == EVFILT_USER && args.events[i].ident == WAKE_IDENT) continue;
798
+ if (a->events[i].filter == EVFILT_USER && a->events[i].ident == WAKE_IDENT) continue;
228
799
  #endif
229
- QUIC_SQE* sqe = cqe_get_sqe(&args.events[i]);
800
+ QUIC_SQE* sqe = cqe_get_sqe(&a->events[i]);
230
801
  if (sqe && sqe->Completion) {
231
- sqe->Completion(&args.events[i]);
802
+ sqe->Completion(&a->events[i]);
232
803
  }
233
804
  }
805
+ return NULL;
806
+ }
807
+
808
+ static VALUE
809
+ quicsilver_poll(VALUE self)
810
+ {
811
+ if (ExecContext == NULL) return INT2NUM(0);
234
812
 
813
+ struct poll_args args;
814
+ args.eq = EventQ;
815
+ args.max_events = 64;
816
+ rb_thread_call_without_gvl(poll_nogvl, &args, RUBY_UBF_IO, NULL);
235
817
  return INT2NUM(args.count);
236
818
  }
237
819
 
238
- // Inline poll for use during synchronous waits (e.g. wait_for_connection).
239
- // Short non-blocking poll — keeps MsQuic alive while we spin.
820
+ // --- Dispatcher side ---------------------------------------------------------
821
+
822
+ // Upper bound on events per dispatch_events call, and the size of the batch
823
+ // copied out under the queue lock. Fixed rather than caller-sized so the
824
+ // stack frame is known.
825
+ #define DISPATCH_BATCH_MAX 64
826
+
827
+ struct queue_wait_args {
828
+ int timeout_ms;
829
+ };
830
+
831
+ static void*
832
+ queue_wait_nogvl(void* arg)
833
+ {
834
+ struct queue_wait_args* a = (struct queue_wait_args*)arg;
835
+ struct timespec deadline;
836
+ clock_gettime(CLOCK_REALTIME, &deadline);
837
+ deadline.tv_sec += a->timeout_ms / 1000;
838
+ deadline.tv_nsec += (long)(a->timeout_ms % 1000) * 1000000L;
839
+ if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; }
840
+
841
+ pthread_mutex_lock(&EventQueue.lock);
842
+ while (EventQueue.len == 0 && !EventQueue.closed) {
843
+ if (pthread_cond_timedwait(&EventQueue.ready, &EventQueue.lock, &deadline) == ETIMEDOUT) break;
844
+ }
845
+ pthread_mutex_unlock(&EventQueue.lock);
846
+ return NULL;
847
+ }
848
+
849
+ // Thread#raise / Thread#kill against a dispatcher blocked in the wait.
240
850
  static void
241
- poll_inline(int timeout_ms)
851
+ queue_wake_ubf(void* arg)
242
852
  {
243
- if (ExecContext == NULL) return;
853
+ (void)arg;
854
+ pthread_mutex_lock(&EventQueue.lock);
855
+ pthread_cond_broadcast(&EventQueue.ready);
856
+ pthread_mutex_unlock(&EventQueue.lock);
857
+ }
244
858
 
245
- MsQuic->ExecutionPoll(ExecContext);
859
+ // What a callback used to do after dispatch, done here with the GVL held.
860
+ static void
861
+ perform_release(RubyEvent* ev)
862
+ {
863
+ switch (ev->release) {
864
+ case RELEASE_NONE:
865
+ break;
866
+ case RELEASE_SEND:
867
+ send_context_release(ev->release_ptr);
868
+ break;
869
+ case RELEASE_STREAM:
870
+ case RELEASE_STREAM_NOCLOSE: {
871
+ StreamContext* ctx = (StreamContext*)ev->release_ptr;
872
+ unregister_stream(ctx->token);
873
+ if (ev->release == RELEASE_STREAM) MsQuic->StreamClose(ctx->stream);
874
+ free(ctx);
875
+ break;
876
+ }
877
+ case RELEASE_CONNECTION: {
878
+ ConnectionContext* ctx = (ConnectionContext*)ev->release_ptr;
879
+ forget_connection_streams(ev->connection);
880
+ if (!NIL_P(ctx->client_obj)) rb_gc_unregister_address(&ctx->client_obj);
881
+ free(ctx->resumption_ticket);
882
+ free(ctx);
883
+ break;
884
+ }
885
+ }
886
+ }
246
887
 
247
- QUIC_CQE events[8];
248
- #if __linux__
249
- int count = epoll_wait(EventQ, events, 8, timeout_ms);
250
- #elif __APPLE__ || __FreeBSD__
251
- struct timespec ts;
252
- ts.tv_sec = timeout_ms / 1000;
253
- ts.tv_nsec = (timeout_ms % 1000) * 1000000;
254
- int count = kevent(EventQ, NULL, 0, events, 8, &ts);
255
- #endif
256
- for (int i = 0; i < count; i++) {
257
- #if __linux__
258
- if (events[i].data.ptr == NULL) {
259
- uint64_t val;
260
- read(WakeFd, &val, sizeof(val)); // drain eventfd
261
- continue;
888
+ // A handler that raised on RECEIVE has broken the stream's byte order for
889
+ // good, so the stream is aborted, as the callback used to do inline.
890
+ static void
891
+ abort_after_failed_receive(RubyEvent* ev)
892
+ {
893
+ if (strncmp(ev->event_type, "RECEIVE", 7) != 0) return;
894
+ uint64_t token = ev->token;
895
+ StreamContext* ctx = find_stream(token);
896
+ if (!ctx) return;
897
+ STATE_LOCK();
898
+ ctx->receive_delivery_failed = 1;
899
+ ctx->receive_credit = 0;
900
+ STATE_UNLOCK();
901
+ MsQuic->StreamShutdown(ctx->stream, ctx->abort_flags, 0);
902
+ wake_event_loop();
903
+ }
904
+
905
+ // Wait up to timeout_ms for events, then deliver up to max_events of them to
906
+ // Ruby. Returns the number delivered, or nil once the queue is closed and
907
+ // empty, which is the dispatcher's signal to exit.
908
+ static VALUE
909
+ quicsilver_dispatch_events(VALUE self, VALUE max_events, VALUE timeout_ms)
910
+ {
911
+ long max = NUM2LONG(max_events);
912
+ if (max < 1) max = 1;
913
+ if (max > DISPATCH_BATCH_MAX) max = DISPATCH_BATCH_MAX;
914
+ struct queue_wait_args wait = { NUM2INT(timeout_ms) };
915
+ rb_thread_call_without_gvl(queue_wait_nogvl, &wait, queue_wake_ubf, NULL);
916
+
917
+ RubyEvent batch[DISPATCH_BATCH_MAX];
918
+ long n = 0;
919
+ int closed;
920
+ pthread_mutex_lock(&EventQueue.lock);
921
+ closed = EventQueue.closed;
922
+ while (n < max && EventQueue.len > 0) {
923
+ RubyEvent* ev = &EventQueue.buf[EventQueue.head];
924
+ if (!ev->dropped && ev->connection_ctx && strcmp(ev->event_type, "DATAGRAM_RECEIVED") == 0) {
925
+ ((ConnectionContext*)ev->connection_ctx)->queued_datagrams--;
262
926
  }
263
- #elif __APPLE__ || __FreeBSD__
264
- if (events[i].filter == EVFILT_USER && events[i].ident == WAKE_IDENT) continue;
265
- #endif
266
- QUIC_SQE* sqe = cqe_get_sqe(&events[i]);
267
- if (sqe && sqe->Completion) {
268
- sqe->Completion(&events[i]);
927
+ batch[n] = *ev;
928
+ if (ev->buffers == ev->inline_buffers) batch[n].buffers = batch[n].inline_buffers;
929
+ n++;
930
+ EventQueue.head = (EventQueue.head + 1) % EventQueue.cap;
931
+ EventQueue.len--;
932
+ }
933
+ pthread_mutex_unlock(&EventQueue.lock);
934
+
935
+ for (long i = 0; i < n; i++) {
936
+ RubyEvent* ev = &batch[i];
937
+ if (ev->dropped) { free(ev->data); continue; }
938
+ const char* data = ev->data ? ev->data : ev->inline_data;
939
+ VALUE payload = Qundef;
940
+ if (ev->buffers) {
941
+ // Complete before the handler runs. A partial completion pauses
942
+ // the stream; a grant from the handler queues the resume; the
943
+ // pause must be queued first.
944
+ struct dispatch_ruby_args gather = { .buffers = ev->buffers,
945
+ .buffer_count = ev->buffer_count, .token = ev->token, .accepted = ev->accepted };
946
+ int gather_state = 0;
947
+ payload = rb_protect(gather_receive_protected, (VALUE)&gather, &gather_state);
948
+ if (gather_state) { rb_set_errinfo(Qnil); payload = Qundef; }
949
+ if (MsQuic) MsQuic->StreamReceiveComplete(ev->stream, ev->accepted);
950
+ if (ev->buffers != ev->inline_buffers) free(ev->buffers);
951
+ if (payload == Qundef) { abort_after_failed_receive(ev); perform_release(ev); continue; }
269
952
  }
953
+ int state = dispatch_to_ruby(ev->connection, ev->connection_ctx, ev->client_obj,
954
+ ev->event_type, ev->stream_id, data, ev->data_len, ev->early_data, payload);
955
+ if (state) abort_after_failed_receive(ev);
956
+ perform_release(ev);
957
+ free(ev->data);
270
958
  }
959
+
960
+ if (n == 0 && closed) return Qnil;
961
+ // The dispatcher does not yield here. Workers get the GVL when the
962
+ // server's admission queue is under pressure (Server#enqueue_work), which
963
+ // is the only reason to interrupt a batch; an unconditional yield per
964
+ // batch cost throughput and doubled tail latency for nothing.
965
+ return LONG2NUM(n);
966
+ }
967
+
968
+ // Datagrams dropped because the dispatcher was behind (DATAGRAM_QUEUE_LIMIT).
969
+ // A drop counter nobody can read is a drop debugged by guessing.
970
+ static VALUE
971
+ quicsilver_dropped_datagrams(VALUE self)
972
+ {
973
+ pthread_mutex_lock(&EventQueue.lock);
974
+ uint64_t dropped = DroppedDatagrams;
975
+ pthread_mutex_unlock(&EventQueue.lock);
976
+ return ULL2NUM(dropped);
271
977
  }
272
978
 
979
+
980
+ static VALUE
981
+ quicsilver_open_event_queue(VALUE self)
982
+ {
983
+ pthread_mutex_lock(&EventQueue.lock);
984
+ EventQueue.closed = 0;
985
+ pthread_mutex_unlock(&EventQueue.lock);
986
+ return Qnil;
987
+ }
988
+
989
+ // Stops the dispatcher once it has drained what is already queued. Call after
990
+ // the poll thread has stopped, so nothing is pushed afterwards.
991
+ static VALUE
992
+ quicsilver_close_event_queue(VALUE self)
993
+ {
994
+ pthread_mutex_lock(&EventQueue.lock);
995
+ EventQueue.closed = 1;
996
+ pthread_cond_broadcast(&EventQueue.ready);
997
+ pthread_mutex_unlock(&EventQueue.lock);
998
+ return Qnil;
999
+ }
1000
+
1001
+
273
1002
  QUIC_STATUS
274
1003
  StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
275
1004
  {
@@ -279,119 +1008,124 @@ StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
279
1008
  return QUIC_STATUS_SUCCESS;
280
1009
  }
281
1010
 
282
- // Apply pending priority on the event loop thread (safe context for SetParam)
283
- for (int i = 0; i < PendingPriorityCount; i++) {
284
- if (PendingPriorities[i].stream == Stream) {
285
- uint16_t priority = PendingPriorities[i].priority_plus_one - 1;
286
- // Remove by swapping with last
287
- PendingPriorities[i] = PendingPriorities[--PendingPriorityCount];
288
- MsQuic->SetParam(Stream, QUIC_PARAM_STREAM_PRIORITY, sizeof(priority), &priority);
289
- break;
290
- }
1011
+ uint64_t token = ctx->token;
1012
+
1013
+ // Lazily cache the QUIC stream ID on first callback.
1014
+ // Can't do this at StreamStart — MsQuic defers ID assignment with FLAG_NONE
1015
+ // until data is sent. By the first callback the ID is always assigned.
1016
+ if (ctx->stream_id == UINT64_MAX) {
1017
+ uint64_t id = UINT64_MAX;
1018
+ uint32_t id_len = sizeof(id);
1019
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &id_len, &id);
1020
+ STATE_LOCK();
1021
+ ctx->stream_id = id;
1022
+ STATE_UNLOCK();
1023
+ }
1024
+
1025
+ // MsQuic requires SetParam on its event thread.
1026
+ uint32_t pending = __atomic_exchange_n(&ctx->pending_priority, 0, __ATOMIC_ACQ_REL);
1027
+ if (pending) {
1028
+ uint16_t priority = (uint16_t)(pending - 1);
1029
+ MsQuic->SetParam(Stream, QUIC_PARAM_STREAM_PRIORITY, sizeof(priority), &priority);
291
1030
  }
292
1031
 
293
1032
  switch (Event->Type) {
294
1033
  case QUIC_STREAM_EVENT_RECEIVE: {
295
- int has_fin = (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) != 0;
296
-
297
1034
  // Track 0-RTT early data for replay protection
298
1035
  if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_0_RTT) {
299
1036
  ctx->early_data = 1;
300
1037
  }
301
1038
 
302
- if (Event->RECEIVE.BufferCount == 0 && has_fin) {
303
- // Empty FIN — headers-only request/response with no body
304
- uint64_t stream_id = 0;
305
- uint32_t stream_id_len = sizeof(stream_id);
306
- MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
307
-
308
- dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj,
309
- "RECEIVE_FIN", stream_id, (const char*)&Stream, sizeof(HQUIC), ctx->early_data);
310
- break;
311
- }
312
-
313
- if (Event->RECEIVE.BufferCount > 0) {
314
- const char* event_type = has_fin ? "RECEIVE_FIN" : "RECEIVE";
315
-
316
- uint64_t stream_id = 0;
317
- uint32_t stream_id_len = sizeof(stream_id);
318
- MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
319
-
320
- size_t total_data_len = 0;
321
- for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
322
- total_data_len += Event->RECEIVE.Buffers[b].Length;
323
- }
324
-
325
- if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) {
326
- // Create combined buffer: [stream_handle(8)][all data]
327
- size_t total_len = sizeof(HQUIC) + total_data_len;
328
- char* combined = (char*)malloc(total_len);
329
- if (combined != NULL) {
330
- memcpy(combined, &Stream, sizeof(HQUIC));
331
- size_t offset = sizeof(HQUIC);
332
- for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
333
- memcpy(combined + offset, Event->RECEIVE.Buffers[b].Buffer, Event->RECEIVE.Buffers[b].Length);
334
- offset += Event->RECEIVE.Buffers[b].Length;
335
- }
336
- dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_len, ctx->early_data);
337
- free(combined);
338
- }
339
- } else if (Event->RECEIVE.BufferCount == 1) {
340
- dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id,
341
- (const char*)Event->RECEIVE.Buffers[0].Buffer, Event->RECEIVE.Buffers[0].Length, 0);
342
- } else {
343
- char* combined = (char*)malloc(total_data_len);
344
- if (combined != NULL) {
345
- size_t offset = 0;
346
- for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
347
- memcpy(combined + offset, Event->RECEIVE.Buffers[b].Buffer, Event->RECEIVE.Buffers[b].Length);
348
- offset += Event->RECEIVE.Buffers[b].Length;
349
- }
350
- dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_data_len, 0);
351
- free(combined);
352
- }
353
- }
354
- }
355
- break;
1039
+ return receive_with_credit(ctx, Event);
356
1040
  }
357
1041
  case QUIC_STREAM_EVENT_SEND_COMPLETE:
358
- // Free the send buffer that was allocated in quicsilver_send_stream
359
- if (Event->SEND_COMPLETE.ClientContext != NULL) {
360
- free(Event->SEND_COMPLETE.ClientContext);
1042
+ // The retained String is dropped on the dispatcher, after Ruby has
1043
+ // seen the event; LiveSends is only ever touched under the GVL.
1044
+ if (!queue_event_full(ctx->connection, ctx->connection_ctx, ctx->client_obj,
1045
+ "SEND_COMPLETE", ctx->stream_id, (const char*)&token, sizeof(token), 0,
1046
+ NULL, RELEASE_SEND, Event->SEND_COMPLETE.ClientContext)) {
1047
+ // Out of memory. LiveSends is only touched under the GVL, so the
1048
+ // SendContext and the String it pins leak rather than race GC.
1049
+ fprintf(stderr, "Quicsilver: event queue allocation failed; leaking one send buffer\n");
361
1050
  }
362
1051
  break;
363
1052
  case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
364
- ctx->shutdown = 1;
365
- free(ctx);
1053
+ // Nothing is torn down here. The stream stays in LiveStreams, and
1054
+ // its handle stays open, until the dispatcher has delivered this
1055
+ // event: Ruby may still be handling an earlier RECEIVE for it (a
1056
+ // uni stream with FIN completes natively before Ruby sees its
1057
+ // data) and must be able to address it. MsQuic returns an error
1058
+ // for operations on a shut-down handle, so that is safe. The
1059
+ // dispatcher unregisters, closes and frees, holding the GVL, which
1060
+ // is what any Ruby thread that found ctx also holds.
1061
+ __atomic_store_n(&ctx->shutdown, 1, __ATOMIC_RELEASE);
366
1062
  MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, NULL);
367
- if (Event->SHUTDOWN_COMPLETE.AppCloseInProgress == FALSE) {
368
- MsQuic->StreamClose(Stream);
1063
+ if (!queue_event_full(ctx->connection, ctx->connection_ctx, ctx->client_obj,
1064
+ "STREAM_SHUTDOWN_COMPLETE", ctx->stream_id, (const char*)&token, sizeof(token), 0,
1065
+ NULL, Event->SHUTDOWN_COMPLETE.AppCloseInProgress ? RELEASE_STREAM_NOCLOSE : RELEASE_STREAM, ctx)) {
1066
+ // Out of memory. The context is not freed here: contexts are
1067
+ // freed only on a thread holding the GVL, so that a Ruby thread
1068
+ // between find_stream and its MsQuic call cannot have the
1069
+ // pointer freed underneath it, and this thread never holds the
1070
+ // GVL. Unregistering only stops future lookups. One context and
1071
+ // one handle leak on an allocation failure that is already a
1072
+ // terminal condition; the invariant stays whole.
1073
+ fprintf(stderr, "Quicsilver: event queue allocation failed; leaking stream %llu\n",
1074
+ (unsigned long long)ctx->stream_id);
1075
+ unregister_stream(token);
369
1076
  }
370
1077
  break;
1078
+ case QUIC_STREAM_EVENT_START_COMPLETE: {
1079
+ // Pack [stream_handle(8)][peer_accepted(1)] for Ruby
1080
+ uint8_t accepted = Event->START_COMPLETE.PeerAccepted ? 1 : 0;
1081
+ char payload[sizeof(token) + 1];
1082
+ memcpy(payload, &token, sizeof(token));
1083
+ payload[sizeof(token)] = accepted;
1084
+ queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj,
1085
+ "STREAM_START_COMPLETE", ctx->stream_id, payload, sizeof(payload), 0);
1086
+ break;
1087
+ }
1088
+ case QUIC_STREAM_EVENT_PEER_ACCEPTED:
1089
+ // Queued stream now accepted — peer raised MAX_STREAMS
1090
+ queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj,
1091
+ "STREAM_PEER_ACCEPTED", ctx->stream_id, (const char*)&token, sizeof(token), 0);
1092
+ break;
371
1093
  case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
1094
+ // If the FIN arrived while a receive was pending, MsQuic reports it
1095
+ // only here, never as a zero-length RECEIVE. Ruby expects RECEIVE_FIN.
1096
+ if (!ctx->fin_delivered && !ctx->receive_delivery_failed) {
1097
+ ctx->fin_delivered = 1;
1098
+ queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj,
1099
+ "RECEIVE_FIN", ctx->stream_id, (const char*)&token, sizeof(token), ctx->early_data);
1100
+ }
372
1101
  break;
373
1102
  case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: {
374
- // Peer sent RESET_STREAM — pack [stream_handle(8)][error_code(8)]
375
- uint64_t stream_id = 0;
376
- uint32_t stream_id_len = sizeof(stream_id);
377
- MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
1103
+ // Peer sent RESET_STREAM — pack
1104
+ // [stream_handle(8)][error_code(8)][final_size(8)]
1105
+ //
1106
+ // RESET_STREAM carries the stream's final size, and WebTransport
1107
+ // session accounting needs it: without it the two endpoints
1108
+ // disagree about how much credit a reset stream consumed
1109
+ // (draft-ietf-webtrans-http3-16 5.4, RFC 9000 4.5).
378
1110
  uint64_t error_code = Event->PEER_SEND_ABORTED.ErrorCode;
379
- char combined[sizeof(HQUIC) + sizeof(uint64_t)];
380
- memcpy(combined, &Stream, sizeof(HQUIC));
381
- memcpy(combined + sizeof(HQUIC), &error_code, sizeof(uint64_t));
382
- dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STREAM_RESET", stream_id, combined, sizeof(combined), 0);
1111
+ // Read straight off the event: calling back into the MsQuic API
1112
+ // from inside a callback risks re-entering locks the callback
1113
+ // already holds.
1114
+ uint64_t final_size = Event->PEER_SEND_ABORTED.FinalSize;
1115
+ char combined[sizeof(token) + sizeof(uint64_t) + sizeof(uint64_t)];
1116
+ memcpy(combined, &token, sizeof(token));
1117
+ memcpy(combined + sizeof(token), &error_code, sizeof(uint64_t));
1118
+ memcpy(combined + sizeof(token) + sizeof(uint64_t), &final_size, sizeof(uint64_t));
1119
+ queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STREAM_RESET", ctx->stream_id, combined, sizeof(combined), 0);
383
1120
  break;
384
1121
  }
385
1122
  case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: {
386
1123
  // Peer sent STOP_SENDING — pack [stream_handle(8)][error_code(8)]
387
- uint64_t stream_id = 0;
388
- uint32_t stream_id_len = sizeof(stream_id);
389
- MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
390
1124
  uint64_t error_code = Event->PEER_RECEIVE_ABORTED.ErrorCode;
391
- char combined[sizeof(HQUIC) + sizeof(uint64_t)];
392
- memcpy(combined, &Stream, sizeof(HQUIC));
393
- memcpy(combined + sizeof(HQUIC), &error_code, sizeof(uint64_t));
394
- dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STOP_SENDING", stream_id, combined, sizeof(combined), 0);
1125
+ char combined[sizeof(token) + sizeof(uint64_t)];
1126
+ memcpy(combined, &token, sizeof(token));
1127
+ memcpy(combined + sizeof(token), &error_code, sizeof(uint64_t));
1128
+ queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STOP_SENDING", ctx->stream_id, combined, sizeof(combined), 0);
395
1129
  break;
396
1130
  }
397
1131
  }
@@ -412,11 +1146,30 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
412
1146
  }
413
1147
 
414
1148
  switch (Event->Type) {
1149
+ case QUIC_CONNECTION_EVENT_RELIABLE_RESET_NEGOTIATED:
1150
+ ctx->reliable_reset_negotiated = Event->RELIABLE_RESET_NEGOTIATED.IsNegotiated;
1151
+ break;
415
1152
  case QUIC_CONNECTION_EVENT_CONNECTED:
416
1153
  ctx->connected = 1;
417
1154
  ctx->failed = 0;
1155
+ ctx->session_resumed = Event->CONNECTED.SessionResumed;
1156
+ // Server: send resumption ticket so client can do 0-RTT on reconnect
1157
+ if (NIL_P(ctx->client_obj)) {
1158
+ MsQuic->ConnectionSendResumptionTicket(Connection, QUIC_SEND_RESUMPTION_FLAG_NONE, 0, NULL);
1159
+ }
418
1160
  // Notify Ruby about new connection - pass ctx pointer for building connection_data
419
- dispatch_to_ruby(Connection, ctx, ctx->client_obj, "CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
1161
+ queue_event(Connection, ctx, ctx->client_obj, "CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
1162
+ break;
1163
+ case QUIC_CONNECTION_EVENT_RESUMPTION_TICKET_RECEIVED:
1164
+ // Client-only: store the ticket for future 0-RTT reconnection
1165
+ if (ctx->resumption_ticket) {
1166
+ free(ctx->resumption_ticket);
1167
+ }
1168
+ ctx->resumption_ticket_length = Event->RESUMPTION_TICKET_RECEIVED.ResumptionTicketLength;
1169
+ ctx->resumption_ticket = (uint8_t*)malloc(ctx->resumption_ticket_length);
1170
+ if (ctx->resumption_ticket) {
1171
+ memcpy(ctx->resumption_ticket, Event->RESUMPTION_TICKET_RECEIVED.ResumptionTicket, ctx->resumption_ticket_length);
1172
+ }
420
1173
  break;
421
1174
  case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
422
1175
  ctx->connected = 0;
@@ -432,13 +1185,17 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
432
1185
  break;
433
1186
  case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
434
1187
  ctx->connected = 0;
435
- dispatch_to_ruby(Connection, ctx, ctx->client_obj, "CONNECTION_CLOSED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
436
- // Free context for all connections (both client and server).
437
- // Client GC registration must be removed before freeing.
438
- if (!NIL_P(ctx->client_obj)) {
439
- rb_gc_unregister_address(&ctx->client_obj);
1188
+ // Freed on the dispatcher after Ruby has seen CONNECTION_CLOSED:
1189
+ // rb_gc_unregister_address needs the GVL, and Ruby may still hold
1190
+ // the context pointer as a handle until it processes this event.
1191
+ if (!queue_event_full(Connection, ctx, ctx->client_obj, "CONNECTION_CLOSED", 0,
1192
+ (const char*)&Connection, sizeof(HQUIC), 0, NULL, RELEASE_CONNECTION, ctx)) {
1193
+ // Out of memory, and the release needs the GVL
1194
+ // (rb_gc_unregister_address), which this thread never holds.
1195
+ // Leaking one small context is the safe failure: freeing it
1196
+ // would leave a GC root pointing at freed memory.
1197
+ fprintf(stderr, "Quicsilver: event queue allocation failed; leaking connection context\n");
440
1198
  }
441
- free(ctx);
442
1199
  break;
443
1200
  case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
444
1201
  // Client opened a stream
@@ -450,15 +1207,40 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
450
1207
  stream_ctx->client_obj = ctx->client_obj; // Copy from connection context
451
1208
  stream_ctx->started = 1;
452
1209
  stream_ctx->shutdown = 0;
1210
+ stream_ctx->stream_id = UINT64_MAX; // Lazily resolved on first callback
453
1211
  stream_ctx->early_data = 0;
454
1212
  stream_ctx->error_status = QUIC_STATUS_SUCCESS;
455
1213
 
1214
+ register_stream(stream_ctx, Stream,
1215
+ Event->PEER_STREAM_STARTED.Flags & QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL
1216
+ ? QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE
1217
+ : QUIC_STREAM_SHUTDOWN_FLAG_ABORT);
1218
+
456
1219
  // Set the stream callback handler to handle data events
457
1220
  MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, stream_ctx);
458
1221
  } else {
459
1222
  MsQuic->StreamClose(Stream);
460
1223
  }
461
1224
  break;
1225
+ case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED:
1226
+ // Bounded by DATAGRAM_QUEUE_LIMIT inside queue_event_full; past it
1227
+ // the datagram is dropped and counted. Datagrams are best-effort by
1228
+ // definition (RFC 9221), so that is a legitimate response to a
1229
+ // dispatcher that has fallen behind.
1230
+ queue_event(Connection, ctx, ctx->client_obj, "DATAGRAM_RECEIVED", 0,
1231
+ (const char*)Event->DATAGRAM_RECEIVED.Buffer->Buffer,
1232
+ Event->DATAGRAM_RECEIVED.Buffer->Length, 0);
1233
+ break;
1234
+ case QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED:
1235
+ break;
1236
+ case QUIC_CONNECTION_EVENT_DATAGRAM_SEND_STATE_CHANGED:
1237
+ // Free the send buffer when the datagram reaches a final state
1238
+ if (QUIC_DATAGRAM_SEND_STATE_IS_FINAL(Event->DATAGRAM_SEND_STATE_CHANGED.State)) {
1239
+ if (Event->DATAGRAM_SEND_STATE_CHANGED.ClientContext != NULL) {
1240
+ free(Event->DATAGRAM_SEND_STATE_CHANGED.ClientContext);
1241
+ }
1242
+ }
1243
+ break;
462
1244
  default:
463
1245
  break;
464
1246
  }
@@ -487,6 +1269,14 @@ ListenerCallback(HQUIC Listener, void* Context, QUIC_LISTENER_EVENT* Event)
487
1269
  conn_ctx->error_status = QUIC_STATUS_SUCCESS;
488
1270
  conn_ctx->error_code = 0;
489
1271
  conn_ctx->client_obj = Qnil; // Server connections have no client object
1272
+ conn_ctx->remote_address[0] = '\0';
1273
+ conn_ctx->remote_port = 0;
1274
+ conn_ctx->session_resumed = 0;
1275
+ conn_ctx->reliable_reset_negotiated = 0;
1276
+ conn_ctx->resumption_ticket = NULL;
1277
+ conn_ctx->queued_datagrams = 0;
1278
+ conn_ctx->dropped_datagrams = 0;
1279
+ conn_ctx->resumption_ticket_length = 0;
490
1280
 
491
1281
  // Set the connection callback
492
1282
  MsQuic->SetCallbackHandler(Event->NEW_CONNECTION.Connection, (void*)ConnectionCallback, conn_ctx);
@@ -522,12 +1312,12 @@ quicsilver_open(VALUE self)
522
1312
  QUIC_STATUS Status;
523
1313
 
524
1314
  // Check if already initialized
525
- if (MsQuic != NULL) {
1315
+ if (MsQuic != NULL && Registration != NULL) {
526
1316
  return Qtrue;
527
1317
  }
528
1318
 
529
1319
  // Open a handle to the library and get the API function table
530
- if (QUIC_FAILED(Status = MsQuicOpenVersion(2, (const void**)&MsQuic))) {
1320
+ if (MsQuic == NULL && QUIC_FAILED(Status = MsQuicOpenVersion(2, (const void**)&MsQuic))) {
531
1321
  rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
532
1322
  return Qfalse;
533
1323
  }
@@ -547,10 +1337,28 @@ quicsilver_open(VALUE self)
547
1337
  return Qfalse;
548
1338
  }
549
1339
 
1340
+ // PollingIdleTimeoutUs: how long MsQuic keeps spinning on an idle
1341
+ // execution context before it lets us sleep in kevent/epoll_wait. At zero
1342
+ // every idle gap is a full sleep-and-wake, which is the fixed latency floor
1343
+ // at low concurrency. A short spin trades CPU for that latency.
1344
+ // QUICSILVER_POLL_IDLE_US overrides for measurement; the default stays 0
1345
+ // until the trade is measured.
1346
+ uint32_t polling_idle_us = 0;
1347
+ const char* idle_env = getenv("QUICSILVER_POLL_IDLE_US");
1348
+ if (idle_env && *idle_env) polling_idle_us = (uint32_t)strtoul(idle_env, NULL, 10);
1349
+
1350
+ // One execution context. The receive path's correctness argument depends
1351
+ // on it: receive_with_credit sets receive_paused and returns a partial
1352
+ // length, and a grant from a Ruby thread calls StreamReceiveSetEnabled,
1353
+ // which MsQuic applies as a queued operation on the connection's worker.
1354
+ // With one context that worker is this ExecutionPoll, so the resume
1355
+ // always runs after the callback that paused. With more than one context
1356
+ // a connection still has exactly one worker, so the argument holds per
1357
+ // connection, but revisit it before changing this.
550
1358
  QUIC_EXECUTION_CONFIG exec_config = { 0, &EventQ };
551
1359
  Status = MsQuic->ExecutionCreate(
552
1360
  QUIC_GLOBAL_EXECUTION_CONFIG_FLAG_NONE,
553
- 0, // PollingIdleTimeoutUs
1361
+ polling_idle_us,
554
1362
  1, // 1 execution context
555
1363
  &exec_config,
556
1364
  &ExecContext
@@ -597,8 +1405,15 @@ quicsilver_open(VALUE self)
597
1405
 
598
1406
  // Create a QUIC configuration (for client connections)
599
1407
  static VALUE
600
- quicsilver_create_configuration(VALUE self, VALUE unsecure)
1408
+ quicsilver_create_configuration(int argc, VALUE *argv, VALUE self)
601
1409
  {
1410
+ VALUE unsecure, datagram_receive_enabled, reliable_reset_enabled, incoming_bidi, incoming_uni,
1411
+ idle_timeout_ms;
1412
+ rb_scan_args(argc, argv, "15", &unsecure, &datagram_receive_enabled,
1413
+ &reliable_reset_enabled, &incoming_bidi, &incoming_uni, &idle_timeout_ms);
1414
+ if (argc < 2) datagram_receive_enabled = Qtrue;
1415
+ if (argc < 3) reliable_reset_enabled = Qtrue;
1416
+
602
1417
  if (MsQuic == NULL) {
603
1418
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
604
1419
  return Qnil;
@@ -609,8 +1424,30 @@ quicsilver_create_configuration(VALUE self, VALUE unsecure)
609
1424
 
610
1425
  // Basic settings
611
1426
  QUIC_SETTINGS Settings = {0};
612
- Settings.IdleTimeoutMs = 10000; // 10 second idle timeout to match server
613
- Settings.IsSet.IdleTimeoutMs = TRUE;
1427
+ Settings.ReliableResetEnabled = RTEST(reliable_reset_enabled);
1428
+ Settings.IsSet.ReliableResetEnabled = TRUE;
1429
+ // The peer advertises its own idle timeout and the lower of the two applies.
1430
+ if (!NIL_P(idle_timeout_ms)) {
1431
+ Settings.IdleTimeoutMs = NUM2ULL(idle_timeout_ms);
1432
+ Settings.IsSet.IdleTimeoutMs = TRUE;
1433
+ }
1434
+ Settings.DatagramReceiveEnabled = RTEST(datagram_receive_enabled);
1435
+ Settings.IsSet.DatagramReceiveEnabled = TRUE;
1436
+
1437
+ // Optional credit for server-initiated streams. Leave native defaults alone
1438
+ // when omitted; the uni limit also covers HTTP/3 control and QPACK streams.
1439
+ if (!NIL_P(incoming_bidi)) {
1440
+ unsigned int count = NUM2UINT(incoming_bidi);
1441
+ if (count > UINT16_MAX) rb_raise(rb_eArgError, "max_incoming_bidi_streams must be between 0 and 65535");
1442
+ Settings.PeerBidiStreamCount = (uint16_t)count;
1443
+ Settings.IsSet.PeerBidiStreamCount = TRUE;
1444
+ }
1445
+ if (!NIL_P(incoming_uni)) {
1446
+ unsigned int count = NUM2UINT(incoming_uni);
1447
+ if (count > UINT16_MAX) rb_raise(rb_eArgError, "max_incoming_uni_streams must be between 0 and 65535");
1448
+ Settings.PeerUnidiStreamCount = (uint16_t)count;
1449
+ Settings.IsSet.PeerUnidiStreamCount = TRUE;
1450
+ }
614
1451
 
615
1452
  // Simple ALPN for now - Ruby can customize this later
616
1453
  QUIC_BUFFER Alpn = { sizeof("h3") - 1, (uint8_t*)"h3" };
@@ -694,6 +1531,8 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
694
1531
  uint64_t handshake_idle_timeout_ms = NUM2ULL(handshake_idle_timeout_ms_val);
695
1532
 
696
1533
  QUIC_SETTINGS Settings = {0};
1534
+ Settings.ReliableResetEnabled = TRUE;
1535
+ Settings.IsSet.ReliableResetEnabled = TRUE;
697
1536
  Settings.IdleTimeoutMs = idle_timeout_ms;
698
1537
  Settings.IsSet.IdleTimeoutMs = TRUE;
699
1538
  Settings.ServerResumptionLevel = server_resumption_level;
@@ -710,6 +1549,8 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
710
1549
  Settings.IsSet.StreamRecvBufferDefault = TRUE;
711
1550
  Settings.ConnFlowControlWindow = connection_flow_control_window;
712
1551
  Settings.IsSet.ConnFlowControlWindow = TRUE;
1552
+ Settings.DatagramReceiveEnabled = TRUE;
1553
+ Settings.IsSet.DatagramReceiveEnabled = TRUE;
713
1554
 
714
1555
  // Throughput settings
715
1556
  Settings.PacingEnabled = pacing_enabled;
@@ -752,7 +1593,7 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
752
1593
 
753
1594
  CredConfig.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE;
754
1595
  CredConfig.CertificateFile = &CertFile;
755
- CredConfig.Flags = QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION;
1596
+ CredConfig.Flags = QUIC_CREDENTIAL_FLAG_NONE;
756
1597
 
757
1598
  if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
758
1599
  MsQuic->ConfigurationClose(Configuration);
@@ -788,6 +1629,14 @@ quicsilver_create_connection(VALUE self, VALUE client_obj)
788
1629
  ctx->error_status = QUIC_STATUS_SUCCESS;
789
1630
  ctx->error_code = 0;
790
1631
  ctx->client_obj = client_obj; // Store Ruby client object (Qnil for server)
1632
+ ctx->remote_address[0] = '\0';
1633
+ ctx->remote_port = 0;
1634
+ ctx->session_resumed = 0;
1635
+ ctx->reliable_reset_negotiated = 0;
1636
+ ctx->resumption_ticket = NULL;
1637
+ ctx->queued_datagrams = 0;
1638
+ ctx->dropped_datagrams = 0;
1639
+ ctx->resumption_ticket_length = 0;
791
1640
 
792
1641
  // Protect from GC if it's a Ruby object
793
1642
  if (!NIL_P(client_obj)) {
@@ -835,17 +1684,28 @@ quicsilver_start_connection(VALUE self, VALUE connection_handle, VALUE config_ha
835
1684
  return Qtrue;
836
1685
  }
837
1686
 
838
- // Wait for connection to complete (connected or failed)
1687
+ // Sleep without GVL so other Ruby threads can run.
1688
+ struct sleep_args { int ms; };
1689
+ static void* sleep_nogvl(void* arg) {
1690
+ struct sleep_args* a = (struct sleep_args*)arg;
1691
+ struct timespec ts = { .tv_sec = a->ms / 1000, .tv_nsec = (a->ms % 1000) * 1000000 };
1692
+ nanosleep(&ts, NULL);
1693
+ return NULL;
1694
+ }
1695
+
1696
+ // Wait for connection to complete (connected or failed).
1697
+ // Waits only; the poll thread is the sole driver of MsQuic.
839
1698
  static VALUE
840
1699
  quicsilver_wait_for_connection(VALUE self, VALUE context_handle, VALUE timeout_ms)
841
1700
  {
842
1701
  ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
843
1702
  int timeout = NUM2INT(timeout_ms);
844
1703
  int elapsed = 0;
845
- const int sleep_interval = 10; // 10ms
846
-
1704
+ const int sleep_interval = 1; // 1ms
1705
+
847
1706
  while (elapsed < timeout && !ctx->connected && !ctx->failed) {
848
- poll_inline(sleep_interval); // Drive MsQuic execution while waiting
1707
+ struct sleep_args sa = { .ms = sleep_interval };
1708
+ rb_thread_call_without_gvl(sleep_nogvl, &sa, RUBY_UBF_IO, NULL);
849
1709
  elapsed += sleep_interval;
850
1710
  }
851
1711
 
@@ -883,6 +1743,316 @@ quicsilver_connection_status(VALUE self, VALUE context_handle)
883
1743
  return status;
884
1744
  }
885
1745
 
1746
+ // Get QUIC connection statistics (QUIC_STATISTICS_V2)
1747
+ static VALUE
1748
+ quicsilver_connection_statistics(VALUE self, VALUE connection_handle_val)
1749
+ {
1750
+ if (MsQuic == NULL) {
1751
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1752
+ return Qnil;
1753
+ }
1754
+
1755
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
1756
+ if (Connection == NULL) return Qnil;
1757
+
1758
+ QUIC_STATISTICS_V2 stats;
1759
+ uint32_t stats_size = sizeof(stats);
1760
+ memset(&stats, 0, stats_size);
1761
+
1762
+ QUIC_STATUS status = MsQuic->GetParam(
1763
+ Connection,
1764
+ QUIC_PARAM_CONN_STATISTICS_V2,
1765
+ &stats_size,
1766
+ &stats);
1767
+
1768
+ if (QUIC_FAILED(status)) {
1769
+ return Qnil;
1770
+ }
1771
+
1772
+ VALUE result = rb_hash_new();
1773
+
1774
+ // RTT (microseconds)
1775
+ rb_hash_aset(result, rb_str_new_cstr("rtt"), UINT2NUM(stats.Rtt));
1776
+ rb_hash_aset(result, rb_str_new_cstr("min_rtt"), UINT2NUM(stats.MinRtt));
1777
+ rb_hash_aset(result, rb_str_new_cstr("max_rtt"), UINT2NUM(stats.MaxRtt));
1778
+
1779
+ // Handshake
1780
+ rb_hash_aset(result, rb_str_new_cstr("resumption_attempted"), stats.ResumptionAttempted ? Qtrue : Qfalse);
1781
+ rb_hash_aset(result, rb_str_new_cstr("resumption_succeeded"), stats.ResumptionSucceeded ? Qtrue : Qfalse);
1782
+
1783
+ // Send
1784
+ rb_hash_aset(result, rb_str_new_cstr("send_path_mtu"), UINT2NUM(stats.SendPathMtu));
1785
+ rb_hash_aset(result, rb_str_new_cstr("send_total_packets"), ULL2NUM(stats.SendTotalPackets));
1786
+ rb_hash_aset(result, rb_str_new_cstr("send_retransmittable_packets"), ULL2NUM(stats.SendRetransmittablePackets));
1787
+ rb_hash_aset(result, rb_str_new_cstr("send_suspected_lost_packets"), ULL2NUM(stats.SendSuspectedLostPackets));
1788
+ rb_hash_aset(result, rb_str_new_cstr("send_spurious_lost_packets"), ULL2NUM(stats.SendSpuriousLostPackets));
1789
+ rb_hash_aset(result, rb_str_new_cstr("send_total_bytes"), ULL2NUM(stats.SendTotalBytes));
1790
+ rb_hash_aset(result, rb_str_new_cstr("send_total_stream_bytes"), ULL2NUM(stats.SendTotalStreamBytes));
1791
+ rb_hash_aset(result, rb_str_new_cstr("send_congestion_count"), UINT2NUM(stats.SendCongestionCount));
1792
+ rb_hash_aset(result, rb_str_new_cstr("send_persistent_congestion_count"), UINT2NUM(stats.SendPersistentCongestionCount));
1793
+ rb_hash_aset(result, rb_str_new_cstr("send_congestion_window"), UINT2NUM(stats.SendCongestionWindow));
1794
+
1795
+ // Recv
1796
+ rb_hash_aset(result, rb_str_new_cstr("recv_total_packets"), ULL2NUM(stats.RecvTotalPackets));
1797
+ rb_hash_aset(result, rb_str_new_cstr("recv_reordered_packets"), ULL2NUM(stats.RecvReorderedPackets));
1798
+ rb_hash_aset(result, rb_str_new_cstr("recv_dropped_packets"), ULL2NUM(stats.RecvDroppedPackets));
1799
+ rb_hash_aset(result, rb_str_new_cstr("recv_duplicate_packets"), ULL2NUM(stats.RecvDuplicatePackets));
1800
+ rb_hash_aset(result, rb_str_new_cstr("recv_total_bytes"), ULL2NUM(stats.RecvTotalBytes));
1801
+ rb_hash_aset(result, rb_str_new_cstr("recv_total_stream_bytes"), ULL2NUM(stats.RecvTotalStreamBytes));
1802
+ rb_hash_aset(result, rb_str_new_cstr("recv_decryption_failures"), ULL2NUM(stats.RecvDecryptionFailures));
1803
+ rb_hash_aset(result, rb_str_new_cstr("recv_valid_ack_frames"), ULL2NUM(stats.RecvValidAckFrames));
1804
+
1805
+ // Misc
1806
+ rb_hash_aset(result, rb_str_new_cstr("key_update_count"), UINT2NUM(stats.KeyUpdateCount));
1807
+
1808
+ return result;
1809
+ }
1810
+
1811
+ // Get global QUIC transport counters.
1812
+ //
1813
+ // These counters are process-wide transport state, not scoped to a single
1814
+ // Quicsilver::Server or QUIC connection. They complement QUIC_STATISTICS_V2,
1815
+ // which is exposed by quicsilver_connection_statistics for a specific
1816
+ // connection handle.
1817
+ //
1818
+ // MsQuic stores the values as a uint64_t array indexed by QUIC_PERF_COUNTER_*.
1819
+ // The public Ruby API uses transport-domain names instead of MsQuic enum names
1820
+ // so Server#stats does not leak the native implementation detail.
1821
+ static VALUE
1822
+ quicsilver_transport_counters(VALUE self)
1823
+ {
1824
+ if (MsQuic == NULL) {
1825
+ rb_raise(rb_eRuntimeError, "QUIC transport not initialized.");
1826
+ return Qnil;
1827
+ }
1828
+
1829
+ #ifdef QUIC_PARAM_GLOBAL_PERF_COUNTERS
1830
+ uint64_t counters[QUIC_PERF_COUNTER_MAX] = {0};
1831
+ uint32_t counters_size = sizeof(counters);
1832
+
1833
+ QUIC_STATUS status = MsQuic->GetParam(
1834
+ NULL,
1835
+ QUIC_PARAM_GLOBAL_PERF_COUNTERS,
1836
+ &counters_size,
1837
+ counters);
1838
+
1839
+ if (QUIC_FAILED(status)) {
1840
+ return Qnil;
1841
+ }
1842
+
1843
+ VALUE result = rb_hash_new();
1844
+
1845
+ #define ADD_COUNTER(key, counter) \
1846
+ rb_hash_aset(result, rb_str_new_cstr(key), ULL2NUM(counters[counter]))
1847
+
1848
+ // Connection lifecycle counters.
1849
+ ADD_COUNTER("connections_created", QUIC_PERF_COUNTER_CONN_CREATED);
1850
+ ADD_COUNTER("connections_handshake_failed", QUIC_PERF_COUNTER_CONN_HANDSHAKE_FAIL);
1851
+ ADD_COUNTER("connections_app_rejected", QUIC_PERF_COUNTER_CONN_APP_REJECT);
1852
+ ADD_COUNTER("connections_resumed", QUIC_PERF_COUNTER_CONN_RESUMED);
1853
+ ADD_COUNTER("connections_active", QUIC_PERF_COUNTER_CONN_ACTIVE);
1854
+ ADD_COUNTER("connections_connected", QUIC_PERF_COUNTER_CONN_CONNECTED);
1855
+ ADD_COUNTER("connections_protocol_errors", QUIC_PERF_COUNTER_CONN_PROTOCOL_ERRORS);
1856
+ ADD_COUNTER("connections_no_alpn", QUIC_PERF_COUNTER_CONN_NO_ALPN);
1857
+ ADD_COUNTER("connections_load_rejected", QUIC_PERF_COUNTER_CONN_LOAD_REJECT);
1858
+
1859
+ // Stream, packet, UDP, and application byte counters.
1860
+ ADD_COUNTER("streams_active", QUIC_PERF_COUNTER_STRM_ACTIVE);
1861
+ ADD_COUNTER("packets_suspected_lost", QUIC_PERF_COUNTER_PKTS_SUSPECTED_LOST);
1862
+ ADD_COUNTER("packets_dropped", QUIC_PERF_COUNTER_PKTS_DROPPED);
1863
+ ADD_COUNTER("packets_decryption_failed", QUIC_PERF_COUNTER_PKTS_DECRYPTION_FAIL);
1864
+ ADD_COUNTER("udp_datagrams_received", QUIC_PERF_COUNTER_UDP_RECV);
1865
+ ADD_COUNTER("udp_datagrams_sent", QUIC_PERF_COUNTER_UDP_SEND);
1866
+ ADD_COUNTER("udp_bytes_received", QUIC_PERF_COUNTER_UDP_RECV_BYTES);
1867
+ ADD_COUNTER("udp_bytes_sent", QUIC_PERF_COUNTER_UDP_SEND_BYTES);
1868
+ ADD_COUNTER("udp_receive_events", QUIC_PERF_COUNTER_UDP_RECV_EVENTS);
1869
+ ADD_COUNTER("udp_send_calls", QUIC_PERF_COUNTER_UDP_SEND_CALLS);
1870
+ ADD_COUNTER("app_bytes_sent", QUIC_PERF_COUNTER_APP_SEND_BYTES);
1871
+ ADD_COUNTER("app_bytes_received", QUIC_PERF_COUNTER_APP_RECV_BYTES);
1872
+
1873
+ // Queue/worker pressure counters. These are useful for distinguishing
1874
+ // transport saturation from Ruby scheduler saturation in Server#stats.
1875
+ ADD_COUNTER("connection_queue_depth", QUIC_PERF_COUNTER_CONN_QUEUE_DEPTH);
1876
+ ADD_COUNTER("connection_operations_queue_depth", QUIC_PERF_COUNTER_CONN_OPER_QUEUE_DEPTH);
1877
+ ADD_COUNTER("connection_operations_queued", QUIC_PERF_COUNTER_CONN_OPER_QUEUED);
1878
+ ADD_COUNTER("connection_operations_completed", QUIC_PERF_COUNTER_CONN_OPER_COMPLETED);
1879
+ ADD_COUNTER("worker_operations_queue_depth", QUIC_PERF_COUNTER_WORK_OPER_QUEUE_DEPTH);
1880
+ ADD_COUNTER("worker_operations_queued", QUIC_PERF_COUNTER_WORK_OPER_QUEUED);
1881
+ ADD_COUNTER("worker_operations_completed", QUIC_PERF_COUNTER_WORK_OPER_COMPLETED);
1882
+
1883
+ // Path validation and stateless response counters.
1884
+ ADD_COUNTER("paths_validated", QUIC_PERF_COUNTER_PATH_VALIDATED);
1885
+ ADD_COUNTER("paths_failed", QUIC_PERF_COUNTER_PATH_FAILURE);
1886
+ ADD_COUNTER("stateless_resets_sent", QUIC_PERF_COUNTER_SEND_STATELESS_RESET);
1887
+ ADD_COUNTER("stateless_retries_sent", QUIC_PERF_COUNTER_SEND_STATELESS_RETRY);
1888
+
1889
+ #undef ADD_COUNTER
1890
+
1891
+ return result;
1892
+ #else
1893
+ return Qnil;
1894
+ #endif
1895
+ }
1896
+
1897
+ // Get the remote address of a QUIC connection. Called lazily from Ruby
1898
+ // so we don't block MsQuic's event thread during the CONNECTED callback.
1899
+ // Get the resumption ticket from a connection context (for 0-RTT)
1900
+ static VALUE
1901
+ quicsilver_get_resumption_ticket(VALUE self, VALUE context_handle_val)
1902
+ {
1903
+ ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle_val);
1904
+ if (ctx == NULL || ctx->resumption_ticket == NULL || ctx->resumption_ticket_length == 0) {
1905
+ return Qnil;
1906
+ }
1907
+ return rb_str_new((const char*)ctx->resumption_ticket, ctx->resumption_ticket_length);
1908
+ }
1909
+
1910
+ // Set a resumption ticket on a connection (for 0-RTT reconnection)
1911
+ static VALUE
1912
+ quicsilver_set_resumption_ticket(VALUE self, VALUE connection_handle_val, VALUE ticket_val)
1913
+ {
1914
+ if (MsQuic == NULL) return Qfalse;
1915
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
1916
+ if (Connection == NULL || NIL_P(ticket_val)) return Qfalse;
1917
+
1918
+ const uint8_t* ticket = (const uint8_t*)RSTRING_PTR(ticket_val);
1919
+ uint32_t ticket_len = (uint32_t)RSTRING_LEN(ticket_val);
1920
+
1921
+ QUIC_STATUS Status = MsQuic->SetParam(
1922
+ Connection,
1923
+ QUIC_PARAM_CONN_RESUMPTION_TICKET,
1924
+ ticket_len,
1925
+ ticket);
1926
+
1927
+ return QUIC_SUCCEEDED(Status) ? Qtrue : Qfalse;
1928
+ }
1929
+
1930
+ static VALUE
1931
+ connection_param_bytes(VALUE connection_handle_val, uint32_t param)
1932
+ {
1933
+ if (MsQuic == NULL) return Qnil;
1934
+
1935
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
1936
+ if (Connection == NULL) return Qnil;
1937
+
1938
+ uint8_t buffer[256];
1939
+ uint32_t buffer_size = sizeof(buffer);
1940
+ memset(buffer, 0, sizeof(buffer));
1941
+
1942
+ QUIC_STATUS status = MsQuic->GetParam(Connection, param, &buffer_size, buffer);
1943
+ if (QUIC_FAILED(status) || buffer_size == 0) {
1944
+ return Qnil;
1945
+ }
1946
+
1947
+ return rb_str_new((const char*)buffer, buffer_size);
1948
+ }
1949
+
1950
+ // MsQuic derives this from the peer's max_datagram_frame_size transport parameter.
1951
+ static VALUE
1952
+ quicsilver_connection_datagram_send_enabled(VALUE self, VALUE connection_handle_val)
1953
+ {
1954
+ if (MsQuic == NULL) return Qfalse;
1955
+
1956
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
1957
+ if (Connection == NULL) return Qfalse;
1958
+
1959
+ BOOLEAN enabled = FALSE;
1960
+ uint32_t size = sizeof(enabled);
1961
+ QUIC_STATUS status = MsQuic->GetParam(
1962
+ Connection, QUIC_PARAM_CONN_DATAGRAM_SEND_ENABLED, &size, &enabled);
1963
+ return QUIC_SUCCEEDED(status) && enabled ? Qtrue : Qfalse;
1964
+ }
1965
+
1966
+ static VALUE
1967
+ quicsilver_connection_reliable_reset_enabled(VALUE self, VALUE connection_handle_val)
1968
+ {
1969
+ if (MsQuic == NULL) return Qfalse;
1970
+ HQUIC connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
1971
+ if (!connection) return Qfalse;
1972
+ ConnectionContext* ctx = MsQuic->GetContext(connection);
1973
+ return ctx && ctx->reliable_reset_negotiated ? Qtrue : Qfalse;
1974
+ }
1975
+
1976
+ static VALUE
1977
+ quicsilver_set_stream_reliable_offset(VALUE self, VALUE stream_handle, VALUE offset_val)
1978
+ {
1979
+ uint64_t token = NUM2ULL(stream_handle);
1980
+ uint64_t offset = NUM2ULL(offset_val);
1981
+ StreamContext* ctx = find_stream(token);
1982
+ if (!MsQuic || !ctx) rb_raise(rb_eIOError, "QUIC stream is closed");
1983
+ QUIC_STATUS status = MsQuic->SetParam(
1984
+ ctx->stream, QUIC_PARAM_STREAM_RELIABLE_OFFSET, sizeof(offset), &offset);
1985
+ if (QUIC_FAILED(status)) rb_raise(rb_eRuntimeError, "Setting reliable offset failed, 0x%x", status);
1986
+ return offset_val;
1987
+ }
1988
+
1989
+ // Returns [ip_string, port] or nil.
1990
+ static VALUE
1991
+ quicsilver_connection_remote_address(VALUE self, VALUE connection_handle_val)
1992
+ {
1993
+ if (MsQuic == NULL) return Qnil;
1994
+
1995
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
1996
+ if (Connection == NULL) return Qnil;
1997
+
1998
+ QUIC_ADDR addr;
1999
+ uint32_t addr_size = sizeof(addr);
2000
+ if (QUIC_FAILED(MsQuic->GetParam(Connection, QUIC_PARAM_CONN_REMOTE_ADDRESS, &addr_size, &addr)))
2001
+ return Qnil;
2002
+
2003
+ char ip[INET6_ADDRSTRLEN];
2004
+ uint16_t port = 0;
2005
+ if (addr.Ip.sa_family == QUIC_ADDRESS_FAMILY_INET) {
2006
+ struct sockaddr_in *v4 = (struct sockaddr_in *)&addr;
2007
+ inet_ntop(AF_INET, &v4->sin_addr, ip, sizeof(ip));
2008
+ port = ntohs(v4->sin_port);
2009
+ } else {
2010
+ struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)&addr;
2011
+ port = ntohs(v6->sin6_port);
2012
+ if (IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr)) {
2013
+ struct in_addr v4;
2014
+ memcpy(&v4, &v6->sin6_addr.s6_addr[12], 4);
2015
+ inet_ntop(AF_INET, &v4, ip, sizeof(ip));
2016
+ } else {
2017
+ inet_ntop(AF_INET6, &v6->sin6_addr, ip, sizeof(ip));
2018
+ }
2019
+ }
2020
+
2021
+ VALUE result = rb_ary_new2(2);
2022
+ rb_ary_push(result, rb_str_new_cstr(ip));
2023
+ rb_ary_push(result, UINT2NUM(port));
2024
+ return result;
2025
+ }
2026
+
2027
+ // QUIC connection ID bytes for server-side routing/debugging.
2028
+ static VALUE
2029
+ quicsilver_connection_ids(VALUE self, VALUE connection_handle_val)
2030
+ {
2031
+ VALUE result = rb_hash_new();
2032
+
2033
+ VALUE odcid = connection_param_bytes(connection_handle_val, QUIC_PARAM_CONN_ORIG_DEST_CID);
2034
+ if (!NIL_P(odcid)) {
2035
+ rb_hash_aset(result, rb_str_new_cstr("original_destination_connection_id"), odcid);
2036
+ }
2037
+
2038
+ return result;
2039
+ }
2040
+
2041
+ static void
2042
+ close_connection(HQUIC connection)
2043
+ {
2044
+ // MsQuic 2.6 defaults to a blocking close outside the worker callback.
2045
+ // Our poll thread needs the GVL to complete that work, so queue it instead.
2046
+ BOOLEAN async = TRUE;
2047
+ QUIC_STATUS status = MsQuic->SetParam(connection, QUIC_PARAM_CONN_CLOSE_ASYNC, sizeof(async), &async);
2048
+ if (QUIC_FAILED(status)) {
2049
+ rb_raise(rb_eRuntimeError, "Enabling asynchronous connection close failed, 0x%x!", status);
2050
+ }
2051
+ forget_connection_streams(connection);
2052
+ MsQuic->ConnectionClose(connection);
2053
+ wake_event_loop();
2054
+ }
2055
+
886
2056
  // Close a QUIC connection and free context
887
2057
  static VALUE
888
2058
  quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
@@ -899,7 +2069,7 @@ quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
899
2069
  (void)context_handle; // ctx freed by SHUTDOWN_COMPLETE, not here
900
2070
 
901
2071
  if (Connection != NULL) {
902
- MsQuic->ConnectionClose(Connection);
2072
+ close_connection(Connection);
903
2073
  }
904
2074
 
905
2075
  // Don't free ctx here — ConnectionClose is async and SHUTDOWN_COMPLETE
@@ -917,7 +2087,7 @@ quicsilver_close_server_connection(VALUE self, VALUE connection_handle)
917
2087
 
918
2088
  HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
919
2089
  if (Connection != NULL) {
920
- MsQuic->ConnectionClose(Connection);
2090
+ close_connection(Connection);
921
2091
  }
922
2092
  return Qnil;
923
2093
  }
@@ -965,6 +2135,7 @@ static VALUE
965
2135
  quicsilver_close(VALUE self)
966
2136
  {
967
2137
  if (MsQuic != NULL) {
2138
+ st_clear(LiveStreams);
968
2139
  if (Registration != NULL) {
969
2140
  MsQuic->RegistrationClose(Registration);
970
2141
  Registration = NULL;
@@ -1033,6 +2204,130 @@ quicsilver_create_listener(VALUE self, VALUE config_handle)
1033
2204
  return result;
1034
2205
  }
1035
2206
 
2207
+ static uint32_t
2208
+ cibir_param_buffer(VALUE cibir_bytes_val, uint8_t* buffer)
2209
+ {
2210
+ StringValue(cibir_bytes_val);
2211
+ long cibir_len = RSTRING_LEN(cibir_bytes_val);
2212
+ if (cibir_len == 0 || cibir_len > 6) {
2213
+ rb_raise(rb_eArgError, "cibir bytes must be 1..6 bytes");
2214
+ return 0;
2215
+ }
2216
+
2217
+ buffer[0] = 0;
2218
+ memcpy(buffer + 1, RSTRING_PTR(cibir_bytes_val), cibir_len);
2219
+ return (uint32_t)(1 + cibir_len);
2220
+ }
2221
+
2222
+ // Configure the fixed Server ID bytes MsQuic places in generated QUIC
2223
+ // connection IDs. Must run before RegistrationOpen, because MsQuic only applies
2224
+ // this process-global setting safely before the library is in use.
2225
+ static VALUE
2226
+ quicsilver_apply_msquic_server_id(VALUE self, VALUE server_id_bytes_val)
2227
+ {
2228
+ QUIC_STATUS Status;
2229
+
2230
+ if (Registration != NULL || ExecContext != NULL) {
2231
+ rb_raise(rb_eRuntimeError, "MsQuic Server ID must be configured before Quicsilver is opened");
2232
+ return Qfalse;
2233
+ }
2234
+
2235
+ if (MsQuic == NULL && QUIC_FAILED(Status = MsQuicOpenVersion(2, (const void**)&MsQuic))) {
2236
+ rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
2237
+ return Qfalse;
2238
+ }
2239
+
2240
+ QUIC_GLOBAL_SETTINGS Settings;
2241
+ memset(&Settings, 0, sizeof(Settings));
2242
+
2243
+ StringValue(server_id_bytes_val);
2244
+ if (RSTRING_LEN(server_id_bytes_val) != sizeof(Settings.FixedServerID)) {
2245
+ rb_raise(rb_eArgError, "Server ID must be exactly 4 bytes");
2246
+ return Qfalse;
2247
+ }
2248
+ Settings.IsSet.LoadBalancingMode = TRUE;
2249
+ Settings.LoadBalancingMode = QUIC_LOAD_BALANCING_SERVER_ID_FIXED;
2250
+ Settings.IsSet.FixedServerID = TRUE;
2251
+ memcpy(&Settings.FixedServerID, RSTRING_PTR(server_id_bytes_val), sizeof(Settings.FixedServerID));
2252
+
2253
+ Status = MsQuic->SetParam(
2254
+ NULL,
2255
+ QUIC_PARAM_GLOBAL_GLOBAL_SETTINGS,
2256
+ sizeof(Settings),
2257
+ &Settings);
2258
+
2259
+ if (QUIC_FAILED(Status)) {
2260
+ rb_raise(rb_eRuntimeError, "MsQuic Server ID configuration failed, 0x%x!", Status);
2261
+ return Qfalse;
2262
+ }
2263
+
2264
+ return Qtrue;
2265
+ }
2266
+
2267
+ // Configure listener-level CIBIR (Connection ID Based Implicit Routing) bytes.
2268
+ static VALUE
2269
+ quicsilver_configure_listener_cibir(VALUE self, VALUE listener_handle, VALUE cibir_bytes_val)
2270
+ {
2271
+ if (MsQuic == NULL) {
2272
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
2273
+ return Qfalse;
2274
+ }
2275
+
2276
+ uint8_t buffer[7];
2277
+ uint32_t buffer_len = cibir_param_buffer(cibir_bytes_val, buffer);
2278
+
2279
+ HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
2280
+ QUIC_STATUS Status = MsQuic->SetParam(
2281
+ Listener,
2282
+ QUIC_PARAM_LISTENER_CIBIR_ID,
2283
+ buffer_len,
2284
+ buffer);
2285
+
2286
+ if (QUIC_FAILED(Status)) {
2287
+ rb_raise(rb_eRuntimeError, "Listener CIBIR configuration failed, 0x%x!", Status);
2288
+ return Qfalse;
2289
+ }
2290
+
2291
+ return Qtrue;
2292
+ }
2293
+
2294
+ // Configure client-side CIBIR bytes before ConnectionStart.
2295
+ static VALUE
2296
+ quicsilver_configure_connection_cibir(VALUE self, VALUE connection_handle, VALUE cibir_bytes_val)
2297
+ {
2298
+ if (MsQuic == NULL) {
2299
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
2300
+ return Qfalse;
2301
+ }
2302
+
2303
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
2304
+ BOOLEAN share_binding = TRUE;
2305
+ QUIC_STATUS Status = MsQuic->SetParam(
2306
+ Connection,
2307
+ QUIC_PARAM_CONN_SHARE_UDP_BINDING,
2308
+ sizeof(share_binding),
2309
+ &share_binding);
2310
+ if (QUIC_FAILED(Status)) {
2311
+ rb_raise(rb_eRuntimeError, "Connection UDP binding configuration failed, 0x%x!", Status);
2312
+ return Qfalse;
2313
+ }
2314
+
2315
+ uint8_t buffer[7];
2316
+ uint32_t buffer_len = cibir_param_buffer(cibir_bytes_val, buffer);
2317
+ Status = MsQuic->SetParam(
2318
+ Connection,
2319
+ QUIC_PARAM_CONN_CIBIR_ID,
2320
+ buffer_len,
2321
+ buffer);
2322
+
2323
+ if (QUIC_FAILED(Status)) {
2324
+ rb_raise(rb_eRuntimeError, "Connection CIBIR configuration failed, 0x%x!", Status);
2325
+ return Qfalse;
2326
+ }
2327
+
2328
+ return Qtrue;
2329
+ }
2330
+
1036
2331
  // Start listener on specific address and port
1037
2332
  static VALUE
1038
2333
  quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port, VALUE alpn)
@@ -1050,14 +2345,17 @@ quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALU
1050
2345
  QUIC_ADDR Address;
1051
2346
  memset(&Address, 0, sizeof(Address));
1052
2347
 
1053
- // Parse address string to determine family
2348
+ // Parse address string to determine family.
2349
+ // "::" or any address with ':' → INET6 (IPv6 only on macOS).
2350
+ // "0.0.0.0" → UNSPEC (dual-stack: accepts both IPv4 and IPv6).
2351
+ // Specific IPv4 (e.g. "127.0.0.1") → INET (IPv4 only).
1054
2352
  const char* addr_str = StringValueCStr(address);
1055
2353
  if (strchr(addr_str, ':') != NULL) {
1056
- // IPv6 address (contains ':')
1057
2354
  QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET6);
1058
- } else {
1059
- // IPv4 address or unspecified - use UNSPEC for dual-stack
2355
+ } else if (strcmp(addr_str, "0.0.0.0") == 0) {
1060
2356
  QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_UNSPEC);
2357
+ } else {
2358
+ QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET);
1061
2359
  }
1062
2360
  QuicAddrSetPort(&Address, Port);
1063
2361
 
@@ -1139,6 +2437,7 @@ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
1139
2437
  ctx->connection = Connection;
1140
2438
  ctx->connection_ctx = conn_ctx; // Store connection context pointer
1141
2439
  ctx->client_obj = conn_ctx ? conn_ctx->client_obj : Qnil;
2440
+ ctx->stream_id = UINT64_MAX; // Sentinel: lazily resolved on first callback
1142
2441
  ctx->started = 1;
1143
2442
  ctx->shutdown = 0;
1144
2443
  ctx->early_data = 0;
@@ -1157,9 +2456,16 @@ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
1157
2456
  return Qnil;
1158
2457
  }
1159
2458
 
1160
- // Start the stream
1161
- Status = MsQuic->StreamStart(Stream, QUIC_STREAM_START_FLAG_NONE);
2459
+ uint64_t token = register_stream(ctx, Stream, RTEST(unidirectional)
2460
+ ? QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND : QUIC_STREAM_SHUTDOWN_FLAG_ABORT);
2461
+
2462
+ // Start the stream. INDICATE_PEER_ACCEPT gives us visibility into
2463
+ // stream flow control — fires PEER_ACCEPTED event when the peer
2464
+ // raises MAX_STREAMS if the stream was initially queued.
2465
+ Status = MsQuic->StreamStart(Stream,
2466
+ QUIC_STREAM_START_FLAG_INDICATE_PEER_ACCEPT | QUIC_STREAM_START_FLAG_SHUTDOWN_ON_FAIL);
1162
2467
  if (QUIC_FAILED(Status)) {
2468
+ unregister_stream(token);
1163
2469
  // StreamClose fires SHUTDOWN_COMPLETE synchronously which frees ctx
1164
2470
  MsQuic->StreamClose(Stream);
1165
2471
  rb_raise(rb_eRuntimeError, "StreamStart failed, 0x%x!", Status);
@@ -1167,7 +2473,7 @@ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
1167
2473
  }
1168
2474
 
1169
2475
  wake_event_loop();
1170
- return ULL2NUM((uintptr_t)Stream);
2476
+ return ULL2NUM(token);
1171
2477
  }
1172
2478
 
1173
2479
  // Send data on a QUIC stream
@@ -1179,31 +2485,34 @@ quicsilver_send_stream(VALUE self, VALUE stream_handle, VALUE data, VALUE send_f
1179
2485
  return Qnil;
1180
2486
  }
1181
2487
 
1182
- HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1183
- // Use StringValuePtr and RSTRING_LEN for binary data with null bytes
1184
- const char* data_str = RSTRING_PTR(data);
1185
- uint32_t data_len = (uint32_t)RSTRING_LEN(data);
1186
-
1187
- void* SendBufferRaw = malloc(sizeof(QUIC_BUFFER) + data_len);
1188
- if (SendBufferRaw == NULL) {
1189
- rb_raise(rb_eRuntimeError, "SendBuffer allocation failed!");
2488
+ uint64_t token = NUM2ULL(stream_handle);
2489
+ StringValue(data);
2490
+ StreamContext* ctx = find_stream(token);
2491
+ if (!ctx || __atomic_load_n(&ctx->shutdown, __ATOMIC_ACQUIRE)) rb_raise(rb_eIOError, "QUIC stream is closed");
2492
+ HQUIC Stream = ctx->stream;
2493
+
2494
+ // MsQuic holds a pointer into the String until SEND_COMPLETE, so the
2495
+ // bytes must not change. A frozen String is sent as-is (zero copy); an
2496
+ // unfrozen one gets a frozen copy-on-write share, so a caller that keeps
2497
+ // mutating its buffer unshares on its own side and never sees a
2498
+ // FrozenError from here. Library callers freeze their output to get the
2499
+ // zero-copy path.
2500
+ data = rb_str_new_frozen(data);
2501
+
2502
+ SendContext* send = send_context_new(data);
2503
+ if (send == NULL) {
2504
+ rb_raise(rb_eRuntimeError, "SendContext allocation failed!");
1190
2505
  return Qnil;
1191
2506
  }
1192
2507
 
1193
- QUIC_BUFFER* SendBuffer = (QUIC_BUFFER*)SendBufferRaw;
1194
- SendBuffer->Buffer = (uint8_t*)SendBufferRaw + sizeof(QUIC_BUFFER);
1195
- SendBuffer->Length = data_len;
1196
-
1197
- memcpy(SendBuffer->Buffer, data_str, data_len);
1198
-
1199
2508
  // Use flag based on parameter (default to FIN for backwards compat)
1200
2509
  QUIC_SEND_FLAGS flags = (NIL_P(send_fin) || RTEST(send_fin))
1201
2510
  ? QUIC_SEND_FLAG_FIN
1202
2511
  : QUIC_SEND_FLAG_NONE;
1203
-
1204
- QUIC_STATUS Status = MsQuic->StreamSend(Stream, SendBuffer, 1, flags, SendBufferRaw);
2512
+
2513
+ QUIC_STATUS Status = MsQuic->StreamSend(Stream, &send->buffer, 1, flags, send);
1205
2514
  if (QUIC_FAILED(Status)) {
1206
- free(SendBufferRaw);
2515
+ send_context_release(send);
1207
2516
  rb_raise(rb_eRuntimeError, "StreamSend failed, 0x%x!", Status);
1208
2517
  return Qfalse;
1209
2518
  }
@@ -1212,67 +2521,153 @@ quicsilver_send_stream(VALUE self, VALUE stream_handle, VALUE data, VALUE send_f
1212
2521
  return Qtrue;
1213
2522
  }
1214
2523
 
1215
- // Reset a QUIC stream (RESET_STREAM frame - abruptly terminates sending)
2524
+ // Send an unreliable datagram on a QUIC connection (RFC 9221).
2525
+ // Datagrams are not retransmitted — best effort delivery.
2526
+ // Data must fit in a single QUIC packet (typically ~1200 bytes).
1216
2527
  static VALUE
1217
- quicsilver_stream_reset(VALUE self, VALUE stream_handle, VALUE error_code)
2528
+ quicsilver_datagram_send(VALUE self, VALUE connection_data, VALUE data)
1218
2529
  {
1219
2530
  if (MsQuic == NULL) {
1220
2531
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1221
2532
  return Qnil;
1222
2533
  }
1223
2534
 
1224
- HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1225
- if (Stream == NULL) return Qnil;
1226
-
1227
- uint64_t ErrorCode = NUM2ULL(error_code);
1228
-
1229
- MsQuic->StreamShutdown(Stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND, ErrorCode);
1230
-
1231
- wake_event_loop();
1232
- return Qtrue;
1233
- }
2535
+ VALUE conn_handle_val = rb_ary_entry(connection_data, 0);
2536
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(conn_handle_val);
1234
2537
 
1235
- // Queue a stream priority change. Called from Ruby threads — just stores the
1236
- // priority. The actual SetParam happens on the MsQuic event thread in StreamCallback.
1237
- static VALUE
1238
- quicsilver_set_stream_priority(VALUE self, VALUE stream_handle, VALUE priority)
1239
- {
1240
- if (MsQuic == NULL) return Qnil;
2538
+ StringValue(data);
2539
+ const char* data_str = RSTRING_PTR(data);
2540
+ uint32_t data_len = (uint32_t)RSTRING_LEN(data);
1241
2541
 
1242
- HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1243
- if (Stream == NULL) return Qnil;
2542
+ void* SendBufferRaw = malloc(sizeof(QUIC_BUFFER) + data_len);
2543
+ if (SendBufferRaw == NULL) {
2544
+ rb_raise(rb_eRuntimeError, "Datagram buffer allocation failed!");
2545
+ return Qnil;
2546
+ }
1244
2547
 
1245
- if (PendingPriorityCount >= MAX_PENDING_PRIORITIES) return Qfalse;
2548
+ QUIC_BUFFER* SendBuffer = (QUIC_BUFFER*)SendBufferRaw;
2549
+ SendBuffer->Buffer = (uint8_t*)SendBufferRaw + sizeof(QUIC_BUFFER);
2550
+ SendBuffer->Length = data_len;
2551
+ memcpy(SendBuffer->Buffer, data_str, data_len);
1246
2552
 
1247
- uint16_t Priority = (uint16_t)NUM2UINT(priority);
1248
- PendingPriorities[PendingPriorityCount].stream = Stream;
1249
- PendingPriorities[PendingPriorityCount].priority_plus_one = Priority + 1;
1250
- PendingPriorityCount++;
2553
+ QUIC_STATUS Status = MsQuic->DatagramSend(Connection, SendBuffer, 1, QUIC_SEND_FLAG_NONE, SendBufferRaw);
2554
+ if (QUIC_FAILED(Status)) {
2555
+ free(SendBufferRaw);
2556
+ if (Status == QUIC_STATUS_BUFFER_TOO_SMALL) {
2557
+ rb_raise(rb_eArgError, "Datagram too large (%u bytes) - must fit in a single QUIC packet", data_len);
2558
+ } else {
2559
+ rb_raise(rb_eRuntimeError, "DatagramSend failed, 0x%x", Status);
2560
+ }
2561
+ return Qfalse;
2562
+ }
1251
2563
 
1252
2564
  wake_event_loop();
1253
2565
  return Qtrue;
1254
2566
  }
1255
2567
 
1256
- // Stop sending on a QUIC stream (STOP_SENDING frame - requests peer to stop)
2568
+ // Get the QUIC stream ID for an open stream.
2569
+ // Must be called after data has been sent (MsQuic defers ID assignment
2570
+ // with QUIC_STREAM_START_FLAG_NONE until data flows).
1257
2571
  static VALUE
1258
- quicsilver_stream_stop_sending(VALUE self, VALUE stream_handle, VALUE error_code)
2572
+ quicsilver_get_stream_id(VALUE self, VALUE stream_handle)
1259
2573
  {
1260
2574
  if (MsQuic == NULL) {
1261
2575
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1262
2576
  return Qnil;
1263
2577
  }
2578
+ StreamContext* ctx = find_stream(NUM2ULL(stream_handle));
2579
+ if (!ctx) return Qnil;
2580
+ HQUIC Stream = ctx->stream;
2581
+ uint64_t stream_id = 0;
2582
+ uint32_t stream_id_len = sizeof(stream_id);
2583
+ QUIC_STATUS Status = MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
2584
+ if (QUIC_FAILED(Status)) {
2585
+ return Qnil;
2586
+ }
2587
+ return ULL2NUM(stream_id);
2588
+ }
1264
2589
 
1265
- HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1266
- if (Stream == NULL) return Qnil;
2590
+ // Experimental native byte/chunk admission. First grant opts in (zero pauses on the
2591
+ // next data indication); subsequent grants add credit. This does not reserve
2592
+ // connection-level control credit or integrate application queue consumption.
2593
+ static VALUE
2594
+ quicsilver_grant_stream_receive_credit(VALUE self, VALUE stream_handle, VALUE bytes, VALUE chunks)
2595
+ {
2596
+ // Validate here, not in Transport::Stream: this module function is a public
2597
+ // boundary, and NUM2ULL silently wraps a negative Integer into huge credit.
2598
+ uint64_t token = NUM2ULL(stream_handle);
2599
+ uint64_t credit = credit_arg(bytes, "Receive credit");
2600
+ uint64_t chunk_credit = credit_arg(chunks, "Receive chunks");
2601
+ StreamContext* ctx = find_stream(token);
2602
+ if (!MsQuic || !ctx || !(ctx->abort_flags & QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE)) return Qfalse;
2603
+
2604
+ // Update under the lock; resume outside it (MsQuic call). A receive that
2605
+ // paused the stream set receive_paused under this same lock, so either it
2606
+ // ran before us and we see the flag, or it runs after us and sees the
2607
+ // credit. StreamReceiveSetEnabled is a queued MsQuic operation that runs
2608
+ // after any callback in flight, so the resume cannot land before the pause
2609
+ // it is meant to undo.
2610
+ int resume = 0;
2611
+ STATE_LOCK();
2612
+ if (ctx->receive_delivery_failed) { STATE_UNLOCK(); return Qfalse; }
2613
+ if (credit > UINT64_MAX - ctx->receive_credit) { STATE_UNLOCK(); rb_raise(rb_eRangeError, "Receive credit exceeds 64 bits"); }
2614
+ if (chunk_credit > UINT64_MAX - ctx->receive_chunks) { STATE_UNLOCK(); rb_raise(rb_eRangeError, "Receive chunks exceed 64 bits"); }
2615
+ ctx->receive_credit_enabled = 1;
2616
+ ctx->receive_credit += credit;
2617
+ ctx->receive_chunks += chunk_credit;
2618
+ if (ctx->receive_paused && ctx->receive_credit && ctx->receive_chunks) {
2619
+ ctx->receive_paused = 0;
2620
+ resume = 1;
2621
+ }
2622
+ STATE_UNLOCK();
1267
2623
 
1268
- uint64_t ErrorCode = NUM2ULL(error_code);
2624
+ if (resume && !resume_receive(ctx)) return Qfalse;
2625
+ wake_event_loop();
2626
+ return Qtrue;
2627
+ }
1269
2628
 
1270
- MsQuic->StreamShutdown(Stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE, ErrorCode);
1271
2629
 
2630
+ static VALUE
2631
+ shutdown_stream(VALUE stream_handle, VALUE error_code, QUIC_STREAM_SHUTDOWN_FLAGS flags)
2632
+ {
2633
+ uint64_t token = NUM2ULL(stream_handle);
2634
+ uint64_t code = NUM2ULL(error_code);
2635
+ StreamContext* ctx = find_stream(token);
2636
+ if (!MsQuic || !ctx) return Qfalse;
2637
+
2638
+ flags &= ctx->abort_flags;
2639
+ if (!flags) return Qfalse;
2640
+ QUIC_STATUS status = MsQuic->StreamShutdown(ctx->stream, flags, code);
2641
+ wake_event_loop();
2642
+ return QUIC_SUCCEEDED(status) ? Qtrue : Qfalse;
2643
+ }
2644
+
2645
+ // Reset a QUIC stream (RESET_STREAM frame - abruptly terminates sending)
2646
+ static VALUE
2647
+ quicsilver_stream_reset(VALUE self, VALUE stream_handle, VALUE error_code)
2648
+ {
2649
+ return shutdown_stream(stream_handle, error_code, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND);
2650
+ }
2651
+
2652
+ static VALUE
2653
+ quicsilver_set_stream_priority(VALUE self, VALUE stream_handle, VALUE priority)
2654
+ {
2655
+ uint64_t token = NUM2ULL(stream_handle);
2656
+ unsigned int value = NUM2UINT(priority);
2657
+ if (value > UINT16_MAX) rb_raise(rb_eArgError, "Stream priority must fit in 16 bits");
2658
+ StreamContext* ctx = find_stream(token);
2659
+ if (!MsQuic || !ctx) return Qfalse;
2660
+ __atomic_store_n(&ctx->pending_priority, value + 1, __ATOMIC_RELEASE);
1272
2661
  wake_event_loop();
1273
2662
  return Qtrue;
1274
2663
  }
1275
2664
 
2665
+ static VALUE
2666
+ quicsilver_stream_stop_sending(VALUE self, VALUE stream_handle, VALUE error_code)
2667
+ {
2668
+ return shutdown_stream(stream_handle, error_code, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE);
2669
+ }
2670
+
1276
2671
  static VALUE
1277
2672
  quicsilver_wake(VALUE self)
1278
2673
  {
@@ -1285,13 +2680,17 @@ void
1285
2680
  Init_quicsilver(void)
1286
2681
  {
1287
2682
  mQuicsilver = rb_define_module("Quicsilver");
2683
+ LiveStreams = st_init_numtable();
2684
+ LiveSends = st_init_numtable();
2685
+ LiveSendsRoot = TypedData_Wrap_Struct(rb_cObject, &live_sends_type, &LiveSendsRootData);
2686
+ rb_gc_register_mark_object(LiveSendsRoot);
1288
2687
 
1289
2688
  // Core initialization
1290
2689
  rb_define_singleton_method(mQuicsilver, "open_connection", quicsilver_open, 0);
1291
2690
  rb_define_singleton_method(mQuicsilver, "close_connection", quicsilver_close, 0);
1292
2691
 
1293
2692
  // Configuration management
1294
- rb_define_singleton_method(mQuicsilver, "create_configuration", quicsilver_create_configuration, 1);
2693
+ rb_define_singleton_method(mQuicsilver, "create_configuration", quicsilver_create_configuration, -1);
1295
2694
  rb_define_singleton_method(mQuicsilver, "create_server_configuration", quicsilver_create_server_configuration, 1);
1296
2695
  rb_define_singleton_method(mQuicsilver, "close_configuration", quicsilver_close_configuration, 1);
1297
2696
 
@@ -1300,12 +2699,24 @@ Init_quicsilver(void)
1300
2699
  rb_define_singleton_method(mQuicsilver, "start_connection", quicsilver_start_connection, 4);
1301
2700
  rb_define_singleton_method(mQuicsilver, "wait_for_connection", quicsilver_wait_for_connection, 2);
1302
2701
  rb_define_singleton_method(mQuicsilver, "connection_status", quicsilver_connection_status, 1);
2702
+ rb_define_singleton_method(mQuicsilver, "connection_statistics", quicsilver_connection_statistics, 1);
2703
+ rb_define_singleton_method(mQuicsilver, "transport_counters", quicsilver_transport_counters, 0);
2704
+ rb_define_singleton_method(mQuicsilver, "connection_reliable_reset_enabled?", quicsilver_connection_reliable_reset_enabled, 1);
2705
+ rb_define_singleton_method(mQuicsilver, "set_stream_reliable_offset", quicsilver_set_stream_reliable_offset, 2);
2706
+ rb_define_singleton_method(mQuicsilver, "connection_datagram_send_enabled?", quicsilver_connection_datagram_send_enabled, 1);
2707
+ rb_define_singleton_method(mQuicsilver, "connection_remote_address", quicsilver_connection_remote_address, 1);
2708
+ rb_define_singleton_method(mQuicsilver, "connection_ids", quicsilver_connection_ids, 1);
2709
+ rb_define_singleton_method(mQuicsilver, "get_resumption_ticket", quicsilver_get_resumption_ticket, 1);
2710
+ rb_define_singleton_method(mQuicsilver, "set_resumption_ticket", quicsilver_set_resumption_ticket, 2);
1303
2711
  rb_define_singleton_method(mQuicsilver, "connection_shutdown", quicsilver_connection_shutdown, 3);
1304
2712
  rb_define_singleton_method(mQuicsilver, "close_connection_handle", quicsilver_close_connection_handle, 1);
1305
2713
  rb_define_singleton_method(mQuicsilver, "close_server_connection", quicsilver_close_server_connection, 1);
1306
2714
 
1307
2715
  // Listener management
2716
+ rb_define_singleton_method(mQuicsilver, "apply_msquic_server_id", quicsilver_apply_msquic_server_id, 1);
1308
2717
  rb_define_singleton_method(mQuicsilver, "create_listener", quicsilver_create_listener, 1);
2718
+ rb_define_singleton_method(mQuicsilver, "configure_listener_cibir", quicsilver_configure_listener_cibir, 2);
2719
+ rb_define_singleton_method(mQuicsilver, "configure_connection_cibir", quicsilver_configure_connection_cibir, 2);
1309
2720
  rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 4);
1310
2721
  rb_define_singleton_method(mQuicsilver, "stop_listener", quicsilver_stop_listener, 1);
1311
2722
  rb_define_singleton_method(mQuicsilver, "close_listener", quicsilver_close_listener, 1);
@@ -1316,8 +2727,15 @@ Init_quicsilver(void)
1316
2727
  rb_define_singleton_method(mQuicsilver, "stream_reset", quicsilver_stream_reset, 2);
1317
2728
  rb_define_singleton_method(mQuicsilver, "stream_stop_sending", quicsilver_stream_stop_sending, 2);
1318
2729
  rb_define_singleton_method(mQuicsilver, "set_stream_priority", quicsilver_set_stream_priority, 2);
2730
+ rb_define_singleton_method(mQuicsilver, "get_stream_id", quicsilver_get_stream_id, 1);
2731
+ rb_define_singleton_method(mQuicsilver, "grant_stream_receive_credit", quicsilver_grant_stream_receive_credit, 3);
2732
+ rb_define_singleton_method(mQuicsilver, "datagram_send", quicsilver_datagram_send, 2);
1319
2733
 
1320
2734
  // Event processing (custom execution — app drives MsQuic)
1321
2735
  rb_define_singleton_method(mQuicsilver, "poll", quicsilver_poll, 0);
1322
2736
  rb_define_singleton_method(mQuicsilver, "wake", quicsilver_wake, 0);
2737
+ rb_define_singleton_method(mQuicsilver, "dispatch_events", quicsilver_dispatch_events, 2);
2738
+ rb_define_singleton_method(mQuicsilver, "open_event_queue", quicsilver_open_event_queue, 0);
2739
+ rb_define_singleton_method(mQuicsilver, "close_event_queue", quicsilver_close_event_queue, 0);
2740
+ rb_define_singleton_method(mQuicsilver, "dropped_datagrams", quicsilver_dropped_datagrams, 0);
1323
2741
  }