uringmachine 0.29.0 → 0.29.2

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: 6a6a6d78b632e43c1ac15e8745ede898e531ff9a731822fd22c5991ca0222d69
4
- data.tar.gz: 0acd35ee028ea519c2788c720336dbcb1c2fa8317912459b7a81f4afbe6b3747
3
+ metadata.gz: 6151ac8411facd0ef37fa66a7cae623d2137f335bbe1e178529cb0a76c9f56c3
4
+ data.tar.gz: 5918fde9ea4ba936760a3d402207bd6fd5806337b005cd3633ab1cf0f8584ed0
5
5
  SHA512:
6
- metadata.gz: ed9f7684132ee1e1aae9c7783131fd927e88604444d44bc889cdf8c362b0a5dd85b3e887873c0512946af2c08dd8189e6c3bf972f19f90a7f12daee4c44c5ccd
7
- data.tar.gz: f5bff069de535e906543a679266700bacbb715d0e72b14d4bf7cd042ab8d77e58d8c69feb1dbd65f16026aa4bcec20d024acffd2e2f18ac255f5b70190c25788
6
+ metadata.gz: 0d77a7f37251930db32ec8023b1cb60e5595348544b279b175d39a528dbe782a703c75f6ebde5b2dd84379ec12ae83decf47f11f0b692e4975a36f146b4bc975
7
+ data.tar.gz: 190d3d88d32f91241c3c9ed3fc674d5318fcf630e64d5d5fdecdc7a86b6e62f03786190a021f4828e6a40022ccbf301a3cffdf0311ff35bc747889d9f00fbb9b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 0.29.2 2026-03-15
2
+
3
+ - Add `Stream#consumed`, `Stream#pending`
4
+ - Add `UM#stream`
5
+ - Add `Stream#skip`
6
+
7
+ # 0.29.1 2026-03-14
8
+
9
+ - Add support for exception instance in `#timeout`
10
+ - Improve buffer commit level handling
11
+
1
12
  # 0.29.0 2026-03-11
2
13
 
3
14
  - Reimplement streams based on buffer pool
data/README.md CHANGED
@@ -32,11 +32,13 @@ implementation that allows integration with the entire Ruby ecosystem.
32
32
  ## Features
33
33
 
34
34
  - Automatic fiber switching when performing blocking I/O operations.
35
- - Automatic cancellation using of ongoing operations with Ruby exceptions.
35
+ - Automatic cancellation ongoing operations with Ruby exceptions.
36
36
  - General-purpose API for cancelling any operation on timeout.
37
37
  - Excellent performance characteristics for concurrent I/O-bound applications.
38
38
  - `Fiber::Scheduler` implementation to automatically integrate with the Ruby
39
39
  ecosystem in a transparent fashion.
40
+ - Read streams with automatic buffer management.
41
+ - Optimized I/O for encrypted SSL connections.
40
42
 
41
43
  ## Design
42
44
 
@@ -61,8 +63,8 @@ allow any library that performs I/O using standard-library classes such as `IO`,
61
63
 
62
64
  To install UringMachine, simply run `gem install uringmachine` or `bundle add
63
65
  uringmachine` in your project directory. Note: to use UringMachine, you'll need
64
- a Linux machine with a minimum kernel version of 6.7. Some features require
65
- newer kernel versions.
66
+ a Linux machine with a minimum Linux kernel version of 6.7. Some features
67
+ require newer kernel versions.
66
68
 
67
69
  To perform I/O using UringMachine, simply create an instance:
68
70
 
@@ -79,7 +81,7 @@ You can perform I/O by directly making method calls such as `write` or `read`
79
81
  ```ruby
80
82
  # Most UringMachine instance methods will need you to provide a file descriptor.
81
83
  # Here we print a message to STDOUT. Note the explicit line break:
82
- machine.write(STDOUT, "Hello, world!\n")
84
+ machine.write(UM::STDOUT_FILENO, "Hello, world!\n")
83
85
  ```
84
86
 
85
87
  UringMachine provides an I/O interface that is to a large degree equivalent to
@@ -91,14 +93,14 @@ the Unix standard C interface:
91
93
  fd = machine.open('foo.txt', UM::O_RDONLY)
92
94
  buf = +''
93
95
  size = machine.read(fd, buf, 8192)
94
- machine.write(STDOUT, "File content: #{buf.inspect}")
96
+ machine.write(UM::STDOUT_FILENO, "File content: #{buf.inspect}")
95
97
  machine.close(fd)
96
98
 
97
99
  # Or alternatively (with automatic file closing):
98
100
  machine.open('foo.txt', UM::O_RDONLY) do |fd|
99
101
  buf = +''
100
102
  size = machine.read(fd, buf, 8192)
101
- machine.write(STDOUT, "File content: #{buf.inspect}")
103
+ machine.write(UM::STDOUT_FILENO, "File content: #{buf.inspect}")
102
104
  end
103
105
  ```
104
106
 
@@ -284,6 +286,104 @@ fiber = Fiber.schedule do
284
286
  end
285
287
  ```
286
288
 
289
+ ## Read Streams
290
+
291
+ A UringMachine stream is used to efficiently read from a socket or other file
292
+ descriptor. Streams are ideal for implementing the read side of protocols, and
293
+ provide an API that is useful for both line-based protocols and binary
294
+ (frame-based) protocols.
295
+
296
+ A stream is associated with a UringMachine instance and a target file descriptor
297
+ (see also [stream modes](#stream-modes) below). Behind the scenes, streams take
298
+ advantage of io_uring's registered buffers feature, and more recently, the
299
+ introduction of [incremental buffer
300
+ consumption](https://github.com/axboe/liburing/wiki/What's-new-with-io_uring-in-6.11-and-6.12#incremental-provided-buffer-consumption).
301
+
302
+ When streams are used, UringMachine automatically manages the buffers it
303
+ provides to the kernel, maximizing buffer reuse and minimizing allocations.
304
+ UringMachine also responds to stress conditions (increased incoming traffic) by
305
+ automatically provisioning additional buffers.
306
+
307
+ To create a stream for a given fd, use `UM#stream`:
308
+
309
+ ```ruby
310
+ stream = machine.stream(fd)
311
+
312
+ # you can also provide a block that will be passed the stream instance:
313
+ machine.stream(fd) { |s| do_something_with(s) }
314
+
315
+ # you can also instantiate a stream directly:
316
+ stream = UM::Stream.new(machine, fd)
317
+ ```
318
+
319
+ The following API is used to interact with the stream:
320
+
321
+ ```ruby
322
+ # Read until a newline character is encountered:
323
+ line = stream.get_line(0)
324
+
325
+ # Read line with a maximum length of 13 bytes:
326
+ line = stream.get_line(13)
327
+
328
+ # Read all data:
329
+ buf = stream.get_string(0)
330
+
331
+ # Read exactly 13 bytes:
332
+ buf = stream.get_string(13)
333
+
334
+ # Read up to 13 bytes:
335
+ buf = stream.get_string(-13)
336
+
337
+ # Skip 3 bytes:
338
+ stream.skip(3)
339
+ ```
340
+
341
+ Here's an example of a how a basic HTTP request parser might be implemented
342
+ using a stream:
343
+
344
+ ```ruby
345
+ def parse_http_request_headers(stream)
346
+ request_line = stream.get_line(0)
347
+ m = request_line.match(REQUEST_LINE_RE)
348
+ return nil if !m
349
+
350
+ headers = {
351
+ ':method' => m[1],
352
+ ':path' => m[2],
353
+ ':protocol' => m[3]
354
+ }
355
+
356
+ while true
357
+ line = stream.get_line(0)
358
+ break if !line || line.empty?
359
+
360
+ m = line.match(HEADER_RE)
361
+ headers[m[1].downcase] = m[2]
362
+ end
363
+ headers
364
+ end
365
+ ```
366
+
367
+ ### Stream modes
368
+
369
+ Stream modes allow streams to be transport agnostic. Currently streams support
370
+ three modes:
371
+
372
+ - `:bp_read` - use the buffer pool, read data using multishot read
373
+ (this is the default mode).
374
+ - `:bp_recv` - use the buffer pool, read data using multishot recv.
375
+ - `:ssl` - read from an `SSLSocket` object.
376
+
377
+ The mode is specified as an additional argument to `Stream.new`:
378
+
379
+ ```ruby
380
+ # stream using recv:
381
+ stream = machine.stream(fd, :bp_recv)
382
+
383
+ # stream on an SSL socket:
384
+ stream = machine.stream(ssl, :ssl)
385
+ ```
386
+
287
387
  ## Performance
288
388
 
289
389
  [Detailed benchmarks](benchmark/README.md)
data/TODO.md CHANGED
@@ -1,6 +1,5 @@
1
1
  ## immediate
2
2
 
3
- - Add support for exception instances in `#timeout`.
4
3
  - Add support for returning a value on timeout
5
4
 
6
5
  Since to do this safely we need to actually raise an exception that wraps the
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'uringmachine', path: '..'
8
+ gem 'benchmark'
9
+ end
10
+
11
+ require 'benchmark'
12
+ require 'uringmachine'
13
+
14
+ C = 10
15
+ N = 1000
16
+
17
+ CMD = <<~EOF
18
+ bash -c "for i in {1..#{C*2}}; do nc -l -p 1234 </dev/random & done; wait $(jobs -p)"
19
+ EOF
20
+
21
+ def start_server
22
+ @pid = fork {
23
+ p :server_launch
24
+ `#{CMD}`
25
+ puts
26
+ p :server_done
27
+ puts
28
+ }
29
+ sleep(0.5)
30
+ end
31
+
32
+ def stop_server
33
+ Process.kill('SIGINT', @pid)
34
+ Process.wait(@pid)
35
+ end
36
+
37
+ def io_gets
38
+ start_server
39
+ tt = C.times.map {
40
+ Thread.new do
41
+ s = TCPSocket.new('localhost', 1234)
42
+ # io = File.open('/dev/random', 'r')
43
+ N.times { s.gets }
44
+ ensure
45
+ s.close
46
+ end
47
+ }
48
+ tt.each(&:join)
49
+ ensure
50
+ stop_server
51
+ end
52
+
53
+ @machine = UM.new
54
+
55
+ def buf_gets(fd, buffer)
56
+ while true
57
+ idx = buffer.byteindex("\n")
58
+ if idx
59
+ line = buffer[0..(idx - 1)]
60
+
61
+ buffer = buffer[(idx + 1)..-1]
62
+ return line
63
+ end
64
+ @machine.read(fd, buffer, 65536, -1)
65
+ end
66
+ end
67
+
68
+ def um_read
69
+ start_server
70
+ ff = C.times.map {
71
+ @machine.spin do
72
+ # fd = @machine.open('/dev/random', UM::O_RDONLY)
73
+ fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
74
+ @machine.connect(fd, '127.0.0.1', 1234)
75
+ buffer = +''.encode(Encoding::US_ASCII)
76
+ N.times { buf_gets(fd, buffer) }
77
+ ensure
78
+ @machine.close(fd)
79
+ end
80
+ }
81
+ @machine.await(ff)
82
+ ensure
83
+ stop_server
84
+ end
85
+
86
+ @total_stream = 0
87
+ def um_stream_do
88
+ # fd = @machine.open('/dev/random', UM::O_RDONLY)
89
+ fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
90
+ @machine.connect(fd, '127.0.0.1', 1234)
91
+ stream = UM::Stream.new(@machine, fd)
92
+ N.times { @total_stream += stream.get_line(0)&.bytesize || 0 }
93
+ rescue => e
94
+ p e
95
+ p e.backtrace
96
+ ensure
97
+ stream.clear
98
+ @machine.close(fd)
99
+ end
100
+
101
+ def um_stream
102
+ start_server
103
+ ff = C.times.map {
104
+ @machine.snooze
105
+ @machine.spin { um_stream_do }
106
+ }
107
+ @machine.await(ff)
108
+ pp total: @total_stream
109
+ ensure
110
+ stop_server
111
+ end
112
+
113
+ p(C:, N:)
114
+ um_stream
115
+ pp @machine.metrics
116
+ exit
117
+
118
+ Benchmark.bm do
119
+ it.report('Thread/IO#gets') { io_gets }
120
+ it.report('Fiber/UM#read+buf') { um_read }
121
+ it.report('Fiber/UM::Stream') { um_stream }
122
+ end
@@ -57,7 +57,7 @@ buffers, to using managed buffers from the buffer pool.
57
57
 
58
58
  - The buffer pool is created and managed automatically. No API is involved.
59
59
  -
60
- -
60
+ -
61
61
 
62
62
  - To use the buffer pool, two dedicated APIs are added:
63
63
 
data/ext/um/um.c CHANGED
@@ -540,15 +540,19 @@ VALUE um_timeout_complete(VALUE arg) {
540
540
  return Qnil;
541
541
  }
542
542
 
543
- VALUE um_timeout(struct um *machine, VALUE interval, VALUE class) {
543
+ VALUE um_timeout(struct um *machine, VALUE interval, VALUE obj) {
544
544
  static ID ID_new = 0;
545
- if (unlikely(!ID_new)) ID_new = rb_intern("new");
545
+
546
+ if (TYPE(obj) == T_CLASS) {
547
+ if (unlikely(!ID_new)) ID_new = rb_intern("new");
548
+ obj = rb_funcall(obj, ID_new, 0);
549
+ }
546
550
 
547
551
  struct um_op *op = um_op_acquire(machine);
548
552
  um_prep_op(machine, op, OP_TIMEOUT, 2, 0);
549
553
  op->ts = um_double_to_timespec(NUM2DBL(interval));
550
554
  RB_OBJ_WRITE(machine->self, &op->fiber, rb_fiber_current());
551
- RB_OBJ_WRITE(machine->self, &op->value, rb_funcall(class, ID_new, 0));
555
+ RB_OBJ_WRITE(machine->self, &op->value, obj);
552
556
  RB_OBJ_WRITE(machine->self, &op->async_op, Qnil);
553
557
 
554
558
  struct io_uring_sqe *sqe = um_get_sqe(machine, op);
@@ -556,6 +560,7 @@ VALUE um_timeout(struct um *machine, VALUE interval, VALUE class) {
556
560
 
557
561
  struct op_ctx ctx = { .machine = machine, .op = op };
558
562
  return rb_ensure(rb_yield, Qnil, um_timeout_complete, (VALUE)&ctx);
563
+ RB_GC_GUARD(obj);
559
564
  }
560
565
 
561
566
  /*******************************************************************************
data/ext/um/um.h CHANGED
@@ -111,12 +111,10 @@ enum um_stream_mode {
111
111
 
112
112
  #define BP_BGID 0xF00B
113
113
  #define BP_BR_ENTRIES 1024
114
-
115
114
  #define BP_INITIAL_BUFFER_SIZE (1U << 14) // 16KB
116
- #define BP_INITIAL_COMMIT_THRESHOLD (1U << 16) // 64KB
115
+ #define BP_INITIAL_COMMIT_LEVEL (BP_INITIAL_BUFFER_SIZE * 16) // 256KB
117
116
  #define BP_MAX_BUFFER_SIZE (1U << 20) // 1MB
118
-
119
- #define BP_MAX_COMMIT_THRESHOLD (BP_MAX_BUFFER_SIZE * BP_BR_ENTRIES) // 1GB
117
+ #define BP_MAX_COMMIT_LEVEL (BP_MAX_BUFFER_SIZE * BP_BR_ENTRIES) // 1GB
120
118
  #define BP_AVAIL_BID_BITMAP_WORDS (BP_BR_ENTRIES / 64)
121
119
 
122
120
  struct um_buffer {
@@ -163,7 +161,7 @@ struct um_op {
163
161
  struct iovec *iovecs; // used for vectorized write/send
164
162
  siginfo_t siginfo; // used for waitid
165
163
  int int_value; // used for getsockopt
166
- size_t bp_commit_threshold; // buffer pool commit threshold
164
+ size_t bp_commit_level; // buffer pool commit threshold
167
165
  };
168
166
  };
169
167
 
@@ -234,7 +232,7 @@ struct um {
234
232
 
235
233
  struct io_uring_buf_ring *bp_br;
236
234
  size_t bp_buffer_size;
237
- size_t bp_commit_threshold;
235
+ size_t bp_commit_level;
238
236
  struct um_buffer **bp_commited_buffers;
239
237
  uint64_t bp_avail_bid_bitmap[BP_AVAIL_BID_BITMAP_WORDS];
240
238
 
@@ -285,7 +283,8 @@ struct um_stream {
285
283
  struct um_op *op;
286
284
  struct um_segment *head;
287
285
  struct um_segment *tail;
288
- size_t pending_len;
286
+ size_t consumed_bytes;
287
+ size_t pending_bytes;
289
288
  size_t pos;
290
289
  int eof;
291
290
  };
@@ -437,7 +436,7 @@ void stream_teardown(struct um_stream *stream);
437
436
  void stream_clear(struct um_stream *stream);
438
437
  VALUE stream_get_line(struct um_stream *stream, VALUE buf, size_t maxlen);
439
438
  VALUE stream_get_string(struct um_stream *stream, VALUE out_buffer, ssize_t len, size_t inc, int safe_inc);
440
- VALUE stream_skip(struct um_stream *stream, size_t len);
439
+ void stream_skip(struct um_stream *stream, size_t inc, int safe_inc);
441
440
  VALUE resp_decode(struct um_stream *stream, VALUE out_buffer);
442
441
  void resp_encode(struct um_write_buffer *buf, VALUE obj);
443
442
  void resp_encode_cmd(struct um_write_buffer *buf, int argc, VALUE *argv);
@@ -31,7 +31,6 @@ inline struct um_buffer *bp_buffer_checkout(struct um *machine) {
31
31
  buffer->ref_count++;
32
32
  buffer->pos = 0;
33
33
  buffer->next = NULL;
34
-
35
34
  return buffer;
36
35
  }
37
36
 
@@ -76,7 +75,7 @@ inline void bp_setup(struct um *machine) {
76
75
  if (unlikely(!machine->bp_br)) rb_syserr_fail(ret, strerror(ret));
77
76
 
78
77
  machine->bp_buffer_size = BP_INITIAL_BUFFER_SIZE;
79
- machine->bp_commit_threshold = BP_INITIAL_COMMIT_THRESHOLD;
78
+ machine->bp_commit_level = BP_INITIAL_COMMIT_LEVEL;
80
79
  machine->bp_commited_buffers = malloc(sizeof(struct um_buffer) * BP_BR_ENTRIES);
81
80
  memset(machine->bp_commited_buffers, 0, sizeof(struct um_buffer) * BP_BR_ENTRIES);
82
81
  memset(machine->bp_avail_bid_bitmap, 0xFF, sizeof(machine->bp_avail_bid_bitmap));
@@ -160,10 +159,13 @@ inline struct um_buffer *get_buffer(struct um *machine, int bid) {
160
159
 
161
160
  inline int should_commit_more_p(struct um *machine) {
162
161
  return (machine->bp_buffer_count < BP_BR_ENTRIES) &&
163
- (machine->bp_total_commited < machine->bp_commit_threshold);
162
+ (machine->bp_total_commited < machine->bp_commit_level);
164
163
  }
165
164
 
166
165
  inline void bp_ensure_commit_level(struct um *machine) {
166
+ if (machine->bp_total_commited > (machine->bp_commit_level / 2))
167
+ return;
168
+
167
169
  int added = 0;
168
170
  while (should_commit_more_p(machine)) {
169
171
  if (likely(commit_buffer(machine, added))) added++;
@@ -178,11 +180,11 @@ inline void bp_ensure_commit_level(struct um *machine) {
178
180
  }
179
181
 
180
182
  inline void bp_handle_enobufs(struct um *machine) {
181
- if (unlikely(machine->bp_commit_threshold >= BP_MAX_COMMIT_THRESHOLD))
183
+ if (unlikely(machine->bp_commit_level >= BP_MAX_COMMIT_LEVEL))
182
184
  rb_raise(eUMError, "Buffer starvation");
183
185
 
184
- machine->bp_commit_threshold *= 2;
185
- while (machine->bp_buffer_size < machine->bp_commit_threshold / 4)
186
+ machine->bp_commit_level *= 2;
187
+ while (machine->bp_buffer_size < machine->bp_commit_level / 4)
186
188
  machine->bp_buffer_size *= 2;
187
189
  bp_discard_buffer_freelist(machine);
188
190
  }
data/ext/um/um_class.c CHANGED
@@ -334,17 +334,17 @@ VALUE UM_schedule(VALUE self, VALUE fiber, VALUE value) {
334
334
  }
335
335
 
336
336
  /* Runs the given block, interrupting its execution if its runtime exceeds the
337
- * given timeout interval (in seconds).
337
+ * given timeout interval (in seconds), raising the specified exception.
338
338
  *
339
339
  * - https://www.man7.org/linux/man-pages//man3/io_uring_prep_timeoute.3.html
340
340
  *
341
341
  * @param interval [Number] timeout interval in seconds
342
- * @param exception_class [any] timeout exception class
342
+ * @param exception [any] timeout exception class or instance
343
343
  * @return [any] block's return value
344
344
  */
345
- VALUE UM_timeout(VALUE self, VALUE interval, VALUE exception_class) {
345
+ VALUE UM_timeout(VALUE self, VALUE interval, VALUE exception) {
346
346
  struct um *machine = um_get_machine(self);
347
- return um_timeout(machine, interval, exception_class);
347
+ return um_timeout(machine, interval, exception);
348
348
  }
349
349
 
350
350
  /* Puts the current fiber to sleep for the given time duration (in seconds),
data/ext/um/um_stream.c CHANGED
@@ -9,7 +9,7 @@ inline void stream_add_segment(struct um_stream *stream, struct um_segment *segm
9
9
  }
10
10
  else
11
11
  stream->head = stream->tail = segment;
12
- stream->pending_len += segment->len;
12
+ stream->pending_bytes += segment->len;
13
13
  }
14
14
 
15
15
  inline int stream_process_op_result(struct um_stream *stream, struct um_op_result *result) {
@@ -50,7 +50,7 @@ void stream_multishot_op_start(struct um_stream *stream) {
50
50
  default:
51
51
  um_raise_internal_error("Invalid multishot op");
52
52
  }
53
- stream->op->bp_commit_threshold = stream->machine->bp_commit_threshold;
53
+ stream->op->bp_commit_level = stream->machine->bp_commit_level;
54
54
  }
55
55
 
56
56
  void stream_multishot_op_stop(struct um_stream *stream) {
@@ -73,7 +73,7 @@ void um_stream_cleanup(struct um_stream *stream) {
73
73
  um_segment_checkin(stream->machine, stream->head);
74
74
  stream->head = next;
75
75
  }
76
- stream->pending_len = 0;
76
+ stream->pending_bytes = 0;
77
77
  }
78
78
 
79
79
  // returns true if case of ENOBUFS error, sets more to true if more data forthcoming
@@ -124,7 +124,7 @@ void stream_clear(struct um_stream *stream) {
124
124
  um_segment_checkin(stream->machine, stream->head);
125
125
  stream->head = next;
126
126
  }
127
- stream->pending_len = 0;
127
+ stream->pending_bytes = 0;
128
128
 
129
129
  if (stream->working_buffer) {
130
130
  bp_buffer_checkin(stream->machine, stream->working_buffer);
@@ -156,16 +156,27 @@ int stream_get_more_segments_bp(struct um_stream *stream) {
156
156
  enobufs = stream_process_segments(stream, &total_bytes, &more);
157
157
  um_op_multishot_results_clear(stream->machine, stream->op);
158
158
  if (unlikely(enobufs)) {
159
- int should_restart = stream->pending_len < (stream->machine->bp_buffer_size * 4);
159
+ int should_restart = stream->pending_bytes < (stream->machine->bp_buffer_size * 4);
160
+ // int same_threshold = stream->op->bp_commit_level == stream->machine->bp_commit_level;
161
+
162
+ // fprintf(stderr, "%p enobufs total: %ld pending: %ld threshold: %ld bc: %d (same: %d, restart: %d)\n",
163
+ // stream,
164
+ // total_bytes, stream->pending_bytes, stream->machine->bp_commit_level,
165
+ // stream->machine->bp_buffer_count,
166
+ // same_threshold, should_restart
167
+ // );
160
168
 
161
169
  // If multiple stream ops are happening at the same time, they'll all get
162
170
  // ENOBUFS! We track the commit threshold in the op in order to prevent
163
171
  // running bp_handle_enobufs() more than once.
164
172
 
165
173
  if (should_restart) {
166
- if (stream->op->bp_commit_threshold == stream->machine->bp_commit_threshold)
174
+ if (stream->op->bp_commit_level == stream->machine->bp_commit_level)
167
175
  bp_handle_enobufs(stream->machine);
168
- stream_multishot_op_start(stream);
176
+
177
+ um_op_release(stream->machine, stream->op);
178
+ stream->op = NULL;
179
+ // stream_multishot_op_start(stream);
169
180
  }
170
181
  else {
171
182
  um_op_release(stream->machine, stream->op);
@@ -215,6 +226,49 @@ int stream_get_more_segments(struct um_stream *stream) {
215
226
 
216
227
  ///////////////////////////////////////////////////////////////////////////////////////
217
228
 
229
+ inline void stream_shift_head(struct um_stream *stream) {
230
+ struct um_segment *consumed = stream->head;
231
+ stream->head = consumed->next;
232
+ if (!stream->head) stream->tail = NULL;
233
+ um_segment_checkin(stream->machine, consumed);
234
+ stream->pos = 0;
235
+ }
236
+
237
+ inline void stream_skip(struct um_stream *stream, size_t inc, int safe_inc) {
238
+ if (unlikely(stream->eof && !stream->head)) return;
239
+ if (safe_inc && !stream->tail && !stream_get_more_segments(stream)) return;
240
+
241
+ while (inc) {
242
+ size_t segment_len = stream->head->len - stream->pos;
243
+ size_t inc_len = (segment_len <= inc) ? segment_len : inc;
244
+ inc -= inc_len;
245
+ stream->pos += inc_len;
246
+ stream->consumed_bytes += inc_len;
247
+ stream->pending_bytes -= inc_len;
248
+ if (stream->pos == stream->head->len) {
249
+ stream_shift_head(stream);
250
+ if (inc && safe_inc && !stream->head) {
251
+ if (!stream_get_more_segments(stream)) break;
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ inline void stream_copy(struct um_stream *stream, char *dest, size_t len) {
258
+ while (len) {
259
+ char *segment_ptr = stream->head->ptr + stream->pos;
260
+ size_t segment_len = stream->head->len - stream->pos;
261
+ size_t cpy_len = (segment_len <= len) ? segment_len : len;
262
+ memcpy(dest, segment_ptr, cpy_len);
263
+
264
+ len -= cpy_len;
265
+ stream->pos += cpy_len;
266
+ stream->consumed_bytes += cpy_len;
267
+ stream->pending_bytes -= cpy_len;
268
+ dest += cpy_len;
269
+ if (stream->pos == stream->head->len) stream_shift_head(stream);
270
+ }
271
+ }
218
272
 
219
273
  VALUE stream_consume_string(struct um_stream *stream, VALUE out_buffer, size_t len, size_t inc, int safe_inc) {
220
274
  VALUE str = Qnil;
@@ -228,69 +282,22 @@ VALUE stream_consume_string(struct um_stream *stream, VALUE out_buffer, size_t l
228
282
  }
229
283
  else
230
284
  str = rb_str_new(NULL, len);
231
- char *str_ptr = RSTRING_PTR(str);
232
- while (len) {
233
- char *segment_ptr = stream->head->ptr + stream->pos;
234
- size_t segment_len = stream->head->len - stream->pos;
235
- size_t cpy_len = (segment_len <= len) ? segment_len : len;
236
- memcpy(str_ptr, segment_ptr, cpy_len);
237
-
238
- len -= cpy_len;
239
- stream->pos += cpy_len;
240
- stream->pending_len -= cpy_len;
241
- str_ptr += cpy_len;
242
- if (stream->pos == stream->head->len) {
243
- struct um_segment *consumed = stream->head;
244
- stream->head = consumed->next;
245
- if (!stream->head) stream->tail = NULL;
246
- um_segment_checkin(stream->machine, consumed);
247
- stream->pos = 0;
248
- }
249
- }
285
+ char *dest = RSTRING_PTR(str);
250
286
 
251
- while (inc) {
252
- size_t segment_len = stream->head->len - stream->pos;
253
- size_t inc_len = (segment_len <= inc) ? segment_len : inc;
254
- inc -= inc_len;
255
- stream->pos += inc_len;
256
- stream->pending_len -= inc_len;
257
- if (stream->pos == stream->head->len) {
258
- struct um_segment *consumed = stream->head;
259
- stream->head = consumed->next;
260
- um_segment_checkin(stream->machine, consumed);
261
- if (!stream->head) {
262
- stream->tail = NULL;
263
- if (inc && safe_inc) {
264
- if (!stream_get_more_segments(stream)) break;
265
- }
266
- }
267
- stream->pos = 0;
268
- }
269
- }
287
+ stream_copy(stream, dest, len);
288
+ stream_skip(stream, inc, safe_inc);
270
289
  return str;
271
290
  RB_GC_GUARD(str);
272
291
  }
273
292
 
274
- // inline void stream_advance(struct um_stream *stream, size_t inc) {
275
- // while (inc) {
276
- // size_t segment_len = stream->head->len - stream->pos;
277
- // size_t inc_len = (segment_len <= inc) ? segment_len : inc;
278
- // inc -= inc_len;
279
- // stream->pos += inc_len;
280
- // if (stream->pos == stream->head->len) {
281
- // struct um_segment *consumed = stream->head;
282
- // stream->head = consumed->next;
283
- // um_segment_checkin(stream->machine, consumed);
284
- // if (!stream->head) {
285
- // stream->tail = NULL;
286
- // if (!stream_get_more_segments(stream)) return;
287
- // }
288
- // stream->pos = 0;
289
- // }
290
- // }
291
- // }
292
-
293
293
  VALUE stream_get_line(struct um_stream *stream, VALUE out_buffer, size_t maxlen) {
294
+ // if (stream->head) {
295
+ // fprintf(stderr, "head: %p pos: %ld: %.*s\n",
296
+ // stream->head, stream->pos,
297
+ // (int)(stream->head->len - stream->pos), stream->head->ptr + stream->pos
298
+ // );
299
+ // }
300
+
294
301
  if (unlikely(stream->eof && !stream->head)) return Qnil;
295
302
  if (!stream->tail && !stream_get_more_segments(stream)) return Qnil;
296
303
 
@@ -102,6 +102,20 @@ static inline void stream_setup(struct um_stream *stream, VALUE target, VALUE mo
102
102
  rb_raise(eUMError, "Invalid stream mode");
103
103
  }
104
104
 
105
+ /* call-seq:
106
+ * UM::Stream.new(machine, fd, mode = nil) -> stream
107
+ * machine.stream(fd, mode = nil) -> stream
108
+ * machine.stream(fd, mode = nil) { |stream| ... }
109
+ *
110
+ * Initializes a new stream with the given UringMachine instance, target and
111
+ * optional mode. The target maybe a file descriptor, or an instance of
112
+ * OpenSSL::SSL::SSLSocket. In case of an SSL socket, the mode should be :ssl.
113
+ *
114
+ * @param machine [UringMachine] UringMachine instance
115
+ * @param target [integer, OpenSSL::SSL::SSLSocket] stream target: file descriptor or SSL socket
116
+ * @param mode [Symbol] optional stream mode: :bp_read, :bp_recv, :ssl
117
+ * @return [void]
118
+ */
105
119
  VALUE Stream_initialize(int argc, VALUE *argv, VALUE self) {
106
120
  VALUE machine;
107
121
  VALUE target;
@@ -118,6 +132,13 @@ VALUE Stream_initialize(int argc, VALUE *argv, VALUE self) {
118
132
  return self;
119
133
  }
120
134
 
135
+ /* call-seq:
136
+ * stream.mode -> mode
137
+ *
138
+ * Returns the stream mode.
139
+ *
140
+ * @return [Symbol] stream mode
141
+ */
121
142
  VALUE Stream_mode(VALUE self) {
122
143
  struct um_stream *stream = um_get_stream(self);
123
144
  switch (stream->mode) {
@@ -129,16 +150,57 @@ VALUE Stream_mode(VALUE self) {
129
150
  return Qnil;
130
151
  }
131
152
 
153
+ /* call-seq:
154
+ * stream.get_line(limit) -> str
155
+ *
156
+ * Reads from the string until a newline character is encountered. Returns the
157
+ * line without the newline delimiter. If limit is 0, the line length is not
158
+ * limited. If no newline delimiter is found before EOF, returns nil.
159
+ *
160
+ * @param limit [integer] maximum line length (0 means no limit)
161
+ * @return [String, nil] read data or nil
162
+ */
132
163
  VALUE Stream_get_line(VALUE self, VALUE limit) {
133
164
  struct um_stream *stream = um_get_stream(self);
134
165
  return stream_get_line(stream, Qnil, NUM2ULONG(limit));
135
166
  }
136
167
 
168
+ /* call-seq:
169
+ * stream.get_string(len) -> str
170
+ *
171
+ * Reads len bytes from the stream. If len is 0, reads all available bytes. If
172
+ * len is negative, reads up to -len available bytes. If len is positive and eof
173
+ * is encountered before len bytes are read, returns nil.
174
+ *
175
+ * @param len [integer] number of bytes to read
176
+ * @return [String, nil] read data or nil
177
+ */
137
178
  VALUE Stream_get_string(VALUE self, VALUE len) {
138
179
  struct um_stream *stream = um_get_stream(self);
139
180
  return stream_get_string(stream, Qnil, NUM2LONG(len), 0, false);
140
181
  }
141
182
 
183
+ /* call-seq:
184
+ * stream.skip(len) -> len
185
+ *
186
+ * Skips len bytes in the stream.
187
+ *
188
+ * @param len [integer] number of bytes to skip
189
+ * @return [Integer] len
190
+ */
191
+ VALUE Stream_skip(VALUE self, VALUE len) {
192
+ struct um_stream *stream = um_get_stream(self);
193
+ stream_skip(stream, NUM2LONG(len), true);
194
+ return len;
195
+ }
196
+
197
+ /* call-seq:
198
+ * stream.resp_decode -> obj
199
+ *
200
+ * Decodes an object from a RESP (Redis protocol) message.
201
+ *
202
+ * @return [any] decoded object
203
+ */
142
204
  VALUE Stream_resp_decode(VALUE self) {
143
205
  struct um_stream *stream = um_get_stream(self);
144
206
  VALUE out_buffer = rb_utf8_str_new_literal("");
@@ -147,6 +209,15 @@ VALUE Stream_resp_decode(VALUE self) {
147
209
  return obj;
148
210
  }
149
211
 
212
+ /* call-seq:
213
+ * stream.resp_encode(obj) -> string
214
+ *
215
+ * Encodes an object into a RESP (Redis protocol) message.
216
+ *
217
+ * @param str [String] string buffer
218
+ * @param obj [any] object to be encoded
219
+ * @return [String] str
220
+ */
150
221
  VALUE Stream_resp_encode(VALUE self, VALUE str, VALUE obj) {
151
222
  struct um_write_buffer buf;
152
223
  write_buffer_init(&buf, str);
@@ -156,11 +227,49 @@ VALUE Stream_resp_encode(VALUE self, VALUE str, VALUE obj) {
156
227
  return str;
157
228
  }
158
229
 
230
+ /* call-seq:
231
+ * stream.eof? -> bool
232
+ *
233
+ * Returns true if stream has reached EOF.
234
+ *
235
+ * @return [bool] EOF reached
236
+ */
159
237
  VALUE Stream_eof_p(VALUE self) {
160
238
  struct um_stream *stream = um_get_stream(self);
161
239
  return stream->eof ? Qtrue : Qfalse;
162
240
  }
163
241
 
242
+ /* call-seq:
243
+ * stream.consumed -> int
244
+ *
245
+ * Returns the total number of bytes consumed from the stream.
246
+ *
247
+ * @return [Integer] total bytes consumed
248
+ */
249
+ VALUE Stream_consumed(VALUE self) {
250
+ struct um_stream *stream = um_get_stream(self);
251
+ return LONG2NUM(stream->consumed_bytes);
252
+ }
253
+
254
+ /* call-seq:
255
+ * stream.pending -> int
256
+ *
257
+ * Returns the number of bytes available for reading.
258
+ *
259
+ * @return [Integer] bytes available
260
+ */
261
+ VALUE Stream_pending(VALUE self) {
262
+ struct um_stream *stream = um_get_stream(self);
263
+ return LONG2NUM(stream->pending_bytes);
264
+ }
265
+
266
+ /* call-seq:
267
+ * stream.clear -> stream
268
+ *
269
+ * Clears all available bytes and stops any ongoing read operation.
270
+ *
271
+ * @return [UM::Stream] self
272
+ */
164
273
  VALUE Stream_clear(VALUE self) {
165
274
  struct um_stream *stream = um_get_stream(self);
166
275
  stream_clear(stream);
@@ -176,11 +285,14 @@ void Init_Stream(void) {
176
285
 
177
286
  rb_define_method(cStream, "get_line", Stream_get_line, 1);
178
287
  rb_define_method(cStream, "get_string", Stream_get_string, 1);
288
+ rb_define_method(cStream, "skip", Stream_skip, 1);
179
289
 
180
290
  rb_define_method(cStream, "resp_decode", Stream_resp_decode, 0);
181
291
  rb_define_singleton_method(cStream, "resp_encode", Stream_resp_encode, 2);
182
292
 
183
293
  rb_define_method(cStream, "eof?", Stream_eof_p, 0);
294
+ rb_define_method(cStream, "consumed", Stream_consumed, 0);
295
+ rb_define_method(cStream, "pending", Stream_pending, 0);
184
296
  rb_define_method(cStream, "clear", Stream_clear, 0);
185
297
 
186
298
  eStreamRESPError = rb_define_class_under(cStream, "RESPError", rb_eStandardError);
data/grant-2025/tasks.md CHANGED
@@ -35,8 +35,9 @@
35
35
  - [v] Data processing through a rewritten stream implementation.
36
36
 
37
37
  - [ ] More benchmarks
38
- - Different reading methods in concurrent setting
39
- - SSL vs SSL-custom-BIO vs SSL-stream
38
+ - [ ] Different reading methods in concurrent setting
39
+ - [ ] SSL vs SSL-custom-BIO vs SSL-stream in concurrent setting
40
+ - [ ] stream resp client vs redis gem in concurrent setting
40
41
 
41
42
  - [v] Sidecar mode
42
43
  - [v] Convert `UM#initialize` to take kwargs
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '0.29.0'
4
+ VERSION = '0.29.2'
5
5
  end
data/lib/uringmachine.rb CHANGED
@@ -194,6 +194,15 @@ class UringMachine
194
194
  close_async(fd)
195
195
  end
196
196
 
197
+ def stream(target, mode = nil)
198
+ stream = UM::Stream.new(self, target, mode)
199
+ return stream if !block_given?
200
+
201
+ res = yield(stream)
202
+ stream.clear
203
+ res
204
+ end
205
+
197
206
  private
198
207
 
199
208
  # @param block [Proc]
data/test/helper.rb CHANGED
@@ -103,3 +103,5 @@ class UMBaseTest < Minitest::Test
103
103
  dir
104
104
  end
105
105
  end
106
+
107
+ puts "Ruby: #{RUBY_VERSION}\nKernel: #{UringMachine.kernel_version}"
@@ -6,6 +6,7 @@ require 'securerandom'
6
6
  require 'socket'
7
7
  require 'net/http'
8
8
  require 'json'
9
+ require 'timeout'
9
10
 
10
11
  class MethodCallAuditor
11
12
  attr_reader :calls
@@ -722,12 +723,11 @@ class FiberSchedulerTest < UMBaseTest
722
723
  sleep 1
723
724
  end
724
725
  res = true
725
- rescue => e
726
- res = e
726
+ rescue => res
727
727
  end
728
728
  @scheduler.join
729
- assert_equal 3, machine.metrics[:total_ops]
730
729
  assert_kind_of Timeout::Error, res
730
+ assert_equal 3, machine.metrics[:total_ops]
731
731
  assert_equal({
732
732
  fiber: 1,
733
733
  timeout_after: 1,
data/test/test_stream.rb CHANGED
@@ -46,7 +46,10 @@ class StreamTest < StreamBaseTest
46
46
  assert stream.eof?
47
47
 
48
48
  stream.clear
49
- assert_equal [5, 0, 256, 16384 * 5, 16384 * 5 - 6], buffer_metrics
49
+
50
+ # initial buffer size: 6BKV, initial buffers commited: 16 (256KB)
51
+ # (plus an additional buffer commited after first usage)
52
+ assert_equal [16, 0, 256, 16384 * 16, 16384 * 16 - 6], buffer_metrics
50
53
  assert_equal 0, machine.metrics[:ops_pending]
51
54
  end
52
55
 
@@ -68,7 +71,7 @@ class StreamTest < StreamBaseTest
68
71
  assert_equal 0, machine.metrics[:ops_pending]
69
72
  assert_equal 256, machine.metrics[:segments_free]
70
73
 
71
- assert_equal [5, 0, 256, 16384 * 5, 16384 * 5 - 6], buffer_metrics
74
+ assert_equal [16, 0, 256, 16384 * 16, 16384 * 16 - 6], buffer_metrics
72
75
  ensure
73
76
  machine.close(rfd) rescue nil
74
77
  machine.close(wfd) rescue nil
@@ -118,11 +121,11 @@ class StreamTest < StreamBaseTest
118
121
  buf = stream.get_string(msg.bytesize)
119
122
  assert_equal msg, buf
120
123
 
121
- assert_equal 16, machine.metrics[:buffers_allocated]
122
- assert_equal 11, machine.metrics[:buffers_free]
124
+ stream.clear
125
+ # numbers may vary with different kernel versions
126
+ assert_in_range 24..32, machine.metrics[:buffers_allocated]
127
+ assert_in_range 10..18, machine.metrics[:buffers_free]
123
128
  assert_equal 256, machine.metrics[:segments_free]
124
- assert_equal 65536 * 4, machine.metrics[:buffer_space_allocated]
125
-
126
129
  ensure
127
130
  machine.terminate(f)
128
131
  machine.join(f)
@@ -136,7 +139,7 @@ class StreamTest < StreamBaseTest
136
139
 
137
140
  assert_equal 'foo', stream.get_line(0)
138
141
 
139
- assert_equal [5, 0, 255, 16384 * 5, 16384 * 5 - 12], buffer_metrics
142
+ assert_equal [16, 0, 255, 16384 * 16, 16384 * 16 - 12], buffer_metrics
140
143
  assert_equal 'bar', stream.get_line(0)
141
144
  assert_nil stream.get_line(0)
142
145
  assert_equal "baz", stream.get_string(-6)
@@ -152,11 +155,11 @@ class StreamTest < StreamBaseTest
152
155
  machine.close(@wfd)
153
156
 
154
157
  # three segments received
155
- assert_equal [5, 0, 253, 16384 * 5, 16384 * 5 - 13], buffer_metrics
158
+ assert_equal [16, 0, 253, 16384 * 16, 16384 * 16 - 13], buffer_metrics
156
159
  assert_equal 'bar', stream.get_line(0)
157
- assert_equal [5, 0, 255, 16384 * 5, 16384 * 5 - 13], buffer_metrics
160
+ assert_equal [16, 0, 255, 16384 * 16, 16384 * 16 - 13], buffer_metrics
158
161
  assert_equal 'baz', stream.get_line(0)
159
- assert_equal [5, 0, 256, 16384 * 5, 16384 * 5 - 13], buffer_metrics
162
+ assert_equal [16, 0, 256, 16384 * 16, 16384 * 16 - 13], buffer_metrics
160
163
  assert_nil stream.get_line(0)
161
164
  end
162
165
 
@@ -182,7 +185,7 @@ class StreamTest < StreamBaseTest
182
185
  assert_equal 'bizz', stream.get_line(5)
183
186
 
184
187
  assert_nil stream.get_line(8)
185
- assert_equal [5, 0, 256, 16384 * 5, 16384 * 5 - 17], buffer_metrics
188
+ assert_equal [16, 0, 256, 16384 * 16, 16384 * 16 - 17], buffer_metrics
186
189
  end
187
190
 
188
191
  def test_stream_get_string
@@ -218,6 +221,16 @@ class StreamTest < StreamBaseTest
218
221
  assert_nil stream.get_string(-3)
219
222
  end
220
223
 
224
+ def test_stream_skip
225
+ machine.write(@wfd, "foobarbaz")
226
+
227
+ stream.skip(2)
228
+ assert_equal 'obar', stream.get_string(4)
229
+
230
+ stream.skip(1)
231
+ assert_equal 'az', stream.get_string(0)
232
+ end
233
+
221
234
  def test_stream_big_data
222
235
  data = SecureRandom.random_bytes(300_000)
223
236
  fiber = machine.spin {
@@ -467,6 +480,23 @@ class StreamDevRandomTest < UMBaseTest
467
480
  machine.close(fd) rescue nil
468
481
  end
469
482
 
483
+ def get_line_do(n, acc)
484
+ fd = @machine.open('/dev/random', UM::O_RDONLY)
485
+ stream = UM::Stream.new(@machine, fd)
486
+ n.times { acc << stream.get_line(0) }
487
+ end
488
+
489
+ def test_stream_dev_random_get_line_concurrent
490
+ acc = []
491
+ c = 1
492
+ n = 100000
493
+ ff = c.times.map {
494
+ machine.spin { get_line_do(n, acc) }
495
+ }
496
+ machine.await(ff)
497
+ assert_equal c * n, acc.size
498
+ end
499
+
470
500
  def test_stream_dev_random_get_string
471
501
  fd = machine.open('/dev/random', UM::O_RDONLY)
472
502
  stream = UM::Stream.new(machine, fd)
@@ -573,3 +603,36 @@ class StreamModeTest < UMBaseTest
573
603
  sock2&.close rescue nil
574
604
  end
575
605
  end
606
+
607
+ class StreamByteCountsTest < StreamBaseTest
608
+ def test_stream_byte_counts
609
+ machine.write(@wfd, "foobar")
610
+
611
+ assert_equal 0, stream.consumed
612
+ assert_equal 0, stream.pending
613
+
614
+ buf = stream.get_string(2)
615
+ assert_equal 'fo', buf
616
+ assert_equal 2, stream.consumed
617
+ assert_equal 4, stream.pending
618
+
619
+ buf = stream.get_string(3)
620
+ assert_equal 'oba', buf
621
+ assert_equal 5, stream.consumed
622
+ assert_equal 1, stream.pending
623
+
624
+ machine.write(@wfd, "abc\ndef")
625
+ machine.snooze
626
+ assert_equal 5, stream.consumed
627
+ assert_equal 1, stream.pending
628
+
629
+ buf = stream.get_line(0)
630
+ assert_equal 'rabc', buf
631
+ assert_equal 10, stream.consumed
632
+ assert_equal 3, stream.pending
633
+
634
+ stream.clear
635
+ assert_equal 10, stream.consumed
636
+ assert_equal 0, stream.pending
637
+ end
638
+ end
data/test/test_um.rb CHANGED
@@ -292,6 +292,20 @@ class ScheduleTest < UMBaseTest
292
292
  assert_kind_of TOError, e
293
293
  end
294
294
 
295
+ def test_timeout_with_exception_instance
296
+ res = nil
297
+ e = TOError.new
298
+ begin
299
+ res = machine.timeout(0.01, e) {
300
+ machine.sleep(1)
301
+ :foo
302
+ }
303
+ rescue => res
304
+ end
305
+
306
+ assert_equal e, res
307
+ end
308
+
295
309
  def test_timeout_with_raising_block
296
310
  e = nil
297
311
  begin
@@ -3491,3 +3505,55 @@ class SetChildSubreaperTest < Minitest::Test
3491
3505
  assert_equal grand_child_pid, res
3492
3506
  end
3493
3507
  end
3508
+
3509
+ class StreamMethodTest < UMBaseTest
3510
+ def setup
3511
+ super
3512
+ @rfd, @wfd = UM.pipe
3513
+ end
3514
+
3515
+ def teardown
3516
+ @stream = nil
3517
+ machine.close(@rfd) rescue nil
3518
+ machine.close(@wfd) rescue nil
3519
+ super
3520
+ end
3521
+
3522
+ def test_stream_method
3523
+ machine.write(@wfd, "foobar")
3524
+ machine.close(@wfd)
3525
+
3526
+ stream = machine.stream(@rfd)
3527
+ assert_kind_of UM::Stream, stream
3528
+
3529
+ buf = stream.get_string(3)
3530
+ assert_equal 'foo', buf
3531
+
3532
+ buf = stream.get_string(-6)
3533
+ assert_equal 'bar', buf
3534
+ assert stream.eof?
3535
+
3536
+ stream.clear
3537
+ end
3538
+
3539
+ def test_stream_method_with_block
3540
+ machine.write(@wfd, "foobar")
3541
+ machine.close(@wfd)
3542
+
3543
+ bufs = []
3544
+ stream_obj = nil
3545
+ res = machine.stream(@rfd) do |s|
3546
+ stream_obj = s
3547
+
3548
+ bufs << s.get_string(3)
3549
+ bufs << s.get_string(-6)
3550
+
3551
+ :foo
3552
+ end
3553
+
3554
+ assert_kind_of UM::Stream, stream_obj
3555
+ assert stream_obj.eof?
3556
+ assert_equal ['foo', 'bar'], bufs
3557
+ assert_equal :foo, res
3558
+ end
3559
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uringmachine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.29.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -67,6 +67,7 @@ files:
67
67
  - benchmark/common.rb
68
68
  - benchmark/dns_client.rb
69
69
  - benchmark/gets.rb
70
+ - benchmark/gets_concurrent.rb
70
71
  - benchmark/http_parse.rb
71
72
  - benchmark/http_server_accept_queue.rb
72
73
  - benchmark/http_server_multi_accept.rb