uringmachine 0.20.0 → 0.22.0
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/.github/workflows/test.yml +3 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +34 -0
- data/TODO.md +132 -26
- data/benchmark/README.md +173 -0
- data/benchmark/bm_io_pipe.rb +70 -0
- data/benchmark/bm_io_socketpair.rb +71 -0
- data/benchmark/bm_mutex_cpu.rb +57 -0
- data/benchmark/bm_mutex_io.rb +64 -0
- data/benchmark/bm_pg_client.rb +109 -0
- data/benchmark/bm_queue.rb +76 -0
- data/benchmark/chart.png +0 -0
- data/benchmark/common.rb +135 -0
- data/benchmark/dns_client.rb +47 -0
- data/{examples/bm_http_parse.rb → benchmark/http_parse.rb} +1 -1
- data/benchmark/run_bm.rb +8 -0
- data/benchmark/sqlite.rb +108 -0
- data/{examples/bm_write.rb → benchmark/write.rb} +6 -3
- data/ext/um/extconf.rb +1 -1
- data/ext/um/um.c +404 -95
- data/ext/um/um.h +77 -24
- data/ext/um/um_async_op.c +2 -2
- data/ext/um/um_class.c +168 -18
- data/ext/um/um_op.c +43 -0
- data/ext/um/um_sync.c +10 -16
- data/ext/um/um_utils.c +16 -0
- data/grant-2025/journal.md +242 -1
- data/grant-2025/tasks.md +136 -41
- data/lib/uringmachine/actor.rb +8 -0
- data/lib/uringmachine/dns_resolver.rb +1 -2
- data/lib/uringmachine/fiber_scheduler.rb +283 -110
- data/lib/uringmachine/version.rb +1 -1
- data/lib/uringmachine.rb +32 -3
- data/test/helper.rb +7 -18
- data/test/test_actor.rb +12 -3
- data/test/test_async_op.rb +10 -10
- data/test/test_fiber.rb +84 -1
- data/test/test_fiber_scheduler.rb +1425 -20
- data/test/test_um.rb +565 -113
- data/uringmachine.gemspec +6 -5
- data/vendor/liburing/src/include/liburing/io_uring.h +1 -0
- data/vendor/liburing/src/include/liburing.h +13 -0
- data/vendor/liburing/src/liburing-ffi.map +1 -0
- data/vendor/liburing/test/bind-listen.c +175 -13
- data/vendor/liburing/test/read-write.c +4 -4
- data/vendor/liburing/test/ringbuf-read.c +4 -4
- data/vendor/liburing/test/send_recv.c +8 -7
- metadata +50 -28
- data/examples/bm_fileno.rb +0 -33
- data/examples/bm_queue.rb +0 -110
- data/examples/bm_side_running.rb +0 -83
- data/examples/bm_sqlite.rb +0 -89
- data/examples/dns_client.rb +0 -12
- /data/{examples/bm_mutex.rb → benchmark/mutex.rb} +0 -0
- /data/{examples/bm_mutex_single.rb → benchmark/mutex_single.rb} +0 -0
- /data/{examples/bm_send.rb → benchmark/send.rb} +0 -0
- /data/{examples/bm_snooze.rb → benchmark/snooze.rb} +0 -0
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'resolv'
|
|
4
|
+
require 'etc'
|
|
5
|
+
require 'uringmachine'
|
|
6
|
+
|
|
3
7
|
class UringMachine
|
|
8
|
+
# Implements a worker thread pool for running blocking operations. Worker
|
|
9
|
+
# threads are started as needed. Worker thread count is limited to the number
|
|
10
|
+
# of CPU cores available.
|
|
11
|
+
class BlockingOperationThreadPool
|
|
12
|
+
|
|
13
|
+
# Initializes a new worker pool.
|
|
14
|
+
#
|
|
15
|
+
# @return [void]
|
|
16
|
+
def initialize
|
|
17
|
+
@pending_count = 0
|
|
18
|
+
@worker_count = 0
|
|
19
|
+
@max_workers = Etc.nprocessors
|
|
20
|
+
@worker_mutex = UM::Mutex.new
|
|
21
|
+
@job_queue = UM::Queue.new
|
|
22
|
+
@workers = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Processes a request by submitting it to the job queue and waiting for the
|
|
26
|
+
# return value. Starts a worker if needed.
|
|
27
|
+
#
|
|
28
|
+
# @param machine [UringMachine] machine
|
|
29
|
+
# @param job [any] callable job object
|
|
30
|
+
# @return [any] return value
|
|
31
|
+
def process(machine, job)
|
|
32
|
+
queue = Fiber.current.mailbox
|
|
33
|
+
if @worker_count == 0 || (@pending_count > 0 && @worker_count < @max_workers)
|
|
34
|
+
start_worker(machine)
|
|
35
|
+
end
|
|
36
|
+
machine.push(@job_queue, [queue, job])
|
|
37
|
+
machine.shift(queue)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# @param machine [UringMachine] machine
|
|
43
|
+
# @return [void]
|
|
44
|
+
def start_worker(machine)
|
|
45
|
+
machine.synchronize(@worker_mutex) do
|
|
46
|
+
return if @worker_count == @max_workers
|
|
47
|
+
|
|
48
|
+
@workers << Thread.new { run_worker_thread }
|
|
49
|
+
@worker_count += 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [void]
|
|
54
|
+
def run_worker_thread
|
|
55
|
+
machine = UM.new(4)
|
|
56
|
+
loop do
|
|
57
|
+
q, op = machine.shift(@job_queue)
|
|
58
|
+
@pending_count += 1
|
|
59
|
+
res = begin
|
|
60
|
+
op.()
|
|
61
|
+
rescue Exception => e
|
|
62
|
+
e
|
|
63
|
+
end
|
|
64
|
+
@pending_count -= 1
|
|
65
|
+
machine.push(q, res)
|
|
66
|
+
rescue => e
|
|
67
|
+
UM.debug("worker e: #{e.inspect}")
|
|
68
|
+
exit!
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
4
73
|
# UringMachine::FiberScheduler implements the Fiber::Scheduler interface for
|
|
5
74
|
# creating fiber-based concurrent applications in Ruby, in tight integration
|
|
6
75
|
# with the standard Ruby I/O and locking APIs.
|
|
7
76
|
class FiberScheduler
|
|
8
|
-
|
|
77
|
+
|
|
78
|
+
# The blocking operation thread pool is shared by all fiber schedulers.
|
|
79
|
+
@@blocking_operation_thread_pool = BlockingOperationThreadPool.new
|
|
80
|
+
|
|
81
|
+
# UringMachine instance associated with scheduler.
|
|
82
|
+
attr_reader :machine
|
|
83
|
+
|
|
84
|
+
# WeakMap holding references scheduler fibers as keys.
|
|
85
|
+
attr_reader :fiber_map
|
|
9
86
|
|
|
10
87
|
# Instantiates a scheduler with the given UringMachine instance.
|
|
11
88
|
#
|
|
@@ -17,48 +94,37 @@ class UringMachine
|
|
|
17
94
|
# @return [void]
|
|
18
95
|
def initialize(machine = nil)
|
|
19
96
|
@machine = machine || UM.new
|
|
20
|
-
@ios = ObjectSpace::WeakMap.new
|
|
21
97
|
@fiber_map = ObjectSpace::WeakMap.new
|
|
98
|
+
@thread = Thread.current
|
|
22
99
|
end
|
|
23
100
|
|
|
101
|
+
# :nodoc:
|
|
24
102
|
def instance_variables_to_inspect
|
|
25
103
|
[:@machine]
|
|
26
104
|
end
|
|
27
105
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
106
|
+
# Creates a new fiber with the given block. The created fiber is added to
|
|
107
|
+
# the fiber map, scheduled on the scheduler machine, and started before this
|
|
108
|
+
# method returns (by calling snooze).
|
|
30
109
|
#
|
|
31
|
-
# @return [
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
@ios = ObjectSpace::WeakMap.new
|
|
35
|
-
@fiber_map = ObjectSpace::WeakMap.new
|
|
36
|
-
self
|
|
37
|
-
end
|
|
110
|
+
# @param block [Proc] fiber block @return [Fiber]
|
|
111
|
+
def fiber(&block)
|
|
112
|
+
fiber = Fiber.new(blocking: false) { @machine.run(fiber, &block) }
|
|
38
113
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@machine.
|
|
42
|
-
|
|
43
|
-
super
|
|
114
|
+
@fiber_map[fiber] = true
|
|
115
|
+
@machine.schedule(fiber, nil)
|
|
116
|
+
@machine.snooze
|
|
117
|
+
fiber
|
|
44
118
|
end
|
|
45
119
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
120
|
+
# Waits for all fiber to terminate. Called upon thread termination or when
|
|
121
|
+
# the thread's fiber scheduler is changed.
|
|
48
122
|
#
|
|
49
123
|
# @return [void]
|
|
50
124
|
def scheduler_close
|
|
51
125
|
join()
|
|
52
126
|
end
|
|
53
127
|
|
|
54
|
-
# fiber_interrupt hook: to be implemented.
|
|
55
|
-
def fiber_interrupt(fiber, exception)
|
|
56
|
-
raise NotImplementedError, "Implement me!"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# For debugging purposes
|
|
60
|
-
def p(o) = UM.debug(o.inspect)
|
|
61
|
-
|
|
62
128
|
# Waits for the given fibers to terminate. If no fibers are given, waits for
|
|
63
129
|
# all fibers to terminate.
|
|
64
130
|
#
|
|
@@ -70,36 +136,36 @@ class UringMachine
|
|
|
70
136
|
@fiber_map = ObjectSpace::WeakMap.new
|
|
71
137
|
end
|
|
72
138
|
|
|
73
|
-
@machine.
|
|
139
|
+
@machine.await_fibers(fibers)
|
|
74
140
|
end
|
|
75
141
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
142
|
+
# Runs the given operation in a separate thread, so as not to block other
|
|
143
|
+
# fibers.
|
|
78
144
|
#
|
|
79
|
-
# @param
|
|
145
|
+
# @param op [callable] blocking operation
|
|
80
146
|
# @return [void]
|
|
81
|
-
def blocking_operation_wait(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
queue = UM::Queue.new
|
|
85
|
-
@machine.push(@blocking_op_queue, [queue, blocking_operation])
|
|
86
|
-
@machine.shift(queue)
|
|
147
|
+
def blocking_operation_wait(op)
|
|
148
|
+
@@blocking_operation_thread_pool.process(@machine, op)
|
|
87
149
|
end
|
|
88
150
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# etc.
|
|
151
|
+
# Blocks the current fiber by yielding to the machine. This hook is called
|
|
152
|
+
# when a synchronization mechanism blocks, e.g. a mutex, a queue, etc.
|
|
92
153
|
#
|
|
93
154
|
# @param blocker [any] blocker object
|
|
94
|
-
# @param timeout [Number, nil] optional
|
|
95
|
-
#
|
|
155
|
+
# @param timeout [Number, nil] optional timeout
|
|
156
|
+
# @return [bool] was the operation successful
|
|
96
157
|
def block(blocker, timeout = nil)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
158
|
+
if timeout
|
|
159
|
+
@machine.timeout(timeout, Timeout::Error) { @machine.yield }
|
|
160
|
+
else
|
|
161
|
+
@machine.yield
|
|
162
|
+
end
|
|
163
|
+
true
|
|
164
|
+
rescue Timeout::Error
|
|
165
|
+
false
|
|
100
166
|
end
|
|
101
167
|
|
|
102
|
-
#
|
|
168
|
+
# Unblocks the given fiber by scheduling it. This hook is
|
|
103
169
|
# called when a synchronization mechanism unblocks, e.g. a mutex, a queue,
|
|
104
170
|
# etc.
|
|
105
171
|
#
|
|
@@ -108,55 +174,112 @@ class UringMachine
|
|
|
108
174
|
# @return [void]
|
|
109
175
|
def unblock(blocker, fiber)
|
|
110
176
|
@machine.schedule(fiber, nil)
|
|
177
|
+
@machine.wakeup if Thread.current != @thread
|
|
111
178
|
end
|
|
112
179
|
|
|
113
|
-
#
|
|
180
|
+
# Sleeps for the given duration.
|
|
114
181
|
#
|
|
115
182
|
# @param duration [Number, nil] sleep duration
|
|
116
183
|
# @return [void]
|
|
117
184
|
def kernel_sleep(duration = nil)
|
|
118
|
-
|
|
119
|
-
@machine.sleep(duration)
|
|
120
|
-
else
|
|
121
|
-
@machine.yield
|
|
122
|
-
end
|
|
185
|
+
duration ? @machine.sleep(duration) : @machine.yield
|
|
123
186
|
end
|
|
124
187
|
|
|
125
|
-
#
|
|
188
|
+
# Yields to the next runnable fiber.
|
|
189
|
+
def yield
|
|
190
|
+
@machine.snooze
|
|
191
|
+
# @machine.yield
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Waits for the given io to become ready.
|
|
126
195
|
#
|
|
127
196
|
# @param io [IO] IO object
|
|
128
197
|
# @param events [Number] readiness bitmask
|
|
129
198
|
# @param timeout [Number, nil] optional timeout
|
|
130
199
|
# @param return
|
|
131
200
|
def io_wait(io, events, timeout = nil)
|
|
201
|
+
# p(io_wait: io, events:)
|
|
132
202
|
timeout ||= io.timeout
|
|
133
203
|
if timeout
|
|
134
204
|
@machine.timeout(timeout, Timeout::Error) {
|
|
135
|
-
@machine.poll(io.fileno, events)
|
|
205
|
+
@machine.poll(io.fileno, events)
|
|
136
206
|
}
|
|
137
207
|
else
|
|
138
|
-
@machine.poll(io.fileno, events)
|
|
208
|
+
@machine.poll(io.fileno, events)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
139
211
|
|
|
212
|
+
# Selects the first ready IOs from the given sets of IOs.
|
|
213
|
+
#
|
|
214
|
+
# @param rios [Array<IO>] readable IOs
|
|
215
|
+
# @param wios [Array<IO>] writable IOs
|
|
216
|
+
# @param eios [Array<IO>] exceptable IOs
|
|
217
|
+
# @param timeout [Number, nil] optional timeout
|
|
218
|
+
def io_select(rios, wios, eios, timeout = nil)
|
|
219
|
+
map_r = map_fds(rios)
|
|
220
|
+
map_w = map_fds(wios)
|
|
221
|
+
map_e = map_fds(eios)
|
|
222
|
+
|
|
223
|
+
r, w, e = nil
|
|
224
|
+
if timeout
|
|
225
|
+
@machine.timeout(timeout, Timeout::Error) {
|
|
226
|
+
r, w, e = @machine.select(map_r.keys, map_w.keys, map_e.keys)
|
|
227
|
+
}
|
|
228
|
+
else
|
|
229
|
+
r, w, e = @machine.select(map_r.keys, map_w.keys, map_e.keys)
|
|
140
230
|
end
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
raise
|
|
231
|
+
|
|
232
|
+
[unmap_fds(r, map_r), unmap_fds(w, map_w), unmap_fds(e, map_e)]
|
|
144
233
|
end
|
|
145
234
|
|
|
146
|
-
#
|
|
147
|
-
# added to the fiber map, scheduled on the scheduler machine, and started
|
|
148
|
-
# before this method returns (by calling snooze).
|
|
235
|
+
# Reads from the given IO.
|
|
149
236
|
#
|
|
150
|
-
# @param
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
237
|
+
# @param io [IO] IO object
|
|
238
|
+
# @param buffer [IO::Buffer] read buffer
|
|
239
|
+
# @param length [Integer] read length
|
|
240
|
+
# @param offset [Integer] buffer offset
|
|
241
|
+
# @return [Integer] bytes read
|
|
242
|
+
def io_read(io, buffer, length, offset)
|
|
243
|
+
length = buffer.size if length == 0
|
|
244
|
+
|
|
245
|
+
if (timeout = io.timeout)
|
|
246
|
+
@machine.timeout(timeout, Timeout::Error) do
|
|
247
|
+
@machine.read(io.fileno, buffer, length, offset)
|
|
248
|
+
rescue Errno::EINTR
|
|
249
|
+
retry
|
|
250
|
+
end
|
|
251
|
+
else
|
|
252
|
+
@machine.read(io.fileno, buffer, length, offset)
|
|
253
|
+
end
|
|
254
|
+
rescue Errno::EINTR
|
|
255
|
+
retry
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Reads from the given IO at the given file offset
|
|
259
|
+
#
|
|
260
|
+
# @param io [IO] IO object
|
|
261
|
+
# @param buffer [IO::Buffer] read buffer
|
|
262
|
+
# @param from [Integer] read offset
|
|
263
|
+
# @param length [Integer] read length
|
|
264
|
+
# @param offset [Integer] buffer offset
|
|
265
|
+
# @return [Integer] bytes read
|
|
266
|
+
def io_pread(io, buffer, from, length, offset)
|
|
267
|
+
length = buffer.size if length == 0
|
|
268
|
+
|
|
269
|
+
if (timeout = io.timeout)
|
|
270
|
+
@machine.timeout(timeout, Timeout::Error) do
|
|
271
|
+
@machine.read(io.fileno, buffer, length, offset, from)
|
|
272
|
+
rescue Errno::EINTR
|
|
273
|
+
retry
|
|
274
|
+
end
|
|
275
|
+
else
|
|
276
|
+
@machine.read(io.fileno, buffer, length, offset, from)
|
|
277
|
+
end
|
|
278
|
+
rescue Errno::EINTR
|
|
279
|
+
retry
|
|
157
280
|
end
|
|
158
281
|
|
|
159
|
-
#
|
|
282
|
+
# Writes to the given IO.
|
|
160
283
|
#
|
|
161
284
|
# @param io [IO] IO object
|
|
162
285
|
# @param buffer [IO::Buffer] write buffer
|
|
@@ -164,73 +287,123 @@ class UringMachine
|
|
|
164
287
|
# @param offset [Integer] write offset
|
|
165
288
|
# @return [Integer] bytes written
|
|
166
289
|
def io_write(io, buffer, length, offset)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
290
|
+
# p(io_write: io, length:, offset:, timeout: io.timeout)
|
|
291
|
+
length = buffer.size if length == 0
|
|
292
|
+
buffer = buffer.slice(offset) if offset > 0
|
|
170
293
|
|
|
171
|
-
|
|
172
|
-
|
|
294
|
+
if (timeout = io.timeout)
|
|
295
|
+
@machine.timeout(timeout, Timeout::Error) do
|
|
296
|
+
@machine.write(io.fileno, buffer, length)
|
|
297
|
+
rescue Errno::EINTR
|
|
298
|
+
retry
|
|
299
|
+
end
|
|
300
|
+
else
|
|
301
|
+
@machine.write(io.fileno, buffer, length)
|
|
302
|
+
end
|
|
173
303
|
rescue Errno::EINTR
|
|
174
304
|
retry
|
|
175
305
|
end
|
|
176
306
|
|
|
177
|
-
#
|
|
307
|
+
# Writes to the given IO at the given file offset.
|
|
178
308
|
#
|
|
179
309
|
# @param io [IO] IO object
|
|
180
|
-
# @param buffer [IO::Buffer]
|
|
181
|
-
# @param length [Integer]
|
|
182
|
-
# @param
|
|
183
|
-
# @
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
ensure_nonblock(io)
|
|
310
|
+
# @param buffer [IO::Buffer] write buffer
|
|
311
|
+
# @param length [Integer] file offset
|
|
312
|
+
# @param length [Integer] write length
|
|
313
|
+
# @param offset [Integer] buffer offset
|
|
314
|
+
# @return [Integer] bytes written
|
|
315
|
+
def io_pwrite(io, buffer, from, length, offset)
|
|
316
|
+
# p(io_pwrite: io, from:, length:, offset:, timeout: io.timeout)
|
|
190
317
|
length = buffer.size if length == 0
|
|
191
|
-
|
|
318
|
+
buffer = buffer.slice(offset) if offset > 0
|
|
319
|
+
|
|
320
|
+
if (timeout = io.timeout)
|
|
321
|
+
@machine.timeout(timeout, Timeout::Error) do
|
|
322
|
+
@machine.write(io.fileno, buffer, length, from)
|
|
323
|
+
rescue Errno::EINTR
|
|
324
|
+
retry
|
|
325
|
+
end
|
|
326
|
+
else
|
|
327
|
+
@machine.write(io.fileno, buffer, length, from)
|
|
328
|
+
end
|
|
192
329
|
rescue Errno::EINTR
|
|
193
330
|
retry
|
|
194
331
|
end
|
|
195
332
|
|
|
196
|
-
|
|
333
|
+
# Closes the given fd.
|
|
334
|
+
#
|
|
335
|
+
# @param fd [Integer] file descriptor
|
|
336
|
+
# @return [Integer] file descriptor
|
|
337
|
+
def io_close(fd)
|
|
338
|
+
# p(io_close: fd)
|
|
339
|
+
@machine.close_async(fd)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
if UM.method_defined?(:waitid_status)
|
|
343
|
+
|
|
344
|
+
# Waits for a process to terminate.
|
|
345
|
+
#
|
|
346
|
+
# @param pid [Integer] process pid (0 for any child process)
|
|
347
|
+
# @param flags [Integer] waitpid flags
|
|
348
|
+
# @return [Process::Status] terminated process status
|
|
197
349
|
def process_wait(pid, flags)
|
|
198
350
|
flags = UM::WEXITED if flags == 0
|
|
199
351
|
@machine.waitid_status(UM::P_PID, pid, flags)
|
|
200
352
|
end
|
|
201
353
|
end
|
|
202
354
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# Ensures the given IO is in blocking mode.
|
|
355
|
+
# Interrupts the given fiber with an exception.
|
|
206
356
|
#
|
|
207
|
-
# @param
|
|
357
|
+
# @param fiber [Fiber] fiber to interrupt
|
|
358
|
+
# @param exception [Exception] Exception
|
|
208
359
|
# @return [void]
|
|
209
|
-
def
|
|
210
|
-
|
|
360
|
+
def fiber_interrupt(fiber, exception)
|
|
361
|
+
@machine.schedule(fiber, exception)
|
|
362
|
+
@machine.wakeup
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Resolves an hostname.
|
|
366
|
+
#
|
|
367
|
+
# @param hostname [String] hostname to resolve
|
|
368
|
+
# @return [Array<Addrinfo>] array of resolved addresses
|
|
369
|
+
def address_resolve(hostname)
|
|
370
|
+
Resolv.getaddresses(hostname)
|
|
371
|
+
end
|
|
211
372
|
|
|
212
|
-
|
|
213
|
-
|
|
373
|
+
# Run the given block with a timeout.
|
|
374
|
+
#
|
|
375
|
+
# @param duration [Number] timeout duration
|
|
376
|
+
# @param exception [Class] exception Class
|
|
377
|
+
# @param message [String] exception message
|
|
378
|
+
# @param block [Proc] block to run
|
|
379
|
+
# @return [any] block return value
|
|
380
|
+
def timeout_after(duration, exception, message, &block)
|
|
381
|
+
@machine.timeout(duration, exception, &block)
|
|
214
382
|
end
|
|
215
383
|
|
|
216
|
-
|
|
384
|
+
private
|
|
385
|
+
|
|
386
|
+
# Prints the given object for debugging purposes.
|
|
217
387
|
#
|
|
388
|
+
# @param o [any]
|
|
218
389
|
# @return [void]
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
rescue Exception => e
|
|
228
|
-
e
|
|
229
|
-
end
|
|
230
|
-
m.push(q, res)
|
|
231
|
-
end
|
|
232
|
-
end
|
|
390
|
+
def p(o) = UM.debug(o.inspect)
|
|
391
|
+
|
|
392
|
+
# Maps the given ios to fds.
|
|
393
|
+
#
|
|
394
|
+
# @param ios [Array<IO>] IOs to map
|
|
395
|
+
# @return [Hash] hash mapping fds to IOs
|
|
396
|
+
def map_fds(ios)
|
|
397
|
+
ios.each_with_object({}) { |io, h| h[io.fileno] = io }
|
|
233
398
|
end
|
|
234
399
|
|
|
400
|
+
# Maps the given fds to IOs using the given fd-to-IO map.
|
|
401
|
+
#
|
|
402
|
+
# @param fds [Array<Integer>] fds to map
|
|
403
|
+
# @param map [Hash] hash mapping fds to IOs
|
|
404
|
+
# @return [Array<IO>] IOs corresponding to fds
|
|
405
|
+
def unmap_fds(fds, map)
|
|
406
|
+
fds.map { map[it] }
|
|
407
|
+
end
|
|
235
408
|
end
|
|
236
|
-
end
|
|
409
|
+
end
|
data/lib/uringmachine/version.rb
CHANGED
data/lib/uringmachine.rb
CHANGED
|
@@ -38,7 +38,7 @@ class UringMachine
|
|
|
38
38
|
results[f] = f.result
|
|
39
39
|
else
|
|
40
40
|
(pending ||= []) << f
|
|
41
|
-
queue ||=
|
|
41
|
+
queue ||= Fiber.current.mailbox
|
|
42
42
|
f.add_done_listener(queue)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -53,6 +53,35 @@ class UringMachine
|
|
|
53
53
|
fibers.size == 1 ? values.first : values
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def await_fibers(fibers)
|
|
57
|
+
if fibers.is_a?(Fiber)
|
|
58
|
+
f = fibers
|
|
59
|
+
if !f.done?
|
|
60
|
+
queue = Fiber.current.mailbox
|
|
61
|
+
f.add_done_listener(queue)
|
|
62
|
+
self.shift(queue)
|
|
63
|
+
end
|
|
64
|
+
return 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
queue = nil
|
|
68
|
+
pending = nil
|
|
69
|
+
fibers.each do |f|
|
|
70
|
+
if !f.done?
|
|
71
|
+
(pending ||= []) << f
|
|
72
|
+
queue ||= Fiber.current.mailbox
|
|
73
|
+
f.add_done_listener(queue)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
if pending
|
|
77
|
+
while !pending.empty?
|
|
78
|
+
f = self.shift(queue)
|
|
79
|
+
pending.delete(f)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
fibers.count
|
|
83
|
+
end
|
|
84
|
+
|
|
56
85
|
def resolve(hostname, type = :A)
|
|
57
86
|
@resolver ||= DNSResolver.new(self)
|
|
58
87
|
@resolver.resolve(hostname, type)
|
|
@@ -71,8 +100,8 @@ class UringMachine
|
|
|
71
100
|
@@fiber_map.delete(fiber)
|
|
72
101
|
self.notify_done_listeners(fiber)
|
|
73
102
|
|
|
74
|
-
#
|
|
75
|
-
self.
|
|
103
|
+
# switch away to a different fiber
|
|
104
|
+
self.switch
|
|
76
105
|
end
|
|
77
106
|
|
|
78
107
|
def notify_done_listeners(fiber)
|
data/test/helper.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative './coverage' if ENV['COVERAGE']
|
|
|
5
5
|
require 'uringmachine'
|
|
6
6
|
require 'socket'
|
|
7
7
|
require 'minitest/autorun'
|
|
8
|
+
require 'securerandom'
|
|
8
9
|
|
|
9
10
|
STDOUT.sync = true
|
|
10
11
|
STDERR.sync = true
|
|
@@ -64,30 +65,18 @@ class UMBaseTest < Minitest::Test
|
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
def teardown
|
|
67
|
-
|
|
68
|
+
pending_fibers = @machine.pending_fibers.keys
|
|
69
|
+
if pending_fibers.size > 0
|
|
70
|
+
raise "leaked fibers: #{pending_fibers}"
|
|
71
|
+
end
|
|
72
|
+
GC.start
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
def assign_port
|
|
71
76
|
@@port_assign_mutex ||= Mutex.new
|
|
72
77
|
@@port_assign_mutex.synchronize do
|
|
73
|
-
@@port ||= 1024 + rand(60000)
|
|
78
|
+
@@port ||= 1024 + SecureRandom.rand(60000)
|
|
74
79
|
@@port += 1
|
|
75
80
|
end
|
|
76
81
|
end
|
|
77
|
-
|
|
78
|
-
def make_socket_pair
|
|
79
|
-
port = 10000 + rand(30000)
|
|
80
|
-
server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
81
|
-
@machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
|
82
|
-
@machine.bind(server_fd, '127.0.0.1', port)
|
|
83
|
-
@machine.listen(server_fd, UM::SOMAXCONN)
|
|
84
|
-
|
|
85
|
-
client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
86
|
-
@machine.connect(client_conn_fd, '127.0.0.1', port)
|
|
87
|
-
|
|
88
|
-
server_conn_fd = @machine.accept(server_fd)
|
|
89
|
-
|
|
90
|
-
@machine.close(server_fd)
|
|
91
|
-
[client_conn_fd, server_conn_fd]
|
|
92
|
-
end
|
|
93
82
|
end
|
data/test/test_actor.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'helper'
|
|
4
|
-
require 'socket'
|
|
5
4
|
require 'uringmachine/actor'
|
|
6
5
|
|
|
7
6
|
class ActorTest < UMBaseTest
|
|
@@ -25,7 +24,7 @@ class ActorTest < UMBaseTest
|
|
|
25
24
|
|
|
26
25
|
def test_basic_actor_functionality
|
|
27
26
|
mailbox = UM::Queue.new
|
|
28
|
-
actor =
|
|
27
|
+
actor = machine.spin_actor(Counter)
|
|
29
28
|
|
|
30
29
|
assert_kind_of Fiber, actor
|
|
31
30
|
|
|
@@ -35,6 +34,11 @@ class ActorTest < UMBaseTest
|
|
|
35
34
|
assert_equal 2, actor.call(mailbox, :get)
|
|
36
35
|
assert_equal actor, actor.cast(:reset)
|
|
37
36
|
assert_equal 0, actor.call(mailbox, :get)
|
|
37
|
+
ensure
|
|
38
|
+
if actor
|
|
39
|
+
actor.stop
|
|
40
|
+
machine.join(actor)
|
|
41
|
+
end
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
module Counter2
|
|
@@ -56,10 +60,15 @@ class ActorTest < UMBaseTest
|
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
|
|
59
|
-
def
|
|
63
|
+
def test_spin_actor_with_args
|
|
60
64
|
actor = @machine.spin_actor(Counter2, 43)
|
|
61
65
|
mailbox = UM::Queue.new
|
|
62
66
|
|
|
63
67
|
assert_equal 43, actor.call(mailbox, :get)
|
|
68
|
+
ensure
|
|
69
|
+
if actor
|
|
70
|
+
actor.stop
|
|
71
|
+
machine.join(actor)
|
|
72
|
+
end
|
|
64
73
|
end
|
|
65
74
|
end
|