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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e019da100b72501ae5f3f097aa00344e763de5483e301b03e619c695e7ed269d
4
- data.tar.gz: 922faa6c8b6b96e121849d4f17823e31cbfdeb05da3d93681603c3fb55840127
3
+ metadata.gz: ebac23fb2ffb37336b61ef65171597216d95ec7a0a43ecb3aabfda810fbb5046
4
+ data.tar.gz: c78df53ff6e4e56f11a7aa3ae93bb1363090d58acf4e3f5b5b2290208b88fee9
5
5
  SHA512:
6
- metadata.gz: c1eb1754a7e98f652825f641d0a1bfb2c131d54fcaceffe2867b8000107752d39c7fcc9e739125085c84bb76272306213e5774730e408b4d6cfc2bb6ce61b5c9
7
- data.tar.gz: 365881fb77b46679c549f6e19dc5988cd94f62a3ab13ee937e0514d12202a45a63c17501c6bf492b6363b2b33c81894651965f74425e44af37cc2e24f37c0aa5
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 pending bytes or a
712
- pending FIN to flush. Phase 4 minimum: linear scan, take the first match
713
- (no round-robin). Sets *out_datav to the byte slice to send, *out_datavcnt
714
- to 0 or 1, *out_flags to NGTCP2_WRITE_STREAM_FLAG_FIN when applicable.
715
- Returns the stream's Ruby VALUE (or Qnil if no candidate). */
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 && NIL_P(c->pending_exc)) {
798
- /* The stream's queued data is beyond what the peer's flow control lets
799
- through right now. Keep it queued and build the packet without it, so
800
- that ACKs and other frames still go out; the data follows once the
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
- if (s->reset || s->closed) {
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`. Bare Streams (built via QUIC::Stream.allocate for unit tests,
121
- with @client = nil) skip the window check entirely. */
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QUIC
4
- VERSION = "0.0.4"
4
+ VERSION = "0.0.5"
5
5
  end
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. If only a partial
333
- # prefix is accepted, `fin: true` is NOT propagated; the caller must
334
- # re-issue with `fin: true` on the trailing chunk.
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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Nakamura