uringmachine 0.5.1 → 0.6.1

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: 0b063db46fac29472c42866eb1864c731b24fc0846fe424eec06213bba8fde0a
4
- data.tar.gz: a535fabf5d16107de8d3823766d9ff0d7d22d0702c5401e263eb925eb222dd5d
3
+ metadata.gz: 602ceb6207baea6b38287928500c73df48258ca2f8cf843f02b45fa65c591e3e
4
+ data.tar.gz: 78b2bc7a274b03e2ae51510e213be0d23911c4db102ad4ab7eee435f915bb5c0
5
5
  SHA512:
6
- metadata.gz: 94ad9e942e84e87f0acbd75007962c3df5ea259de77ef4e590ff91dc4ea614dcd22e7db3da1954be05b11951e621f0cf58858391b677e109e97590bb0f8e3dee
7
- data.tar.gz: 4c77e826fb33fc879de748b877eec9a6d49f6927b8167377b4191c30b9aa2b638dbd0cc76a77ec0a17daec648d2587ca4357f367501391aad06c7532606e85b2
6
+ metadata.gz: c0dcbb93493c9778cf4ac12769f88611364d4e190c9e9dfa666543643e3aa3b44fa01cc49cf1ea2a1ba4798c4893b75e8b47e5b1eeac23b48bc5a6d84046e458
7
+ data.tar.gz: 5b012fcc3e7fd1a6d4e68aae58b9d099f19d5a89670772cdfdd6ef4ac76ed9781f533fd576e2b19b02c7774ba31d3ce6ee06dc590e04ce07a62e3c7fb2394191
@@ -13,7 +13,7 @@ jobs:
13
13
  matrix:
14
14
  # macos-latest uses arm64, macos-13 uses x86
15
15
  os: [ubuntu-latest]
16
- ruby: ['3.3', 'head']
16
+ ruby: ['3.3', '3.4', 'head']
17
17
 
18
18
  name: ${{matrix.os}}, ${{matrix.ruby}}
19
19
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 2025-04-23 Version 0.6.1
2
+
3
+ - Improve `#snooze` to prevent op completion starvation
4
+
5
+ # 2025-04-23 Version 0.6
6
+
7
+ - Add `#periodically` for multishot timeout
8
+ - Add `UM::Actor` class
1
9
  - Add `#prep_timeout` and `AsyncOp`
2
10
 
3
11
  # 2024-11-14 Version 0.5
data/TODO.md CHANGED
@@ -1,3 +1,7 @@
1
+ - [ ] multishot timeout
2
+ - [v] machine.periodically(interval) { ... }
3
+ - [ ] machine.prep_timeout_multishot(interval)
4
+
1
5
  - splice / - tee
2
6
  - sendto
3
7
  - recvfrom
@@ -0,0 +1,149 @@
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-ips'
9
+ gem 'http_parser.rb'
10
+ end
11
+
12
+ require 'benchmark/ips'
13
+ require 'uringmachine'
14
+ require 'http/parser'
15
+
16
+ $machine = UM.new
17
+
18
+ HTTP_MSG = "GET /foo/bar HTTP/1.1\r\nServer: foobar.com\r\nFoo: bar\r\n\r\n"
19
+
20
+ $count = 0
21
+
22
+ def parse_http_parser
23
+ current_fiber = Fiber.current
24
+ $count += 1
25
+ r, w = IO.pipe
26
+ parser = Http::Parser.new
27
+ $machine.spin do
28
+ buffer = +''
29
+ loop do
30
+ res = $machine.read(r.fileno, buffer, 512)
31
+ break if res == 0
32
+ parser << buffer
33
+ end
34
+ rescue Exception => e
35
+ $machine.schedule(current_fiber, e)
36
+ # puts e.backtrace.join("\n")
37
+ # exit!
38
+ end
39
+ parser.on_message_complete = -> do
40
+ headers = parser.headers
41
+ headers['method'] = parser.http_method.downcase
42
+ headers['path'] = parser.request_url
43
+ headers['protocol'] = parser.http_version
44
+ $machine.schedule(current_fiber, headers)
45
+ end
46
+
47
+ $machine.write(w.fileno, HTTP_MSG)
48
+ $machine.yield
49
+ ensure
50
+ $machine.close(r.fileno)
51
+ $machine.close(w.fileno)
52
+ end
53
+
54
+ require 'stringio'
55
+
56
+ RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+(http\/[0-9\.]{1,3})/i
57
+ RE_HEADER_LINE = /^([a-z0-9\-]+)\:\s+(.+)/i
58
+
59
+ def get_line(fd, sio, buffer)
60
+ while true
61
+ line = sio.gets(chomp: true)
62
+ return line if line
63
+
64
+ res = $machine.read(fd, buffer, 1024, -1)
65
+ return nil if res == 0
66
+ end
67
+ end
68
+
69
+ def get_request_line(fd, sio, buffer)
70
+ line = get_line(fd, sio, buffer)
71
+
72
+ m = line.match(RE_REQUEST_LINE)
73
+ return nil if !m
74
+
75
+ {
76
+ 'method' => m[1].downcase,
77
+ 'path' => m[2],
78
+ 'protocol' => m[3].downcase
79
+ }
80
+ end
81
+
82
+ def parse_headers(fd)
83
+ buffer = String.new('', capacity: 4096)
84
+ sio = StringIO.new(buffer)
85
+
86
+ headers = get_request_line(fd, sio, buffer)
87
+ return nil if !headers
88
+
89
+ while true
90
+ line = get_line(fd, sio, buffer)
91
+ break if line.empty?
92
+
93
+ m = line.match(RE_HEADER_LINE)
94
+ raise "Invalid header" if !m
95
+
96
+ headers[m[1]] = m[2]
97
+ end
98
+
99
+ headers
100
+ end
101
+
102
+ def parse_http_stringio
103
+ current_fiber = Fiber.current
104
+ r, w = IO.pipe
105
+
106
+ $machine.spin do
107
+ headers = parse_headers(r.fileno)
108
+ $machine.schedule(current_fiber, headers)
109
+ rescue Exception => e
110
+ p e
111
+ puts e.backtrace.join("\n")
112
+ exit!
113
+ end
114
+
115
+ $machine.write(w.fileno, HTTP_MSG)
116
+ $machine.yield
117
+ ensure
118
+ $machine.close(r.fileno)
119
+ $machine.close(w.fileno)
120
+ end
121
+
122
+ # p parse_http_parser
123
+ # p parse_http_stringio
124
+ # exit
125
+
126
+ GC.disable
127
+
128
+ def alloc_count
129
+ count0 = ObjectSpace.count_objects[:TOTAL]
130
+ yield
131
+ count1 = ObjectSpace.count_objects[:TOTAL]
132
+ count1 - count0
133
+ end
134
+
135
+ X = 100
136
+ p(
137
+ alloc_http_parser: alloc_count { X.times { parse_http_parser } },
138
+ alloc_stringio: alloc_count { X.times { parse_http_stringio } }
139
+ )
140
+ exit
141
+
142
+ Benchmark.ips do |x|
143
+ x.config(:time => 5, :warmup => 3)
144
+
145
+ x.report("http_parser") { parse_http_parser }
146
+ x.report("homegrown") { parse_http_stringio }
147
+
148
+ x.compare!
149
+ end
@@ -0,0 +1,111 @@
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-ips'
9
+ end
10
+
11
+ require 'benchmark/ips'
12
+ require 'uringmachine'
13
+
14
+ COUNT = 1000
15
+ NUM_PRODUCERS = 2
16
+ NUM_CONSUMERS = 10
17
+
18
+ def run_threads
19
+ queue = Queue.new
20
+ done = Queue.new
21
+
22
+ NUM_PRODUCERS.times do
23
+ Thread.new do
24
+ COUNT.times { queue << rand(1000) }
25
+ done << true
26
+ end
27
+ end
28
+
29
+ total = 0
30
+ NUM_CONSUMERS.times do
31
+ Thread.new do
32
+ loop do
33
+ item = queue.shift
34
+ break if item.nil?
35
+
36
+ total += item
37
+ end
38
+ done << true
39
+ end
40
+ end
41
+
42
+ # wait for producers
43
+ NUM_PRODUCERS.times { done.shift }
44
+
45
+ # stop and wait for consumers
46
+ NUM_CONSUMERS.times do
47
+ queue << nil
48
+ done.shift
49
+ end
50
+
51
+ total
52
+ end
53
+
54
+ def run_um
55
+ machine = UM.new
56
+ queue = UM::Queue.new
57
+ done = UM::Queue.new
58
+
59
+ NUM_PRODUCERS.times do
60
+ machine.spin do
61
+ COUNT.times { machine.push(queue, rand(1000)) }
62
+ machine.push(done, true)
63
+ end
64
+ end
65
+
66
+ total = 0
67
+ NUM_CONSUMERS.times do
68
+ machine.spin do
69
+ loop do
70
+ item = machine.shift(queue)
71
+ break if item.nil?
72
+
73
+ total += item
74
+ end
75
+ machine.push(done, true)
76
+ end
77
+ end
78
+
79
+ # wait for producers
80
+ NUM_PRODUCERS.times { machine.shift(done) }
81
+
82
+ # stop and wait for consumers
83
+ NUM_CONSUMERS.times do
84
+ machine.push(queue, nil)
85
+ machine.shift(done)
86
+ end
87
+
88
+ total
89
+ end
90
+
91
+
92
+ # puts "running"
93
+ # res = run_threads
94
+ # p threads: res
95
+
96
+ # 100.times {
97
+ # res = run_um
98
+ # p fibers: res
99
+ # }
100
+
101
+
102
+ # __END__
103
+
104
+ Benchmark.ips do |x|
105
+ x.config(:time => 5, :warmup => 2)
106
+
107
+ x.report("threads") { run_threads }
108
+ x.report("UM") { run_um }
109
+
110
+ x.compare!
111
+ end
data/ext/um/extconf.rb CHANGED
@@ -90,7 +90,7 @@ def get_config
90
90
  }
91
91
  end
92
92
 
93
- config_ssl
93
+ # config_ssl
94
94
 
95
95
  config = get_config
96
96
  puts "Building UringMachine (\n#{config.map { |(k, v)| " #{k}: #{v}\n"}.join})"
data/ext/um/um.c CHANGED
@@ -88,8 +88,9 @@ static inline void um_process_cqe(struct um *machine, struct io_uring_cqe *cqe)
88
88
  op->result.flags = cqe->flags;
89
89
  }
90
90
 
91
- if (!(op->flags & OP_F_ASYNC))
92
- um_runqueue_push(machine, op);
91
+ if (op->flags & OP_F_ASYNC) return;
92
+
93
+ um_runqueue_push(machine, op);
93
94
  }
94
95
 
95
96
  // copied from liburing/queue.c
@@ -174,8 +175,28 @@ inline VALUE process_runqueue_op(struct um *machine, struct um_op *op) {
174
175
  inline VALUE um_fiber_switch(struct um *machine) {
175
176
  while (true) {
176
177
  struct um_op *op = um_runqueue_shift(machine);
177
- if (op)
178
+ if (op) {
179
+ // in case of a snooze, we need to prevent a situation where completions
180
+ // are not processed because the runqueue is never empty. Theoretically,
181
+ // we can still have a situation where multiple fibers are all doing a
182
+ // snooze repeatedly, which can prevent completions from being processed.
183
+
184
+ // is the op a snooze op and is this the same fiber as the current one?
185
+ if (unlikely(op->kind == OP_SCHEDULE && op->fiber == rb_fiber_current())) {
186
+ // are there any pending ops (i.e. waiting for completion)?
187
+ if (machine->pending_count > 0) {
188
+ // if yes, process completions, get runqueue head, put original op
189
+ // back on runqueue.
190
+ um_wait_for_and_process_ready_cqes(machine);
191
+ struct um_op *op2 = um_runqueue_shift(machine);
192
+ if (likely(op2 && op2 != op)) {
193
+ um_runqueue_push(machine, op);
194
+ op = op2;
195
+ }
196
+ }
197
+ }
178
198
  return process_runqueue_op(machine, op);
199
+ }
179
200
 
180
201
  um_wait_for_and_process_ready_cqes(machine);
181
202
  }
@@ -216,6 +237,8 @@ inline void um_prep_op(struct um *machine, struct um_op *op, enum op_kind kind)
216
237
  case OP_ACCEPT_MULTISHOT:
217
238
  case OP_READ_MULTISHOT:
218
239
  case OP_RECV_MULTISHOT:
240
+ case OP_TIMEOUT_MULTISHOT:
241
+ case OP_SLEEP_MULTISHOT:
219
242
  op->flags |= OP_F_MULTISHOT;
220
243
  default:
221
244
  }
@@ -297,6 +320,33 @@ VALUE um_sleep(struct um *machine, double duration) {
297
320
  return raise_if_exception(ret);
298
321
  }
299
322
 
323
+ // VALUE um_periodically(struct um *machine, double interval) {
324
+ // struct um_op op;
325
+ // VALUE ret = Qnil;
326
+ // um_prep_op(machine, &op, OP_SLEEP_MULTISHOT);
327
+ // op.ts = um_double_to_timespec(interval);
328
+ // op.flags |= OP_F_MULTISHOT;
329
+ // struct io_uring_sqe *sqe = um_get_sqe(machine, &op);
330
+ // io_uring_prep_timeout(sqe, &op.ts, 0, IORING_TIMEOUT_MULTISHOT);
331
+
332
+ // while (true) {
333
+ // ret = um_fiber_switch(machine);
334
+
335
+ // if (!um_op_completed_p(&op)) {
336
+ // um_cancel_and_wait(machine, &op);
337
+ // break;
338
+ // }
339
+ // else {
340
+ // if (op.result.res != -ETIME) um_raise_on_error_result(op.result.res);
341
+ // ret = DBL2NUM(interval);
342
+ // }
343
+ // }
344
+
345
+ // RB_GC_GUARD(ret);
346
+ // return raise_if_exception(ret);
347
+
348
+ // }
349
+
300
350
  inline VALUE um_read(struct um *machine, int fd, VALUE buffer, int maxlen, int buffer_offset) {
301
351
  struct um_op op;
302
352
  um_prep_op(machine, &op, OP_READ);
@@ -701,3 +751,44 @@ VALUE um_recv_each(struct um *machine, int fd, int bgid, int flags) {
701
751
  struct op_ctx ctx = { .machine = machine, .op = &op, .fd = fd, .bgid = bgid, .read_buf = NULL, .flags = flags };
702
752
  return rb_ensure(read_recv_each_begin, (VALUE)&ctx, multishot_ensure, (VALUE)&ctx);
703
753
  }
754
+
755
+ VALUE periodically_begin(VALUE arg) {
756
+ struct op_ctx *ctx = (struct op_ctx *)arg;
757
+ struct io_uring_sqe *sqe = um_get_sqe(ctx->machine, ctx->op);
758
+ io_uring_prep_timeout(sqe, &ctx->ts, 0, IORING_TIMEOUT_MULTISHOT);
759
+
760
+ while (true) {
761
+ VALUE ret = um_fiber_switch(ctx->machine);
762
+ if (!um_op_completed_p(ctx->op))
763
+ return raise_if_exception(ret);
764
+
765
+ int more = false;
766
+ struct um_op_result *result = &ctx->op->result;
767
+ while (result) {
768
+ more = (result->flags & IORING_CQE_F_MORE);
769
+ if (result->res < 0 && result->res != -ETIME) {
770
+ um_op_multishot_results_clear(ctx->machine, ctx->op);
771
+ return Qnil;
772
+ }
773
+ rb_yield(Qnil);
774
+ result = result->next;
775
+ }
776
+ um_op_multishot_results_clear(ctx->machine, ctx->op);
777
+ if (more)
778
+ ctx->op->flags &= ~OP_F_COMPLETED;
779
+ else
780
+ break;
781
+ }
782
+
783
+ return Qnil;
784
+ }
785
+
786
+ VALUE um_periodically(struct um *machine, double interval) {
787
+ struct um_op op;
788
+ um_prep_op(machine, &op, OP_SLEEP_MULTISHOT);
789
+ op.ts = um_double_to_timespec(interval);
790
+
791
+ struct op_ctx ctx = { .machine = machine, .op = &op, .ts = op.ts, .read_buf = NULL };
792
+ return rb_ensure(periodically_begin, (VALUE)&ctx, multishot_ensure, (VALUE)&ctx);
793
+ }
794
+
data/ext/um/um.h CHANGED
@@ -43,14 +43,16 @@ enum op_kind {
43
43
 
44
44
  OP_ACCEPT_MULTISHOT,
45
45
  OP_READ_MULTISHOT,
46
- OP_RECV_MULTISHOT
46
+ OP_RECV_MULTISHOT,
47
+ OP_TIMEOUT_MULTISHOT,
48
+ OP_SLEEP_MULTISHOT
47
49
  };
48
50
 
49
- #define OP_F_COMPLETED (1U << 0)
50
- #define OP_F_TRANSIENT (1U << 1)
51
- #define OP_F_ASYNC (1U << 2)
52
- #define OP_F_IGNORE_CANCELED (1U << 3)
53
- #define OP_F_MULTISHOT (1U << 4)
51
+ #define OP_F_COMPLETED (1U << 0) // op is completed (set on each CQE for multishot ops)
52
+ #define OP_F_TRANSIENT (1U << 1) // op is heap allocated
53
+ #define OP_F_ASYNC (1U << 2) // op belongs to an AsyncOp
54
+ #define OP_F_IGNORE_CANCELED (1U << 3) // CQE with -ECANCEL should be ignored
55
+ #define OP_F_MULTISHOT (1U << 4) // op is multishot
54
56
 
55
57
  struct um_op_result {
56
58
  __s32 res;
@@ -199,6 +201,7 @@ void um_schedule(struct um *machine, VALUE fiber, VALUE value);
199
201
  VALUE um_timeout(struct um *machine, VALUE interval, VALUE class);
200
202
 
201
203
  VALUE um_sleep(struct um *machine, double duration);
204
+ VALUE um_periodically(struct um *machine, double interval);
202
205
  VALUE um_read(struct um *machine, int fd, VALUE buffer, int maxlen, int buffer_offset);
203
206
  VALUE um_read_each(struct um *machine, int fd, int bgid);
204
207
  VALUE um_write(struct um *machine, int fd, VALUE str, int len);
data/ext/um/um_class.c CHANGED
@@ -91,6 +91,11 @@ VALUE UM_sleep(VALUE self, VALUE duration) {
91
91
  return um_sleep(machine, NUM2DBL(duration));
92
92
  }
93
93
 
94
+ VALUE UM_periodically(VALUE self, VALUE interval) {
95
+ struct um *machine = um_get_machine(self);
96
+ return um_periodically(machine, NUM2DBL(interval));
97
+ }
98
+
94
99
  VALUE UM_read(int argc, VALUE *argv, VALUE self) {
95
100
  struct um *machine = um_get_machine(self);
96
101
  VALUE fd;
@@ -329,6 +334,7 @@ void Init_UM(void) {
329
334
  rb_define_method(cUM, "read", UM_read, -1);
330
335
  rb_define_method(cUM, "read_each", UM_read_each, 2);
331
336
  rb_define_method(cUM, "sleep", UM_sleep, 1);
337
+ rb_define_method(cUM, "periodically", UM_periodically, 1);
332
338
  rb_define_method(cUM, "write", UM_write, -1);
333
339
 
334
340
  rb_define_method(cUM, "waitpid", UM_waitpid, 2);
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UringMachine
4
+ def spin_actor(mod, *a, **k)
5
+ target = Object.new.extend(mod)
6
+ mailbox = UM::Queue.new
7
+ actor = spin(nil, Actor) { actor.run(self, target, mailbox) }
8
+ target.setup(*a, **k)
9
+ snooze
10
+ actor
11
+ end
12
+
13
+ class Actor < Fiber
14
+ def run(machine, target, mailbox)
15
+ @machine = machine
16
+ @target = target
17
+ @mailbox = mailbox
18
+ while (msg = machine.shift(mailbox))
19
+ process_message(msg)
20
+ end
21
+ ensure
22
+ @target.teardown if @target.respond_to?(:teardown)
23
+ end
24
+
25
+ def cast(sym, *a, **k)
26
+ self << [:cast, nil, sym, a, k]
27
+ self
28
+ end
29
+
30
+ def call(sym, *a, **k)
31
+ self << [:call, Fiber.current, sym, a, k]
32
+ @machine.yield
33
+ end
34
+
35
+ private
36
+
37
+ def process_message(msg)
38
+ type, fiber, sym, args, kwargs = msg
39
+ case type
40
+ when :cast
41
+ @target.send(sym, *args, **kwargs)
42
+ when :call
43
+ res = @target.send(sym, *args, **kwargs)
44
+ @machine.schedule(fiber, res)
45
+ end
46
+ end
47
+
48
+ def <<(msg)
49
+ @machine.push(@mailbox, msg)
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '0.5.1'
4
+ VERSION = '0.6.1'
5
5
  end
data/lib/uringmachine.rb CHANGED
@@ -12,8 +12,8 @@ class UringMachine
12
12
  @@fiber_map
13
13
  end
14
14
 
15
- def spin(value = nil, &block)
16
- f = Fiber.new do |resume_value|
15
+ def spin(value = nil, fiber_class = Fiber, &block)
16
+ f = fiber_class.new do |resume_value|
17
17
  block.(resume_value)
18
18
  rescue Exception => e
19
19
  STDERR.puts "Unhandled fiber exception: #{e.inspect}"
data/test/run.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'socket'
5
+ require 'uringmachine/actor'
6
+
7
+ class ActorTest < UMBaseTest
8
+ module Counter
9
+ def setup
10
+ @count = 0
11
+ end
12
+
13
+ def incr
14
+ @count += 1
15
+ end
16
+
17
+ def get
18
+ @count
19
+ end
20
+
21
+ def reset
22
+ @count = 0
23
+ end
24
+ end
25
+
26
+ def test_basic_actor_functionality
27
+ actor = @machine.spin_actor(Counter)
28
+
29
+ assert_kind_of Fiber, actor
30
+
31
+ assert_equal 0, actor.call(:get)
32
+ assert_equal 1, actor.call(:incr)
33
+ assert_equal actor, actor.cast(:incr)
34
+ assert_equal 2, actor.call(:get)
35
+ assert_equal actor, actor.cast(:reset)
36
+ assert_equal 0, actor.call(:get)
37
+ end
38
+
39
+ module Counter2
40
+ def setup(count)
41
+ @count = count
42
+ end
43
+
44
+ def incr
45
+ @count += 1
46
+ end
47
+
48
+ def get
49
+ @count
50
+ end
51
+
52
+ def reset
53
+ @count = 0
54
+ end
55
+ end
56
+
57
+
58
+ def test_actor_with_args
59
+ actor = @machine.spin_actor(Counter2, 43)
60
+
61
+ assert_equal 43, actor.call(:get)
62
+ end
63
+ end
data/test/test_um.rb CHANGED
@@ -212,6 +212,57 @@ class SleepTest < UMBaseTest
212
212
  end
213
213
  end
214
214
 
215
+ class PeriodicallyTest < UMBaseTest
216
+ class Cancel < StandardError; end
217
+
218
+ def test_periodically
219
+ count = 0
220
+ cancel = 0
221
+
222
+ t0 = monotonic_clock
223
+ assert_equal 0, machine.pending_count
224
+ begin
225
+ machine.periodically(0.01) do
226
+ count += 1
227
+ raise Cancel if count >= 5
228
+ end
229
+ rescue Cancel
230
+ cancel = 1
231
+ end
232
+ machine.snooze
233
+ assert_equal 0, machine.pending_count
234
+ t1 = monotonic_clock
235
+ assert_in_range 0.05..0.09, t1 - t0
236
+ assert_equal 5, count
237
+ assert_equal 1, cancel
238
+ end
239
+
240
+ def test_periodically_with_timeout
241
+ count = 0
242
+ cancel = 0
243
+
244
+ t0 = monotonic_clock
245
+ assert_equal 0, machine.pending_count
246
+ begin
247
+ machine.timeout(0.05, Cancel) do
248
+ machine.periodically(0.01) do
249
+ count += 1
250
+ raise Cancel if count >= 5
251
+ end
252
+ end
253
+ rescue Cancel
254
+ cancel = 1
255
+ end
256
+ machine.snooze
257
+ assert_equal 0, machine.pending_count
258
+ t1 = monotonic_clock
259
+ assert_in_range 0.05..0.08, t1 - t0
260
+ assert_in_range 4..6, count
261
+ assert_equal 1, cancel
262
+
263
+ end
264
+ end
265
+
215
266
  class ReadTest < UMBaseTest
216
267
  def test_read
217
268
  r, w = IO.pipe
@@ -274,6 +325,26 @@ class ReadTest < UMBaseTest
274
325
  assert_equal 3, result
275
326
  assert_equal 'foobar', buffer
276
327
  end
328
+
329
+ def test_read_with_string_io
330
+ require 'stringio'
331
+
332
+ buffer = +'foo'
333
+ sio = StringIO.new(buffer)
334
+
335
+ r, w = IO.pipe
336
+ w << 'bar'
337
+
338
+ result = machine.read(r.fileno, buffer, 100, -1)
339
+ assert_equal 3, result
340
+ assert_equal 'foobar', sio.read
341
+
342
+ w << 'baz'
343
+
344
+ result = machine.read(r.fileno, buffer, 100, -1)
345
+ assert_equal 3, result
346
+ assert_equal 'baz', sio.read
347
+ end
277
348
  end
278
349
 
279
350
  class ReadEachTest < UMBaseTest
@@ -846,32 +917,28 @@ class QueueTest < UMBaseTest
846
917
  q = UM::Queue.new
847
918
  buf = []
848
919
 
849
- f1 = Fiber.new do
920
+ machine.spin do
850
921
  buf << [1, machine.pop(q)]
851
- machine.yield
852
922
  end
853
923
 
854
- machine.schedule(f1, nil)
855
-
856
- f2 = Fiber.new do
924
+ machine.spin do
857
925
  buf << [2, machine.pop(q)]
858
- machine.yield
859
926
  end
860
927
 
861
- machine.schedule(f2, nil)
862
-
863
928
  machine.snooze
864
929
  assert_equal [], buf
930
+ assert_equal 2, machine.pending_count
865
931
 
866
932
  machine.push(q, :foo)
867
933
  assert_equal 1, q.count
868
- machine.sleep(0.02)
934
+ machine.snooze
935
+ assert_equal 1, machine.pending_count
869
936
  assert_equal [[1, :foo]], buf
870
937
 
871
938
  machine.push(q, :bar)
872
939
  assert_equal 1, q.count
873
940
 
874
- machine.sleep(0.02)
941
+ machine.snooze
875
942
  assert_equal [[1, :foo], [2, :bar]], buf
876
943
  assert_equal 0, q.count
877
944
  end
@@ -898,7 +965,7 @@ class QueueTest < UMBaseTest
898
965
  end
899
966
  machine.schedule(f2, nil)
900
967
 
901
- 3.times { machine.snooze }
968
+ machine.snooze
902
969
 
903
970
  assert_equal [[1, :bar], [2, :foo]], buf.sort
904
971
  assert_equal 0, q.count
@@ -925,12 +992,12 @@ class QueueTest < UMBaseTest
925
992
  end
926
993
  machine.schedule(f2, nil)
927
994
 
928
- machine.sleep 0.01
995
+ machine.snooze
929
996
 
930
997
  assert_equal [[1, :foo]], buf
931
998
  machine.push(q, :bar)
932
999
 
933
- machine.sleep 0.01
1000
+ machine.snooze
934
1001
  assert_equal [[1, :foo], [2, :bar]], buf
935
1002
  end
936
1003
 
data/uringmachine.gemspec CHANGED
@@ -20,8 +20,8 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
  s.required_ruby_version = '>= 3.3'
22
22
 
23
- s.add_development_dependency 'rake-compiler', '1.2.8'
24
- s.add_development_dependency 'minitest', '5.25.1'
23
+ s.add_development_dependency 'rake-compiler', '1.2.9'
24
+ s.add_development_dependency 'minitest', '5.25.4'
25
25
  s.add_development_dependency 'http_parser.rb', '0.8.0'
26
26
  s.add_development_dependency 'benchmark-ips', '2.14.0'
27
27
  s.add_development_dependency 'localhost', '1.3.1'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uringmachine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-11 00:00:00.000000000 Z
10
+ date: 2025-04-23 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rake-compiler
@@ -16,28 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - '='
18
17
  - !ruby/object:Gem::Version
19
- version: 1.2.8
18
+ version: 1.2.9
20
19
  type: :development
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - '='
25
24
  - !ruby/object:Gem::Version
26
- version: 1.2.8
25
+ version: 1.2.9
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: minitest
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - '='
32
31
  - !ruby/object:Gem::Version
33
- version: 5.25.1
32
+ version: 5.25.4
34
33
  type: :development
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - '='
39
38
  - !ruby/object:Gem::Version
40
- version: 5.25.1
39
+ version: 5.25.4
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: http_parser.rb
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +79,6 @@ dependencies:
80
79
  - - '='
81
80
  - !ruby/object:Gem::Version
82
81
  version: 1.3.1
83
- description:
84
82
  email: sharon@noteflakes.com
85
83
  executables: []
86
84
  extensions:
@@ -98,6 +96,8 @@ files:
98
96
  - README.md
99
97
  - Rakefile
100
98
  - TODO.md
99
+ - examples/bm_http_parse.rb
100
+ - examples/bm_queue.rb
101
101
  - examples/bm_snooze.rb
102
102
  - examples/bm_sqlite.rb
103
103
  - examples/bm_write.rb
@@ -129,12 +129,15 @@ files:
129
129
  - ext/um/um_sync.c
130
130
  - ext/um/um_utils.c
131
131
  - lib/uringmachine.rb
132
+ - lib/uringmachine/actor.rb
132
133
  - lib/uringmachine/dns_resolver.rb
133
134
  - lib/uringmachine/ssl.rb
134
135
  - lib/uringmachine/ssl/context_builder.rb
135
136
  - lib/uringmachine/version.rb
136
137
  - supressions/ruby.supp
137
138
  - test/helper.rb
139
+ - test/run.rb
140
+ - test/test_actor.rb
138
141
  - test/test_async_op.rb
139
142
  - test/test_ssl.rb
140
143
  - test/test_um.rb
@@ -439,7 +442,6 @@ metadata:
439
442
  source_code_uri: https://github.com/digital-fabric/uringmachine
440
443
  documentation_uri: https://www.rubydoc.info/gems/uringmachine
441
444
  changelog_uri: https://github.com/digital-fabric/uringmachine/blob/master/CHANGELOG.md
442
- post_install_message:
443
445
  rdoc_options:
444
446
  - "--title"
445
447
  - UringMachine
@@ -458,8 +460,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
458
460
  - !ruby/object:Gem::Version
459
461
  version: '0'
460
462
  requirements: []
461
- rubygems_version: 3.5.16
462
- signing_key:
463
+ rubygems_version: 3.6.2
463
464
  specification_version: 4
464
465
  summary: A lean, mean io_uring machine
465
466
  test_files: []