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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/LICENSE-DEPENDENCIES.txt +2 -2
- data/README.md +76 -17
- data/ext/http3/extconf.rb +8 -5
- data/ext/http3/nghttp3.c +1 -1
- data/ext/http3/nghttp3.h +3 -1
- data/ext/http3/nghttp3_callbacks.c +8 -10
- data/ext/http3/nghttp3_connection.c +1 -1
- data/ext/http3/nghttp3_qpack.c +2 -2
- data/ext/http3/session.c +234 -9
- data/lib/http3/body_pipe.rb +160 -0
- data/lib/http3/body_stream.rb +120 -0
- data/lib/http3/rack_adapter.rb +114 -47
- data/lib/http3/request.rb +4 -2
- data/lib/http3/server.rb +402 -62
- data/lib/http3/session.rb +458 -89
- data/lib/http3/version.rb +1 -1
- data/lib/http3/worker_pool.rb +71 -0
- data/lib/http3.rb +3 -0
- data/sig/http3/body_pipe.rbs +45 -0
- data/sig/http3/body_stream.rbs +41 -0
- data/sig/http3/rack_adapter.rbs +21 -11
- data/sig/http3/request.rbs +5 -3
- data/sig/http3/response.rbs +2 -2
- data/sig/http3/server.rbs +8 -1
- data/sig/http3/session.rbs +104 -25
- data/sig/http3/worker_pool.rbs +19 -0
- metadata +9 -6
- data/.clang-format +0 -1
- data/.vscode/extensions.json +0 -6
- data/.vscode/settings.json +0 -10
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|