quic 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 +6 -0
- data/ext/quic/connection_common.c +65 -12
- data/ext/quic/stream.c +25 -6
- data/ext/quic/stream.h +6 -0
- data/lib/quic/version.rb +1 -1
- data/sig/quic.rbs +8 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebac23fb2ffb37336b61ef65171597216d95ec7a0a43ecb3aabfda810fbb5046
|
|
4
|
+
data.tar.gz: c78df53ff6e4e56f11a7aa3ae93bb1363090d58acf4e3f5b5b2290208b88fee9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34f56bd2d3d86c35bdcbe43e912f3b8ebd30454915332523b97a95a9306cb9febf0a7933f904f5fa3afc40b66014796523f59a6e5c0f47465b4c628db681b7f9
|
|
7
|
+
data.tar.gz: d00248c71fc9436bec13a5f258038512726c65427fe76315347ba40db071174d1c1b47dbfac02e1d7d46fb4c2e161e190cbaf9cbcf0c947a139ee5162cf2334d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.0.5] - 2026-10-24
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- When the peer sends STOP_SENDING on a stream, what was queued on it but not yet sent is dropped, and `QUIC::Stream#write` and `#write_nonblock` raise `QUIC::Error::StreamClosed` carrying the STOP_SENDING's error code, a `#write` waiting for the peer's window included. Before, every `#write_pkt` after it raised `QUIC::Error::StreamClosed` while the stream had bytes left to send, and a waiting `#write` returned only at the idle timeout. ([#21](https://github.com/unasuke/quic-ruby/pull/21))
|
|
5
|
+
- `#write_pkt` no longer raises `QUIC::Error::StreamClosed` after `QUIC::Stream#reset` on a stream with bytes queued; those bytes are dropped. ([#21](https://github.com/unasuke/quic-ruby/pull/21))
|
|
6
|
+
|
|
1
7
|
## [0.0.4] - 2026-10-22
|
|
2
8
|
|
|
3
9
|
### Added
|
|
@@ -508,7 +508,9 @@ static VALUE quic_acked_stream_data_offset_body(VALUE arg) {
|
|
|
508
508
|
/* Shift any head Chunks of @pending_chunks whose end-offset is fully
|
|
509
509
|
covered by acked_offset. Each Chunk's start-offset in the stream is
|
|
510
510
|
pending_shifted (after previous shifts); its end-offset is
|
|
511
|
-
pending_shifted + RSTRING_LEN(head).
|
|
511
|
+
pending_shifted + RSTRING_LEN(head). A stopped stream's
|
|
512
|
+
@pending_chunks is empty, and acks for what it sent before shift
|
|
513
|
+
nothing. */
|
|
512
514
|
VALUE pending = rb_ivar_get(stream, rb_intern("@pending_chunks"));
|
|
513
515
|
while (RARRAY_LEN(pending) > 0) {
|
|
514
516
|
VALUE head = RARRAY_AREF(pending, 0);
|
|
@@ -626,9 +628,51 @@ static int quic_stream_reset_cb(ngtcp2_conn *conn, int64_t stream_id,
|
|
|
626
628
|
return quic_conn_protect(c, quic_stream_reset_body, &a);
|
|
627
629
|
}
|
|
628
630
|
|
|
631
|
+
/* Nothing queued on stream can go out any more: drop it all, and have
|
|
632
|
+
#write_pkt skip the stream from now on. ngtcp2 retransmits no STREAM
|
|
633
|
+
frame of a stream it has reset, so it no longer reads the bytes it was
|
|
634
|
+
given either. */
|
|
635
|
+
static void quic_stream_stop(VALUE stream) {
|
|
636
|
+
quic_stream_t *s;
|
|
637
|
+
TypedData_Get_Struct(stream, quic_stream_t, &quic_stream_data_type, s);
|
|
638
|
+
s->stopped = true;
|
|
639
|
+
rb_ary_clear(rb_ivar_get(stream, rb_intern("@pending_chunks")));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
static VALUE quic_recv_stop_sending_body(VALUE arg) {
|
|
643
|
+
struct quic_stream_cb_args *a = (struct quic_stream_cb_args *)arg;
|
|
644
|
+
VALUE stream = quic_conn_lookup_stream(a->c, a->stream_id);
|
|
645
|
+
if (NIL_P(stream))
|
|
646
|
+
return Qnil;
|
|
647
|
+
|
|
648
|
+
quic_stream_t *s;
|
|
649
|
+
TypedData_Get_Struct(stream, quic_stream_t, &quic_stream_data_type, s);
|
|
650
|
+
s->stop_app_error_code = a->app_error_code;
|
|
651
|
+
quic_stream_stop(stream);
|
|
652
|
+
return Qnil;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* The peer sent STOP_SENDING. ngtcp2 shuts the send side and sends
|
|
656
|
+
RESET_STREAM itself once this returns. A stream handler learns of it when
|
|
657
|
+
#write_pkt finds the send side shut (stream_shut_write). */
|
|
658
|
+
static int quic_recv_stop_sending_cb(ngtcp2_conn *conn, int64_t stream_id,
|
|
659
|
+
uint64_t app_error_code, void *user_data,
|
|
660
|
+
void *stream_user_data) {
|
|
661
|
+
(void)conn;
|
|
662
|
+
(void)stream_user_data;
|
|
663
|
+
quic_conn_t *c = (quic_conn_t *)user_data;
|
|
664
|
+
if (c->handler)
|
|
665
|
+
return 0;
|
|
666
|
+
struct quic_stream_cb_args a = {
|
|
667
|
+
.c = c, .stream_id = stream_id, .app_error_code = app_error_code};
|
|
668
|
+
return quic_conn_protect(c, quic_recv_stop_sending_body, &a);
|
|
669
|
+
}
|
|
670
|
+
|
|
629
671
|
/* The callbacks below matter only to a stream handler. Without one, the
|
|
630
672
|
QUIC::Stream mode needs none of them. */
|
|
631
673
|
|
|
674
|
+
/* This endpoint stopped reading the stream, and STOP_SENDING is going out.
|
|
675
|
+
Not to be confused with recv_stop_sending above. */
|
|
632
676
|
static int quic_stream_stop_sending_cb(ngtcp2_conn *conn, int64_t stream_id,
|
|
633
677
|
uint64_t app_error_code, void *user_data,
|
|
634
678
|
void *stream_user_data) {
|
|
@@ -701,6 +745,7 @@ void quic_conn_fill_common_callbacks(ngtcp2_callbacks *callbacks) {
|
|
|
701
745
|
callbacks->acked_stream_data_offset = quic_acked_stream_data_offset_cb;
|
|
702
746
|
callbacks->stream_close = quic_stream_close_cb;
|
|
703
747
|
callbacks->stream_reset = quic_stream_reset_cb;
|
|
748
|
+
callbacks->recv_stop_sending = quic_recv_stop_sending_cb;
|
|
704
749
|
callbacks->stream_stop_sending = quic_stream_stop_sending_cb;
|
|
705
750
|
callbacks->extend_max_stream_data = quic_extend_max_stream_data_cb;
|
|
706
751
|
callbacks->extend_max_remote_streams_bidi =
|
|
@@ -708,11 +753,12 @@ void quic_conn_fill_common_callbacks(ngtcp2_callbacks *callbacks) {
|
|
|
708
753
|
callbacks->handshake_completed = quic_handshake_completed_cb;
|
|
709
754
|
}
|
|
710
755
|
|
|
711
|
-
/* Find the first stream in @streams that has either
|
|
712
|
-
pending FIN to flush. Phase 4 minimum: linear scan,
|
|
713
|
-
(no round-robin). Sets *out_datav to the byte slice
|
|
714
|
-
to 0 or 1, *out_flags to
|
|
715
|
-
Returns the stream's Ruby
|
|
756
|
+
/* Find the first stream in @streams, stopped ones aside, that has either
|
|
757
|
+
pending bytes or a pending FIN to flush. Phase 4 minimum: linear scan,
|
|
758
|
+
take the first match (no round-robin). Sets *out_datav to the byte slice
|
|
759
|
+
to send, *out_datavcnt to 0 or 1, *out_flags to
|
|
760
|
+
NGTCP2_WRITE_STREAM_FLAG_FIN when applicable. Returns the stream's Ruby
|
|
761
|
+
VALUE (or Qnil if no candidate). */
|
|
716
762
|
static VALUE quic_conn_pick_send_stream(VALUE self, ngtcp2_vec *out_datav,
|
|
717
763
|
size_t *out_datavcnt,
|
|
718
764
|
uint32_t *out_flags,
|
|
@@ -724,7 +770,7 @@ static VALUE quic_conn_pick_send_stream(VALUE self, ngtcp2_vec *out_datav,
|
|
|
724
770
|
VALUE st = RARRAY_AREF(stream_values, i);
|
|
725
771
|
quic_stream_t *s;
|
|
726
772
|
TypedData_Get_Struct(st, quic_stream_t, &quic_stream_data_type, s);
|
|
727
|
-
if (s->fin_flushed)
|
|
773
|
+
if (s->fin_flushed || s->stopped)
|
|
728
774
|
continue;
|
|
729
775
|
|
|
730
776
|
VALUE pending = rb_ivar_get(st, rb_intern("@pending_chunks"));
|
|
@@ -794,11 +840,18 @@ static ngtcp2_ssize quic_conn_write_pkt_streams(VALUE self, quic_conn_t *c,
|
|
|
794
840
|
c->conn, path, pi, dest, destlen, &pdatalen, writev_flags, send_stream_id,
|
|
795
841
|
&datav, datavcnt, c->now);
|
|
796
842
|
|
|
797
|
-
if (n == NGTCP2_ERR_STREAM_DATA_BLOCKED
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
peer extends its window (MAX_STREAM_DATA / MAX_DATA).
|
|
843
|
+
if ((n == NGTCP2_ERR_STREAM_DATA_BLOCKED || n == NGTCP2_ERR_STREAM_SHUT_WR) &&
|
|
844
|
+
NIL_P(c->pending_exc)) {
|
|
845
|
+
/* DATA_BLOCKED: the stream's queued data is beyond what the peer's flow
|
|
846
|
+
control lets through right now. Keep it queued; it follows once the
|
|
847
|
+
peer extends its window (MAX_STREAM_DATA / MAX_DATA).
|
|
848
|
+
SHUT_WR: ngtcp2 shut the stream's send side without recv_stop_sending
|
|
849
|
+
marking the stream, as #reset does. What is queued can never go out,
|
|
850
|
+
so drop it.
|
|
851
|
+
Either way, build the packet without the stream's data, so that ACKs
|
|
852
|
+
and other frames still go out. */
|
|
853
|
+
if (n == NGTCP2_ERR_STREAM_SHUT_WR)
|
|
854
|
+
quic_stream_stop(selected_stream);
|
|
802
855
|
selected_stream = Qnil;
|
|
803
856
|
datavcnt = 0;
|
|
804
857
|
writev_flags = 0;
|
data/ext/quic/stream.c
CHANGED
|
@@ -45,6 +45,19 @@ quic_stream_new(int64_t stream_id, VALUE owner) {
|
|
|
45
45
|
return self;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/* Raise QUIC::Error::StreamClosed if nothing more can be written to the
|
|
49
|
+
stream: it was reset or has closed, or the peer sent STOP_SENDING. */
|
|
50
|
+
static void quic_stream_check_writable(quic_stream_t *s) {
|
|
51
|
+
if (s->reset || s->closed) {
|
|
52
|
+
rb_raise(rb_eQUICErrorStreamClosed, "stream is closed");
|
|
53
|
+
}
|
|
54
|
+
if (s->stopped) {
|
|
55
|
+
rb_raise(rb_eQUICErrorStreamClosed,
|
|
56
|
+
"the peer asked to stop sending (error code %llu)",
|
|
57
|
+
(unsigned long long)s->stop_app_error_code);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
/* Append a binary copy of |data| to @pending_chunks and optionally set the
|
|
49
62
|
fin_sent flag. Flow control is the caller's concern (#write blocks in
|
|
50
63
|
quic_stream_write_m and #write_nonblock returns a partial byte count in
|
|
@@ -55,9 +68,7 @@ static VALUE quic_stream_enqueue(VALUE self, VALUE data, bool fin) {
|
|
|
55
68
|
quic_stream_t *s;
|
|
56
69
|
TypedData_Get_Struct(self, quic_stream_t, &quic_stream_data_type, s);
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
rb_raise(rb_eQUICErrorStreamClosed, "stream is closed");
|
|
60
|
-
}
|
|
71
|
+
quic_stream_check_writable(s);
|
|
61
72
|
if (s->fin_sent) {
|
|
62
73
|
rb_raise(rb_eQUICErrorStreamClosed, "stream FIN already sent");
|
|
63
74
|
}
|
|
@@ -94,7 +105,9 @@ static bool quic_stream_fin_kwarg(int argc, VALUE *argv, VALUE *data) {
|
|
|
94
105
|
per-stream and the per-connection flow control limits ngtcp2 tracks, less
|
|
95
106
|
the bytes already queued in @pending_chunks but not yet handed to ngtcp2.
|
|
96
107
|
ngtcp2 only counts what it has sent, so without the subtraction a run of
|
|
97
|
-
writes could queue far more than the peer allows.
|
|
108
|
+
writes could queue far more than the peer allows. Callers check the
|
|
109
|
+
stream is writable first: a stopped stream's @pending_chunks no longer
|
|
110
|
+
matches its offsets. */
|
|
98
111
|
static uint64_t quic_stream_window_left(VALUE self, VALUE client_v,
|
|
99
112
|
quic_stream_t *s) {
|
|
100
113
|
ngtcp2_conn *conn = quic_conn_ptr(client_v);
|
|
@@ -117,8 +130,10 @@ static uint64_t quic_stream_window_left(VALUE self, VALUE client_v,
|
|
|
117
130
|
/* Blocking write: block by repeatedly invoking #pump_once on the owning
|
|
118
131
|
connection (@client) until the peer's flow control window has enough room
|
|
119
132
|
for the full payload, then enqueue. IO#write-compatible: always queues all
|
|
120
|
-
of `data`.
|
|
121
|
-
|
|
133
|
+
of `data`. A window that can no longer open, because the peer sent
|
|
134
|
+
STOP_SENDING or the stream ended meanwhile, raises
|
|
135
|
+
QUIC::Error::StreamClosed. Bare Streams (built via QUIC::Stream.allocate
|
|
136
|
+
for unit tests, with @client = nil) skip the window check entirely. */
|
|
122
137
|
static VALUE quic_stream_write_m(int argc, VALUE *argv, VALUE self) {
|
|
123
138
|
VALUE data;
|
|
124
139
|
bool fin = quic_stream_fin_kwarg(argc, argv, &data);
|
|
@@ -139,8 +154,10 @@ static VALUE quic_stream_write_m(int argc, VALUE *argv, VALUE self) {
|
|
|
139
154
|
/* Loop until the full payload would fit in the current window. pump_once
|
|
140
155
|
raises QUIC::Error::NotBound if the connection has not been #bind'ed;
|
|
141
156
|
that error surfaces verbatim. */
|
|
157
|
+
quic_stream_check_writable(s);
|
|
142
158
|
while ((uint64_t)needed > quic_stream_window_left(self, client_v, s)) {
|
|
143
159
|
rb_funcall(client_v, rb_intern("pump_once"), 0);
|
|
160
|
+
quic_stream_check_writable(s);
|
|
144
161
|
}
|
|
145
162
|
}
|
|
146
163
|
|
|
@@ -172,6 +189,8 @@ static VALUE quic_stream_write_nonblock_m(int argc, VALUE *argv, VALUE self) {
|
|
|
172
189
|
|
|
173
190
|
quic_stream_t *s;
|
|
174
191
|
TypedData_Get_Struct(self, quic_stream_t, &quic_stream_data_type, s);
|
|
192
|
+
/* Before the window: a stream that cannot be written to never gets one. */
|
|
193
|
+
quic_stream_check_writable(s);
|
|
175
194
|
uint64_t avail = quic_stream_window_left(self, client_v, s);
|
|
176
195
|
|
|
177
196
|
if (avail == 0) {
|
data/ext/quic/stream.h
CHANGED
|
@@ -29,6 +29,12 @@ typedef struct {
|
|
|
29
29
|
bool closed; /* stream_close callback fired */
|
|
30
30
|
uint64_t close_app_error_code;
|
|
31
31
|
bool close_has_app_error_code;
|
|
32
|
+
/* The send side is shut: the peer's STOP_SENDING arrived, or ngtcp2
|
|
33
|
+
reported the send side shut while writing. @pending_chunks has been
|
|
34
|
+
emptied, so the offsets above no longer describe it, and #write_pkt
|
|
35
|
+
skips the stream. */
|
|
36
|
+
bool stopped;
|
|
37
|
+
uint64_t stop_app_error_code; /* the STOP_SENDING's, 0 if none arrived */
|
|
32
38
|
/* Set once a peer-initiated stream has been pushed onto the owning
|
|
33
39
|
connection's @accept_queue, so recv_stream_data does not enqueue it
|
|
34
40
|
again on subsequent data arrivals. */
|
data/lib/quic/version.rb
CHANGED
data/sig/quic.rbs
CHANGED
|
@@ -323,15 +323,19 @@ module QUIC
|
|
|
323
323
|
# Server) until the peer's flow control window has room for the full
|
|
324
324
|
# payload, then queues all bytes and returns data.bytesize. Requires the
|
|
325
325
|
# owning connection to be bound to a socket via #bind; raises
|
|
326
|
-
# QUIC::Error::NotBound otherwise.
|
|
326
|
+
# QUIC::Error::NotBound otherwise. Raises QUIC::Error::StreamClosed,
|
|
327
|
+
# while waiting too, once the stream has been reset or closed, or the
|
|
328
|
+
# peer has sent STOP_SENDING. STOP_SENDING also drops what was queued
|
|
329
|
+
# but not yet sent.
|
|
327
330
|
def write: (String data, ?fin: bool) -> Integer
|
|
328
331
|
|
|
329
332
|
# Non-blocking variant. Queues as much of `data` as the current flow
|
|
330
333
|
# control window allows and returns the byte count actually queued.
|
|
331
334
|
# Raises QUIC::Error::WaitWritable (mixed in with IO::WaitWritable) when
|
|
332
|
-
# the window is zero and the payload is non-empty
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
+
# the window is zero and the payload is non-empty, and
|
|
336
|
+
# QUIC::Error::StreamClosed as #write does. If only a partial prefix is
|
|
337
|
+
# accepted, `fin: true` is NOT propagated; the caller must re-issue with
|
|
338
|
+
# `fin: true` on the trailing chunk.
|
|
335
339
|
def write_nonblock: (String data, ?fin: bool) -> Integer
|
|
336
340
|
def eof?: () -> bool
|
|
337
341
|
|