http3 0.0.4 → 0.0.5

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.
data/ext/http3/session.c CHANGED
@@ -6,6 +6,7 @@
6
6
  #include <stdlib.h>
7
7
  #include <string.h>
8
8
  #include <sys/random.h>
9
+ #include <time.h>
9
10
  #include <unistd.h>
10
11
 
11
12
  /*
@@ -28,7 +29,7 @@
28
29
  VALUE rb_cHTTP3Session;
29
30
 
30
31
  static ID id_on_begin_headers, id_on_header, id_on_end_headers, id_on_data,
31
- id_on_end_stream, id_on_fields_too_large, id_on_reset;
32
+ id_on_end_stream, id_on_fields_too_large, id_on_reset, id_on_stop_sending;
32
33
 
33
34
  /* What h3_session.field_sizes holds for a request whose header section has
34
35
  * gone over max_field_section_size. */
@@ -65,12 +66,19 @@ typedef struct {
65
66
  size_t nchunks, cap;
66
67
  size_t handed; /* how many of chunks read_data has handed to nghttp3 */
67
68
  uint64_t unsent; /* bytes queued but not handed to nghttp3 yet */
69
+ uint64_t queued; /* bytes ever queued, the pieces read from a file too */
68
70
  uint64_t acked; /* bytes the peer has acknowledged */
69
71
  uint64_t shifted; /* bytes of the chunks already dropped */
70
72
  int fd; /* a file body (#attach_file) not read in full yet, or -1 */
71
73
  off_t file_off, file_end;
72
- bool eof; /* nothing follows the queued chunks */
73
- bool blocked; /* read_data returned NGHTTP3_ERR_WOULDBLOCK */
74
+ /* When the body began to wait on the peer, or last moved: the peer
75
+ * acknowledged some of it, or widened the stream's flow control window
76
+ * (CLOCK_MONOTONIC seconds; #response_stalled_since) */
77
+ double since;
78
+ bool fc_blocked; /* the peer's flow control window holds it back */
79
+ bool eof; /* nothing follows the queued chunks */
80
+ bool blocked; /* read_data returned NGHTTP3_ERR_WOULDBLOCK */
81
+ bool aborted; /* #abort_stream stopped it: no more of the body is sent */
74
82
  } h3_stream;
75
83
 
76
84
  typedef struct {
@@ -89,8 +97,18 @@ typedef struct {
89
97
  /* stream_id => size of the header section received so far, while the
90
98
  * headers of that request come in */
91
99
  st_table *field_sizes;
100
+ /* When the peer last acknowledged some response body, on any stream
101
+ * (CLOCK_MONOTONIC seconds) */
102
+ double acked_at;
92
103
  } h3_session;
93
104
 
105
+ /* CLOCK_MONOTONIC in seconds, as Process.clock_gettime gives it. */
106
+ static double h3_now(void) {
107
+ struct timespec ts;
108
+ clock_gettime(CLOCK_MONOTONIC, &ts);
109
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
110
+ }
111
+
94
112
  /* Streams */
95
113
 
96
114
  static void h3_chunk_release(h3_chunk *chunk) {
@@ -130,9 +148,20 @@ static int h3_stream_push(h3_stream *st, const uint8_t *base, size_t len,
130
148
  }
131
149
  st->chunks[st->nchunks++] = (h3_chunk){base, len, str};
132
150
  st->unsent += len;
151
+ st->queued += len;
133
152
  return 0;
134
153
  }
135
154
 
155
+ /* Bytes of the body the peer has not acknowledged: queued, the rest of a
156
+ * file not read yet, or sent; 0 once the stream has been aborted. */
157
+ static uint64_t h3_stream_unacked(const h3_stream *st) {
158
+ if (st->aborted) {
159
+ return 0;
160
+ }
161
+ uint64_t rest = st->fd >= 0 ? (uint64_t)(st->file_end - st->file_off) : 0;
162
+ return st->queued - st->acked + rest;
163
+ }
164
+
136
165
  /* The whole file body has been read: let go of its descriptor now rather
137
166
  * than when the stream closes, which waits for the peer to acknowledge
138
167
  * every byte. */
@@ -142,6 +171,24 @@ static void h3_stream_end_file(h3_stream *st) {
142
171
  st->eof = true;
143
172
  }
144
173
 
174
+ /* The stream has been aborted, and no more of its body will be sent: let go
175
+ * at once of what nghttp3 has not taken, and of the file's descriptor,
176
+ * rather than when the stream closes, which waits for the peer to
177
+ * acknowledge the reset. The chunks nghttp3 has taken stay until then,
178
+ * since it still points into them. */
179
+ static void h3_stream_abort(h3_stream *st) {
180
+ for (size_t i = st->handed; i < st->nchunks; i++) {
181
+ h3_chunk_release(&st->chunks[i]);
182
+ }
183
+ st->nchunks = st->handed;
184
+ st->unsent = 0;
185
+ if (st->fd >= 0) {
186
+ close(st->fd);
187
+ st->fd = -1;
188
+ }
189
+ st->aborted = true;
190
+ }
191
+
145
192
  /* Drop the chunks at the front the peer has acknowledged to their end. A
146
193
  * chunk acknowledged only in part stays. */
147
194
  static void h3_stream_acked(h3_stream *st, uint64_t n) {
@@ -276,7 +323,14 @@ static VALUE h3_end_headers_body(VALUE arg) {
276
323
  static int h3_acked_stream_data_cb(nghttp3_conn *conn, int64_t id,
277
324
  uint64_t datalen, void *user_data,
278
325
  void *stream_user_data) {
279
- h3_stream *st = h3_stream_find(user_data, id);
326
+ h3_session *s = user_data;
327
+ h3_stream *st = h3_stream_find(s, id);
328
+ if (datalen > 0) {
329
+ s->acked_at = h3_now();
330
+ if (st != NULL) {
331
+ st->since = s->acked_at;
332
+ }
333
+ }
280
334
  if (st != NULL) {
281
335
  h3_stream_acked(st, datalen);
282
336
  }
@@ -468,7 +522,7 @@ static void h3_raise(h3_session *s, int rv, const char *message) {
468
522
  if (nghttp3_err_is_fatal(rv) || rv == NGHTTP3_ERR_QPACK_FATAL) {
469
523
  h3_fail(s, rv);
470
524
  }
471
- nghttp3_rb_raise(rv, message);
525
+ nghttp3_rb_raise(rv, "%s", message);
472
526
  }
473
527
 
474
528
  /* Make the nghttp3 connection and its three unidirectional streams, as
@@ -579,14 +633,22 @@ static int h3_stream_data_written(void *ud, int64_t id, size_t n) {
579
633
  return rv == 0 ? 0 : h3_fail(s, rv);
580
634
  }
581
635
 
636
+ /* The peer's flow control window for the stream is used up. */
582
637
  static int h3_stream_blocked(void *ud, int64_t id) {
583
638
  h3_session *s = ud;
639
+ h3_stream *st = h3_stream_find(s, id);
640
+ if (st != NULL) {
641
+ st->fc_blocked = true;
642
+ }
584
643
  if (s->h3 != NULL) {
585
644
  nghttp3_conn_block_stream(s->h3, id);
586
645
  }
587
646
  return 0;
588
647
  }
589
648
 
649
+ /* quic found our side of the stream shut when next_stream_data offered it:
650
+ * nghttp3 sends no more of it. recv_stop_sending and #abort_stream tell
651
+ * nghttp3 first, so this is what is left for a stream shut otherwise. */
590
652
  static int h3_stream_shut_write(void *ud, int64_t id) {
591
653
  h3_session *s = ud;
592
654
  if (s->h3 != NULL) {
@@ -604,8 +666,15 @@ static int h3_acked_stream_data(void *ud, int64_t id, uint64_t n) {
604
666
  return rv == 0 ? 0 : h3_fail(s, rv);
605
667
  }
606
668
 
669
+ /* The peer widened the stream's flow control window: it has read some of
670
+ * the body. */
607
671
  static int h3_extend_max_stream_data(void *ud, int64_t id) {
608
672
  h3_session *s = ud;
673
+ h3_stream *st = h3_stream_find(s, id);
674
+ if (st != NULL) {
675
+ st->fc_blocked = false;
676
+ st->since = h3_now();
677
+ }
609
678
  if (s->h3 == NULL) {
610
679
  return 0;
611
680
  }
@@ -646,6 +715,51 @@ static int h3_stream_reset(void *ud, int64_t id, uint64_t final_size,
646
715
  return h3_call_ruby(s, id_on_reset, id, 0, NULL);
647
716
  }
648
717
 
718
+ /* The client asked for no more of what goes out on the stream
719
+ * (STOP_SENDING); ngtcp2 resets our side of it once this returns. Of a
720
+ * response, what nghttp3 has not taken is let go at once, and nghttp3
721
+ * sends no more. The rest of the request is not read either: RFC 9114
722
+ * section 4.1.1 has a cancelled request stopped in every direction still
723
+ * open. That is done here, as #stop_reading does it, rather than left to
724
+ * the Ruby part, so that nothing more of the request comes in, from the
725
+ * packets read after this one too, even on a stream the Ruby part has not
726
+ * heard of yet, since STOP_SENDING may come before the request. quic sends
727
+ * nothing if all of the request has arrived. The Ruby part then drops the
728
+ * request (#on_stop_sending).
729
+ *
730
+ * Our unidirectional streams are the control and QPACK streams, which the
731
+ * client must not ask us to close, and whose closing is a connection error
732
+ * of type H3_CLOSED_CRITICAL_STREAM (RFC 9114 section 6.2.1, RFC 9204
733
+ * section 4.2): the connection fails at once, rather than when the stream
734
+ * has closed. The client's own unidirectional streams never get here:
735
+ * ngtcp2 refuses STOP_SENDING on a stream we only receive on. */
736
+ static int h3_recv_stop_sending(void *ud, int64_t id, uint64_t app_error_code) {
737
+ h3_session *s = ud;
738
+ if (s->h3 == NULL) {
739
+ return 0;
740
+ }
741
+ if (id & 0x2) {
742
+ return h3_fail_with(s, NGHTTP3_H3_CLOSED_CRITICAL_STREAM);
743
+ }
744
+ h3_stream *st = h3_stream_find(s, id);
745
+ if (st != NULL) {
746
+ h3_stream_abort(st);
747
+ }
748
+ nghttp3_conn_shutdown_stream_write(s->h3, id);
749
+ int rv = h3_shutdown_stream_read(s, id);
750
+ if (rv != 0) {
751
+ return rv;
752
+ }
753
+ if (http3_quic_api->shutdown_stream_read(s->qc, id,
754
+ NGHTTP3_H3_REQUEST_CANCELLED) != 0) {
755
+ return h3_fail_with(s, NGHTTP3_H3_INTERNAL_ERROR);
756
+ }
757
+ /* No end_headers will come to drop the count of a section cut short. */
758
+ st_data_t key = (st_data_t)id;
759
+ st_delete(s->field_sizes, &key, NULL);
760
+ return h3_call_ruby(s, id_on_stop_sending, id, 0, NULL);
761
+ }
762
+
649
763
  static int h3_stream_close(void *ud, int64_t id, int has_code,
650
764
  uint64_t app_error_code) {
651
765
  h3_session *s = ud;
@@ -660,8 +774,16 @@ static int h3_stream_close(void *ud, int64_t id, int has_code,
660
774
  return h3_fail(s, rv);
661
775
  }
662
776
 
777
+ /* The size of a handler filled in up to member m. The size tells quic.so
778
+ * which members the handler has (quic/ext.h): those it fills in, whatever
779
+ * quic/ext.h it is built against. */
780
+ #define H3_HANDLER_SIZE_TO(m) \
781
+ (offsetof(quic_ext_stream_handler, m) + \
782
+ sizeof(((quic_ext_stream_handler *)0)->m))
783
+
784
+ /* An ABI 1.1 handler: recv_stop_sending came with quic 0.0.6. */
663
785
  static const quic_ext_stream_handler h3_handler = {
664
- .size = sizeof(quic_ext_stream_handler),
786
+ .size = H3_HANDLER_SIZE_TO(recv_stop_sending),
665
787
  .recv_stream_data = h3_recv_stream_data,
666
788
  .next_stream_data = h3_next_stream_data,
667
789
  .stream_data_written = h3_stream_data_written,
@@ -674,6 +796,7 @@ static const quic_ext_stream_handler h3_handler = {
674
796
  .stream_reset = h3_stream_reset,
675
797
  .stream_close = h3_stream_close,
676
798
  .handshake_completed = h3_handshake_completed,
799
+ .recv_stop_sending = h3_recv_stop_sending,
677
800
  };
678
801
 
679
802
  /* The Ruby object */
@@ -703,6 +826,11 @@ static int h3_free_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
703
826
  return ST_CONTINUE;
704
827
  }
705
828
 
829
+ static int h3_abort_stream_i(st_data_t key, st_data_t value, st_data_t arg) {
830
+ h3_stream_abort((h3_stream *)value);
831
+ return ST_CONTINUE;
832
+ }
833
+
706
834
  /* Never touches qc: the QUIC connection may be gone already. */
707
835
  static void h3_session_free(void *ptr) {
708
836
  h3_session *s = ptr;
@@ -783,6 +911,12 @@ static void h3_raise_no_body(VALUE rb_id) {
783
911
  "no response body is open on stream %" PRIsVALUE, rb_id);
784
912
  }
785
913
 
914
+ NORETURN(static void h3_raise_aborted(VALUE rb_id));
915
+ static void h3_raise_aborted(VALUE rb_id) {
916
+ rb_raise(rb_eNghttp3InvalidStateError,
917
+ "stream %" PRIsVALUE " has been aborted", rb_id);
918
+ }
919
+
786
920
  /* The body of stream rb_id. Ruby code that runs once it is found may make
787
921
  * the connection go on and close the stream, so convert everything else
788
922
  * first. */
@@ -974,18 +1108,26 @@ static VALUE rb_http3_session_stop_reading(VALUE self, VALUE rb_id) {
974
1108
  * session.push_chunk(stream_id, str) -> self
975
1109
  *
976
1110
  * Queue a frozen copy of str as the next piece of the response body.
1111
+ * Raises InvalidStateError once the stream has been aborted.
977
1112
  */
978
1113
  static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
979
1114
  h3_session *s = h3_session_enter(self);
980
1115
  StringValue(str);
981
1116
  VALUE copy = rb_str_new_frozen(str);
982
1117
  h3_stream *st = h3_session_stream(self, rb_id);
1118
+ if (st->aborted) {
1119
+ h3_raise_aborted(rb_id);
1120
+ }
983
1121
  if (st->eof) {
984
1122
  rb_raise(rb_eNghttp3InvalidStateError, "the body has ended");
985
1123
  }
986
1124
  if (RSTRING_LEN(copy) == 0) {
987
1125
  return self;
988
1126
  }
1127
+ if (h3_stream_unacked(st) == 0) {
1128
+ /* The body waits on the peer from now. */
1129
+ st->since = h3_now();
1130
+ }
989
1131
  if (h3_stream_push(st, (const uint8_t *)RSTRING_PTR(copy),
990
1132
  (size_t)RSTRING_LEN(copy), copy) != 0) {
991
1133
  rb_raise(rb_eNoMemError, "failed to queue a body chunk");
@@ -999,11 +1141,15 @@ static VALUE rb_http3_session_push_chunk(VALUE self, VALUE rb_id, VALUE str) {
999
1141
  * call-seq:
1000
1142
  * session.push_eof(stream_id) -> self
1001
1143
  *
1002
- * Mark the end of the response body.
1144
+ * Mark the end of the response body. Raises InvalidStateError once the
1145
+ * stream has been aborted.
1003
1146
  */
1004
1147
  static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
1005
1148
  h3_session *s = h3_session_enter(self);
1006
1149
  h3_stream *st = h3_session_stream(self, rb_id);
1150
+ if (st->aborted) {
1151
+ h3_raise_aborted(rb_id);
1152
+ }
1007
1153
  st->eof = true;
1008
1154
  h3_stream_wake(s, st);
1009
1155
  return self;
@@ -1017,7 +1163,8 @@ static VALUE rb_http3_session_push_eof(VALUE self, VALUE rb_id) {
1017
1163
  * descriptor is duplicated, so io may be closed afterwards. The duplicate
1018
1164
  * is close-on-exec, as Ruby's own descriptors are, so that a process the
1019
1165
  * application spawns does not inherit it, and is closed as soon as the
1020
- * whole length has been read.
1166
+ * whole length has been read. Raises InvalidStateError once the stream has
1167
+ * been aborted.
1021
1168
  */
1022
1169
  static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
1023
1170
  VALUE offset, VALUE length) {
@@ -1039,6 +1186,10 @@ static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
1039
1186
  close(fd);
1040
1187
  h3_raise_no_body(rb_id);
1041
1188
  }
1189
+ if (st->aborted) {
1190
+ close(fd);
1191
+ h3_raise_aborted(rb_id);
1192
+ }
1042
1193
  if (st->fd >= 0 || st->eof || st->nchunks > 0) {
1043
1194
  close(fd);
1044
1195
  rb_raise(rb_eNghttp3InvalidStateError, "the body is already given");
@@ -1046,6 +1197,7 @@ static VALUE rb_http3_session_attach_file(VALUE self, VALUE rb_id, VALUE io,
1046
1197
  st->fd = fd;
1047
1198
  st->file_off = off;
1048
1199
  st->file_end = off + len;
1200
+ st->since = h3_now();
1049
1201
  if (len == 0) {
1050
1202
  h3_stream_end_file(st);
1051
1203
  }
@@ -1065,6 +1217,36 @@ static VALUE rb_http3_session_unsent_bytes(VALUE self, VALUE rb_id) {
1065
1217
  return ULL2NUM(st == NULL ? 0 : st->unsent);
1066
1218
  }
1067
1219
 
1220
+ /*
1221
+ * call-seq:
1222
+ * session.response_stalled_since(stream_id) -> Float or nil
1223
+ *
1224
+ * Since when (CLOCK_MONOTONIC seconds, as Process.clock_gettime gives it)
1225
+ * the response body on stream_id has waited on the client: some of it has
1226
+ * not been acknowledged, and nothing has moved since. nil while none of it
1227
+ * waits, for a stream without a body, and once the stream has been
1228
+ * aborted.
1229
+ *
1230
+ * Moving is the client acknowledging some of the body, or widening the
1231
+ * stream's flow control window. While that window does not hold the body
1232
+ * back, an acknowledgement of any response body on the connection counts
1233
+ * too: nghttp3 sends the responses of one priority one at a time, so a
1234
+ * response waiting its turn behind another the client is reading gets no
1235
+ * acknowledgements of its own.
1236
+ */
1237
+ static VALUE rb_http3_session_response_stalled_since(VALUE self, VALUE rb_id) {
1238
+ h3_session *s = h3_session_get(self);
1239
+ h3_stream *st = h3_stream_find(s, NUM2LL(rb_id));
1240
+ if (st == NULL || h3_stream_unacked(st) == 0) {
1241
+ return Qnil;
1242
+ }
1243
+ double since = st->since;
1244
+ if (!st->fc_blocked && s->acked_at > since) {
1245
+ since = s->acked_at;
1246
+ }
1247
+ return DBL2NUM(since);
1248
+ }
1249
+
1068
1250
  /*
1069
1251
  * call-seq:
1070
1252
  * session.take_closed -> Array
@@ -1116,13 +1298,21 @@ static VALUE rb_http3_session_shutdown_notice(VALUE self) {
1116
1298
  * session.abort_stream(stream_id, app_error_code) -> self
1117
1299
  *
1118
1300
  * Stop the stream in both directions with app_error_code (for instance
1119
- * H3_INTERNAL_ERROR after the response headers went out).
1301
+ * H3_INTERNAL_ERROR after the response headers went out). What of the
1302
+ * response body nghttp3 has not taken is let go at once, so that
1303
+ * #unsent_bytes no longer counts it, and a file body's descriptor is
1304
+ * closed; #push_chunk, #push_eof and #attach_file raise InvalidStateError
1305
+ * from then on.
1120
1306
  */
1121
1307
  static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
1122
1308
  VALUE rb_code) {
1123
1309
  h3_session *s = h3_session_enter(self);
1124
1310
  int64_t id = NUM2LL(rb_id);
1125
1311
  uint64_t code = NUM2ULL(rb_code);
1312
+ h3_stream *st = h3_stream_find(s, id);
1313
+ if (st != NULL) {
1314
+ h3_stream_abort(st);
1315
+ }
1126
1316
  if (s->h3 != NULL) {
1127
1317
  nghttp3_conn_shutdown_stream_write(s->h3, id);
1128
1318
  }
@@ -1131,6 +1321,36 @@ static VALUE rb_http3_session_abort_stream(VALUE self, VALUE rb_id,
1131
1321
  return self;
1132
1322
  }
1133
1323
 
1324
+ /*
1325
+ * call-seq:
1326
+ * session.release_connection -> self (private; #release_all calls it)
1327
+ *
1328
+ * Let go of the HTTP/3 connection now, rather than when the Session is
1329
+ * garbage collected: the nghttp3 connection is deleted, and every response
1330
+ * body aborted as #abort_stream aborts one, though no stream is reset, so
1331
+ * that what nghttp3 had not taken of it is let go of and a file body's
1332
+ * descriptor closed. What nghttp3 had taken stays until the Session is
1333
+ * freed, even once the stream closes, since without nghttp3 nothing tells
1334
+ * of that: ngtcp2 may still point into it, to send it again.
1335
+ *
1336
+ * For a QUIC connection that has gone. One still driven goes on as after a
1337
+ * failure (h3_fail_with), with H3_NO_ERROR; without nghttp3, it sends
1338
+ * nothing more of a body let go of, its FIN included. A connection that
1339
+ * failed keeps the error it failed with. From then on #submit_response and
1340
+ * #stop_reading raise InvalidStateError, as on a failed connection, and
1341
+ * #push_chunk, #push_eof and #attach_file as on an aborted stream. Raises
1342
+ * InvalidStateError inside a callback of the Session, where nghttp3 may be
1343
+ * running. Calling it again does nothing.
1344
+ */
1345
+ static VALUE rb_http3_session_release_connection(VALUE self) {
1346
+ h3_session *s = h3_session_enter(self);
1347
+ if (!s->failed) {
1348
+ h3_fail_with(s, NGHTTP3_H3_NO_ERROR);
1349
+ }
1350
+ st_foreach(s->streams, h3_abort_stream_i, 0);
1351
+ return self;
1352
+ }
1353
+
1134
1354
  void Init_http3_session(VALUE rb_mHTTP3) {
1135
1355
  id_on_begin_headers = rb_intern("on_begin_headers");
1136
1356
  id_on_header = rb_intern("on_header");
@@ -1139,6 +1359,7 @@ void Init_http3_session(VALUE rb_mHTTP3) {
1139
1359
  id_on_end_stream = rb_intern("on_end_stream");
1140
1360
  id_on_fields_too_large = rb_intern("on_fields_too_large");
1141
1361
  id_on_reset = rb_intern("on_reset");
1362
+ id_on_stop_sending = rb_intern("on_stop_sending");
1142
1363
 
1143
1364
  rb_cHTTP3Session = rb_define_class_under(rb_mHTTP3, "Session", rb_cObject);
1144
1365
  rb_undef_alloc_func(rb_cHTTP3Session);
@@ -1155,6 +1376,8 @@ void Init_http3_session(VALUE rb_mHTTP3) {
1155
1376
  rb_http3_session_attach_file, 4);
1156
1377
  rb_define_method(rb_cHTTP3Session, "unsent_bytes",
1157
1378
  rb_http3_session_unsent_bytes, 1);
1379
+ rb_define_method(rb_cHTTP3Session, "response_stalled_since",
1380
+ rb_http3_session_response_stalled_since, 1);
1158
1381
  rb_define_method(rb_cHTTP3Session, "take_closed",
1159
1382
  rb_http3_session_take_closed, 0);
1160
1383
  rb_define_method(rb_cHTTP3Session, "take_error", rb_http3_session_take_error,
@@ -1163,4 +1386,6 @@ void Init_http3_session(VALUE rb_mHTTP3) {
1163
1386
  rb_http3_session_shutdown_notice, 0);
1164
1387
  rb_define_method(rb_cHTTP3Session, "abort_stream",
1165
1388
  rb_http3_session_abort_stream, 2);
1389
+ rb_define_private_method(rb_cHTTP3Session, "release_connection",
1390
+ rb_http3_session_release_connection, 0);
1166
1391
  }
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP3
4
+ # The stream a response was being sent on is gone: the client reset it or
5
+ # asked for no more of it (STOP_SENDING), the connection ended, the server
6
+ # stopped, or the response was aborted after waiting on its client for
7
+ # response_timeout. Body::Pipe's write, finish and flush raise it. It is
8
+ # an IOError, as a write to a closed socket raises one.
9
+ class StreamClosed < IOError
10
+ end
11
+
12
+ module Body
13
+ # A response body written by the thread that runs the application and
14
+ # taken by the server's I/O loop, which sends it. On the writer's side,
15
+ # #write waits while capacity bytes or more are queued, and #finish ends
16
+ # the body, #fail ends it with an exception, each waiting until all of
17
+ # it has been taken. On the I/O loop's side, #take never waits, and
18
+ # #cancel gives up on the body: from then on the writer's write, finish
19
+ # and flush raise StreamClosed, those waiting included, and fail
20
+ # returns false. Since a writer waits for the
21
+ # end of its body to be taken, the pipes holding chunks are no more than
22
+ # the writers.
23
+ #
24
+ # wake is called, with no lock held, each time the body goes from
25
+ # having nothing to take to having something: a chunk, its end, or the
26
+ # exception that ended it.
27
+ class Pipe
28
+ def initialize(capacity:, wake:)
29
+ @capacity = capacity
30
+ @wake = wake
31
+ @lock = Thread::Mutex.new
32
+ @changed = Thread::ConditionVariable.new
33
+ @chunks = []
34
+ @bytes = 0
35
+ @finished = false
36
+ @end_taken = false
37
+ @error = nil
38
+ @cancelled = false
39
+ end
40
+
41
+ # Queue chunk, a String, first waiting while capacity bytes or more are
42
+ # queued. A String that can still change is copied. Returns its size.
43
+ def write(chunk)
44
+ raise TypeError, "chunk must be a String (got #{chunk.class})" unless chunk.is_a?(String)
45
+
46
+ chunk = chunk.dup.freeze unless chunk.frozen?
47
+ woke = @lock.synchronize do
48
+ @changed.wait(@lock) while @bytes >= @capacity && !@cancelled
49
+ check_writable
50
+ next false if chunk.empty?
51
+
52
+ was_empty = @chunks.empty?
53
+ @chunks << chunk
54
+ @bytes += chunk.bytesize
55
+ was_empty
56
+ end
57
+ @wake.call if woke
58
+ chunk.bytesize
59
+ end
60
+
61
+ # End the body, and wait until the I/O loop has taken all of it, its
62
+ # end included.
63
+ def finish
64
+ woke = @lock.synchronize do
65
+ check_writable
66
+ @finished = true
67
+ @chunks.empty?
68
+ end
69
+ @wake.call if woke
70
+ @lock.synchronize do
71
+ wait_for_the_end
72
+ raise StreamClosed, "stream closed" unless @end_taken
73
+ end
74
+ nil
75
+ end
76
+
77
+ # End the body with error, for the I/O loop to abort the stream once
78
+ # it has taken what is queued, and wait until it has. Returns true
79
+ # once the I/O loop has taken error, and false when it never will:
80
+ # the body had ended or been given up on, and fail did nothing, or it
81
+ # was given up on while fail waited.
82
+ def fail(error)
83
+ woke = @lock.synchronize do
84
+ next nil if @cancelled || @finished || @error
85
+
86
+ @error = error
87
+ @chunks.empty?
88
+ end
89
+ return false if woke.nil?
90
+
91
+ @wake.call if woke
92
+ @lock.synchronize do
93
+ wait_for_the_end
94
+ @end_taken
95
+ end
96
+ end
97
+
98
+ # Wait until the I/O loop has taken every chunk queued.
99
+ def flush
100
+ @lock.synchronize do
101
+ @changed.wait(@lock) until @chunks.empty? || @cancelled
102
+ raise StreamClosed, "stream closed" if @cancelled
103
+ end
104
+ self
105
+ end
106
+
107
+ # The next of what the writer queued, without waiting: a chunk; once
108
+ # they have all been taken, :eof for a body that ended, or the
109
+ # exception given to #fail; nil while there is nothing yet, and after
110
+ # #cancel.
111
+ def take
112
+ @lock.synchronize do
113
+ return nil if @cancelled
114
+
115
+ if (chunk = @chunks.shift)
116
+ @bytes -= chunk.bytesize
117
+ @changed.broadcast
118
+ return chunk
119
+ end
120
+ return nil unless @finished || @error
121
+
122
+ @end_taken = true
123
+ @changed.broadcast
124
+ @error || :eof
125
+ end
126
+ end
127
+
128
+ # True when #take has something new to return: a chunk, or the end.
129
+ def ready?
130
+ @lock.synchronize do
131
+ !@cancelled && (!@chunks.empty? || ((@finished || !@error.nil?) && !@end_taken))
132
+ end
133
+ end
134
+
135
+ # Give up on the body: what is queued is dropped, and the writer's
136
+ # calls, those waiting included, raise StreamClosed.
137
+ def cancel
138
+ @lock.synchronize do
139
+ @cancelled = true
140
+ @chunks.clear
141
+ @bytes = 0
142
+ @changed.broadcast
143
+ end
144
+ nil
145
+ end
146
+
147
+ private
148
+
149
+ # With the lock held.
150
+ def wait_for_the_end
151
+ @changed.wait(@lock) until @end_taken || @cancelled
152
+ end
153
+
154
+ def check_writable
155
+ raise StreamClosed, "stream closed" if @cancelled
156
+ raise IOError, "body already ended" if @finished || @error
157
+ end
158
+ end
159
+ end
160
+ end