uringmachine 0.29.1 → 0.30.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/CHANGELOG.md +12 -0
- data/README.md +106 -6
- data/TODO.md +24 -86
- data/benchmark/bm_io_ssl.rb +128 -0
- data/benchmark/bm_redis_client.rb +76 -0
- data/benchmark/common.rb +1 -0
- data/benchmark/http_parse.rb +3 -3
- data/docs/design/buffer_pool.md +1 -1
- data/ext/um/um.c +51 -0
- data/ext/um/um.h +13 -6
- data/ext/um/um_buffer_pool.c +11 -11
- data/ext/um/um_class.c +57 -0
- data/ext/um/um_ssl.c +6 -5
- data/ext/um/um_stream.c +46 -9
- data/ext/um/um_stream_class.c +126 -0
- data/grant-2025/final-report.md +267 -0
- data/lib/uringmachine/version.rb +1 -1
- data/lib/uringmachine.rb +50 -0
- data/test/test_ssl.rb +27 -0
- data/test/test_stream.rb +76 -0
- data/test/test_um.rb +209 -0
- metadata +4 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Final Report for Ruby Association Grant Program 2025
|
|
2
|
+
|
|
3
|
+
## Project Summary
|
|
4
|
+
|
|
5
|
+
Io_uring is a relatively new Linux API, permitting the asynchronous invocation
|
|
6
|
+
of Linux system calls. UringMachine is a gem that brings low-level access to the
|
|
7
|
+
io_uring interface to Ruby programs, and permits not only asynchronous I/O on
|
|
8
|
+
files and sockets, but also timeouts, futex wait/wake, statx and other system
|
|
9
|
+
calls, for use with fiber-based concurrency. This project aims to enhance
|
|
10
|
+
UringMachine to include a fiber scheduler implementation for usage with the
|
|
11
|
+
standard Ruby I/O classes, to have builtin support for SSL, to support more
|
|
12
|
+
io_uring ops such as writev, splice, fsync, mkdir, fadvise, etc.
|
|
13
|
+
|
|
14
|
+
I'd like to express my deep gratitude to Samuel Williams for his help and
|
|
15
|
+
guidance throughout this project.
|
|
16
|
+
|
|
17
|
+
## About UringMachine
|
|
18
|
+
|
|
19
|
+
UringMachine provides an API that closely follows the shape of various Linux
|
|
20
|
+
system calls for I/O operations, such as `open`, `read`, `accept`, `setsockopt`
|
|
21
|
+
etc. Behind the scenes, UringMachine uses io_uring to submit I/O operations and
|
|
22
|
+
automatically switch between fibers when waiting for those operations to
|
|
23
|
+
complete. Here's a simple example of an echo server using UringMachine:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require 'uringmachine'
|
|
27
|
+
|
|
28
|
+
machine = UM.new
|
|
29
|
+
server_fd = machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
30
|
+
machine.bind(server_fd, '0.0.0.0', 1234)
|
|
31
|
+
machine.listen(server_fd, UM::SOMAXCONN)
|
|
32
|
+
|
|
33
|
+
def handle_connection(machine, fd)
|
|
34
|
+
buf = +''
|
|
35
|
+
while true
|
|
36
|
+
res = machine.recv(fd, buf, 8192, 0)
|
|
37
|
+
break if res == 0
|
|
38
|
+
|
|
39
|
+
machine.send(fd, buf, res, 0)
|
|
40
|
+
end
|
|
41
|
+
ensure
|
|
42
|
+
machine.close(fd)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
while true
|
|
46
|
+
fd = machine.accept(server_fd)
|
|
47
|
+
machine.spin(fd) {
|
|
48
|
+
handle_connection(machine, it)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
UringMachine also provides some higher-level methods and abstractions that make
|
|
54
|
+
use of more advanced io_uring features such as [multishot
|
|
55
|
+
operations](https://man.archlinux.org/man/io_uring_multishot.7.en) (e.g.
|
|
56
|
+
`UM#accept_each`) and [provided buffer
|
|
57
|
+
rings](https://man.archlinux.org/man/io_uring_provided_buffers.7.en) (the
|
|
58
|
+
`UM::Stream` class). This not only results in more concise code, but also has
|
|
59
|
+
[performance](#benchmarking) benefits. In addition, UringMachine provides some
|
|
60
|
+
convenience methods, such as `tcp_listen` which returns a TCP socket fd bound to
|
|
61
|
+
the given address and ready for accepting incoming connections.
|
|
62
|
+
|
|
63
|
+
Here's the same echo server program but using UringMachine's more advanced
|
|
64
|
+
features:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
require 'uringmachine'
|
|
68
|
+
|
|
69
|
+
machine = UM.new
|
|
70
|
+
# Combined socket, bind, listen
|
|
71
|
+
server_fd = machine.tcp_listen('0.0.0.0', 1234)
|
|
72
|
+
|
|
73
|
+
def handle_connection(machine, fd)
|
|
74
|
+
stream = machine.stream(fd)
|
|
75
|
+
stream.each { |buf| machine.send(fd, buf, buf.size, 0) }
|
|
76
|
+
ensure
|
|
77
|
+
machine.close(fd)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
machine.accept_each(server_fd) do |fd|
|
|
81
|
+
machine.spin(fd) {
|
|
82
|
+
handle_connection(machine, it)
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
UringMachine also includes a fiber scheduler implementation that effectively
|
|
88
|
+
provides an alternative backend for performing I/O operations while still using
|
|
89
|
+
the same familiar API of `IO`, `Socket` and related standard classes:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
machine = UM.new
|
|
93
|
+
scheduler = UM::FiberScheduler.new(machine)
|
|
94
|
+
Fiber.set_scheduler(scheduler)
|
|
95
|
+
|
|
96
|
+
Fiber.schedule {
|
|
97
|
+
puts('Hello from UringMachine!')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# await all pending fibers
|
|
101
|
+
scheduler.join
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## The Work Done
|
|
105
|
+
|
|
106
|
+
### Improvements to the Ruby `Fiber::Scheduler` interface
|
|
107
|
+
|
|
108
|
+
- [PR](https://github.com/ruby/ruby/pull/15213) to expose
|
|
109
|
+
`rb_process_status_new` internal Ruby C API
|
|
110
|
+
(https://bugs.ruby-lang.org/issues/21704). This is needed in order to allow
|
|
111
|
+
FiberScheduler implementations to instantiate `Process::Status` objects in the
|
|
112
|
+
`#process_wait` hook. This PR is still not merged.
|
|
113
|
+
|
|
114
|
+
- [PR](https://github.com/ruby/ruby/pull/15385) to cleanup FiberScheduler and
|
|
115
|
+
fiber state in a forked process (https://bugs.ruby-lang.org/issues/21717).
|
|
116
|
+
This was merged into Ruby 4.0.
|
|
117
|
+
|
|
118
|
+
- [PR](https://github.com/ruby/ruby/pull/15609) to invoke FiberScheduler
|
|
119
|
+
`io_write` hook on IO flush (https://bugs.ruby-lang.org/issues/21789). This
|
|
120
|
+
was merged into Ruby 4.0.
|
|
121
|
+
|
|
122
|
+
- Found an issue while implementing the `#io_pwrite` hook, which resulted in a
|
|
123
|
+
[PR](https://github.com/ruby/ruby/pull/15428) submitted by Samuel Williams,
|
|
124
|
+
and merged into Ruby 4.0.
|
|
125
|
+
|
|
126
|
+
- Worked with Samuel Williams on how to implement the `#io_close` hook, which
|
|
127
|
+
resulted in a [PR](https://github.com/ruby/ruby/pull/15434) submitted by
|
|
128
|
+
Samuel and merged into Ruby 4.0.
|
|
129
|
+
|
|
130
|
+
- [PR](https://github.com/ruby/ruby/pull/15865) to add socket I/O hooks to the
|
|
131
|
+
FiberScheduler interface (https://bugs.ruby-lang.org/issues/21837). This PR is
|
|
132
|
+
currently in draft phase, waiting input from Samuel Williams.
|
|
133
|
+
|
|
134
|
+
### UringMachine `Fiber::Scheduler` Implementation
|
|
135
|
+
|
|
136
|
+
- I developed a [full
|
|
137
|
+
implementation](https://github.com/digital-fabric/uringmachine/blob/main/lib/uringmachine/fiber_scheduler.rb)
|
|
138
|
+
of the `Fiber::Scheduler` interface using UringMachine, with methods for all
|
|
139
|
+
hooks:
|
|
140
|
+
|
|
141
|
+
- `#scheduler_close`
|
|
142
|
+
- `#fiber`, `#yield`
|
|
143
|
+
- `#blocking_operation_wait`, `#block`, `#unblock`, `#fiber_interrupt`
|
|
144
|
+
- `#kernel_sleep`, `#timeout_after`
|
|
145
|
+
- `#io_read`, `#io_write`, `#io_pread`, `#io_pwrite`, `#io_close`
|
|
146
|
+
- `#io_wait`, `#io_select`
|
|
147
|
+
- `#process_wait` (relies on the `rb_process_status_new` PR)
|
|
148
|
+
- `#address_resolve`
|
|
149
|
+
|
|
150
|
+
- Wrote [extensive
|
|
151
|
+
tests](https://github.com/digital-fabric/uringmachine/blob/main/test/test_fiber_scheduler.rb)
|
|
152
|
+
for the UringMachine fiber scheduler.
|
|
153
|
+
|
|
154
|
+
### SSL Integration
|
|
155
|
+
|
|
156
|
+
- I've added UringMachine method that installs a [custom OpenSSL
|
|
157
|
+
BIO](https://github.com/digital-fabric/uringmachine/blob/main/ext/um/um_ssl.c)
|
|
158
|
+
on the given SSLSocket, as well as read/write methods that provide higher
|
|
159
|
+
throughput on SSL connections:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock, OpenSSL::SSL::SSLContext.new)
|
|
163
|
+
machine.ssl_set_bio(ssl)
|
|
164
|
+
msg = 'foo'
|
|
165
|
+
ssl.write(msg) # Performs IO using the UringMachine BIO
|
|
166
|
+
|
|
167
|
+
# Or: bypass the OpenSSL gem, directly invoking SSL_write and using the
|
|
168
|
+
# UringMachine BIO
|
|
169
|
+
machine.ssl_write(ssl, msg, msg.bytesize)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- I've also submitted a [PR to the OpenSSL
|
|
173
|
+
gem](https://github.com/ruby/openssl/pull/1000) that adds support for using a
|
|
174
|
+
custom BIO method for reading and writing on SSL connections.
|
|
175
|
+
|
|
176
|
+
### Buffered Reads Using io_uring Provided Buffers
|
|
177
|
+
|
|
178
|
+
- I developed the `Stream` API that uses io_uring [provided
|
|
179
|
+
buffers](https://man.archlinux.org/man/io_uring_provided_buffers.7.en) to
|
|
180
|
+
allow buffered reads (e.g. similar behavior to `IO#gets`, `IO#readpartial`)
|
|
181
|
+
while minimizing copying of data.
|
|
182
|
+
- The [buffer
|
|
183
|
+
pool](https://github.com/digital-fabric/uringmachine/blob/main/ext/um/um_buffer_pool.c):
|
|
184
|
+
UringMachine allocates and provides buffers for kernel usage in multishot
|
|
185
|
+
read/recv operations. An adaptive algorithm ensures that the kernel always has
|
|
186
|
+
enough buffer space for reading data. When a buffer is no longer used by the
|
|
187
|
+
kernel or the stream, it is recycled and eventually provided back to the
|
|
188
|
+
kernel.
|
|
189
|
+
- Streams are
|
|
190
|
+
[implemented](https://github.com/digital-fabric/uringmachine/blob/main/ext/um/um_stream.c)
|
|
191
|
+
as a linked list of buffer segments, directly referencing the buffers shared
|
|
192
|
+
with the kernel and referenced in CQEs.
|
|
193
|
+
- Stream read methods suitable for both line-based protocols and binary
|
|
194
|
+
protocols.
|
|
195
|
+
- An `:ssl` stream mode allows reading from a `SSLSocket` instead of from an fd,
|
|
196
|
+
using the custom SSL BIO discussed above.
|
|
197
|
+
|
|
198
|
+
### Other Improvements to UringMachine
|
|
199
|
+
|
|
200
|
+
- Improved various internal aspects of the C-extension: performance and
|
|
201
|
+
correctness of mutex and queue implementations.
|
|
202
|
+
|
|
203
|
+
- Added support for accepting instances of `IO::Buffer` as buffer for the
|
|
204
|
+
various I/O operations, in order to facilitate the `Fiber::Scheduler`
|
|
205
|
+
implementation.
|
|
206
|
+
|
|
207
|
+
- Added various methods for working with processes:
|
|
208
|
+
|
|
209
|
+
- `UringMachine#waitid`
|
|
210
|
+
- `UringMachine.pidfd_open`
|
|
211
|
+
- `UringMachine.pidfd_send_signal`
|
|
212
|
+
|
|
213
|
+
- Added detailed internal metrics.
|
|
214
|
+
|
|
215
|
+
- Added support for vectorized write/send using io_uring: `UringMachine#writev`
|
|
216
|
+
and `UringMachine#sendv`.
|
|
217
|
+
|
|
218
|
+
- Added support for `SQPOLL` mode - this io_uring mode lets us avoid entering
|
|
219
|
+
the kernel when submitting I/O operations as the kernel is busy polling the SQ
|
|
220
|
+
ring.
|
|
221
|
+
|
|
222
|
+
- Added support for sidecar mode: an auxiliary thread is used to enter the
|
|
223
|
+
kernel and wait for CQE's (I/O operation completion entries), letting the Ruby
|
|
224
|
+
thread avoid entering the kernel in order to wait for CQEs.
|
|
225
|
+
|
|
226
|
+
- Added support for watching file system events using the `inotify` interface.
|
|
227
|
+
- Methods for low-level work with `inotify`:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
fd = UM.inotify_init
|
|
231
|
+
wd = UM.inotify_add_watch(fd, '/tmp', UM::IN_CLOSE_WRITE)
|
|
232
|
+
IO.write('/tmp/foo.txt', 'foofoo')
|
|
233
|
+
events = machine.inotify_get_events(fd)
|
|
234
|
+
#=> { wd: wd, mask: UM::IN_CLOSE_WRITE, name: 'foo.txt' }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- A higher-level API for watching directories:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
mask = UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE
|
|
241
|
+
machine.file_watch(FileUtils.pwd, mask) { handle_fs_event(it) }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- Added more low-level methods for performing I/O operations supported by
|
|
245
|
+
io_uring: `splice`, `tee`, `fsync`.
|
|
246
|
+
|
|
247
|
+
### Benchmarking
|
|
248
|
+
|
|
249
|
+
- I did extensive benchmarking comparing different solutions for performing
|
|
250
|
+
concurrent I/O in Ruby:
|
|
251
|
+
|
|
252
|
+
- Using normal Ruby threads
|
|
253
|
+
- Using Samuel's [Async](https://github.com/socketry/async/) gem which
|
|
254
|
+
implements a `Fiber::Scheduler`
|
|
255
|
+
- Using the UringMachine `Fiber::Scheduler`
|
|
256
|
+
- Using the UringMachine low-level API
|
|
257
|
+
|
|
258
|
+
- The benchmarks simulate different kinds of workloads:
|
|
259
|
+
|
|
260
|
+
- Writing and reading from pipes
|
|
261
|
+
- Writing and reading from sockets
|
|
262
|
+
- Doing CPU-bound work synchronized by mutex
|
|
263
|
+
- Doing I/O-bound work synchronized by mutex
|
|
264
|
+
- Pushing and pulling items from queues
|
|
265
|
+
- Running queries on a PostgreSQL database
|
|
266
|
+
|
|
267
|
+
- The results are here: https://github.com/digital-fabric/uringmachine/blob/main/benchmark/README.md
|
data/lib/uringmachine/version.rb
CHANGED
data/lib/uringmachine.rb
CHANGED
|
@@ -194,6 +194,56 @@ class UringMachine
|
|
|
194
194
|
close_async(fd)
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
+
# call-seq:
|
|
198
|
+
# machine.stream(fd, mode = nil) -> stream
|
|
199
|
+
# machine.stream(fd, mode = nil) { |stream| }
|
|
200
|
+
#
|
|
201
|
+
# Creates a stream for reading from the given target fd or other object. The
|
|
202
|
+
# mode indicates the type of target and how it is read from:
|
|
203
|
+
#
|
|
204
|
+
# - :bp_read - read from the given fd using the buffer pool (default mode)
|
|
205
|
+
# - :bp_recv - receive from the given socket fd using the buffer pool
|
|
206
|
+
# - :ssl - read from the given SSL connection
|
|
207
|
+
#
|
|
208
|
+
# If a block is given, the block will be called with the stream object and the
|
|
209
|
+
# method will return the block's return value.
|
|
210
|
+
#
|
|
211
|
+
# @param target [Integer, OpenSSL::SSL::SSLSocket] fd or ssl connection
|
|
212
|
+
# @param mode [Symbol, nil] stream mode
|
|
213
|
+
# @return [UringMachine::Stream] stream object
|
|
214
|
+
def stream(target, mode = nil)
|
|
215
|
+
stream = UM::Stream.new(self, target, mode)
|
|
216
|
+
return stream if !block_given?
|
|
217
|
+
|
|
218
|
+
res = yield(stream)
|
|
219
|
+
stream.clear
|
|
220
|
+
res
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Creates, binds and sets up a TCP socket for listening on the given host and
|
|
224
|
+
# port.
|
|
225
|
+
#
|
|
226
|
+
# @param host [String] host IP address
|
|
227
|
+
# @param port [Integer] TCP port
|
|
228
|
+
# @return [Integer] socket fd
|
|
229
|
+
def tcp_listen(host, port)
|
|
230
|
+
fd = socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
231
|
+
bind(fd, host, port)
|
|
232
|
+
listen(fd, UM::SOMAXCONN)
|
|
233
|
+
fd
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Creates and connects a TCP socket to the given host and port.
|
|
237
|
+
#
|
|
238
|
+
# @param host [String] host IP address
|
|
239
|
+
# @param port [Integer] TCP port
|
|
240
|
+
# @return [Integer] socket fd
|
|
241
|
+
def tcp_connect(host, port)
|
|
242
|
+
fd = socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
243
|
+
connect(fd, host, port)
|
|
244
|
+
fd
|
|
245
|
+
end
|
|
246
|
+
|
|
197
247
|
private
|
|
198
248
|
|
|
199
249
|
# @param block [Proc]
|
data/test/test_ssl.rb
CHANGED
|
@@ -82,4 +82,31 @@ class SSLTest < UMBaseTest
|
|
|
82
82
|
sock1&.close rescue nil
|
|
83
83
|
sock2&.close rescue nil
|
|
84
84
|
end
|
|
85
|
+
|
|
86
|
+
def test_ssl_write_0
|
|
87
|
+
sock1, sock2 = UNIXSocket.pair
|
|
88
|
+
|
|
89
|
+
s1 = OpenSSL::SSL::SSLSocket.new(sock1, @server_ctx)
|
|
90
|
+
s1.sync_close = true
|
|
91
|
+
s2 = OpenSSL::SSL::SSLSocket.new(sock2, OpenSSL::SSL::SSLContext.new)
|
|
92
|
+
s2.sync_close = true
|
|
93
|
+
|
|
94
|
+
@machine.ssl_set_bio(s2)
|
|
95
|
+
assert_equal true, s2.instance_variable_get(:@__um_bio__)
|
|
96
|
+
|
|
97
|
+
t = Thread.new { s1.accept rescue nil }
|
|
98
|
+
|
|
99
|
+
assert_equal 0, @machine.metrics[:total_ops]
|
|
100
|
+
s2.connect
|
|
101
|
+
refute_equal 0, @machine.metrics[:total_ops]
|
|
102
|
+
|
|
103
|
+
assert_equal 6, @machine.ssl_write(s1, 'foobar', 0)
|
|
104
|
+
buf = +''
|
|
105
|
+
assert_equal 6, @machine.ssl_read(s2, buf, 6)
|
|
106
|
+
assert_equal 'foobar', buf
|
|
107
|
+
ensure
|
|
108
|
+
t&.join(0.1)
|
|
109
|
+
sock1&.close rescue nil
|
|
110
|
+
sock2&.close rescue nil
|
|
111
|
+
end
|
|
85
112
|
end
|
data/test/test_stream.rb
CHANGED
|
@@ -221,6 +221,16 @@ class StreamTest < StreamBaseTest
|
|
|
221
221
|
assert_nil stream.get_string(-3)
|
|
222
222
|
end
|
|
223
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
|
+
|
|
224
234
|
def test_stream_big_data
|
|
225
235
|
data = SecureRandom.random_bytes(300_000)
|
|
226
236
|
fiber = machine.spin {
|
|
@@ -241,6 +251,39 @@ class StreamTest < StreamBaseTest
|
|
|
241
251
|
assert_equal 9, received.size
|
|
242
252
|
assert_equal data, received.join
|
|
243
253
|
end
|
|
254
|
+
|
|
255
|
+
def test_stream_each
|
|
256
|
+
bufs = []
|
|
257
|
+
f = machine.spin do
|
|
258
|
+
bufs << :ready
|
|
259
|
+
stream.each {
|
|
260
|
+
assert_kind_of IO::Buffer, it
|
|
261
|
+
bufs << it.get_string
|
|
262
|
+
}
|
|
263
|
+
bufs << :done
|
|
264
|
+
rescue => e
|
|
265
|
+
p e
|
|
266
|
+
p e.backtrace
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
machine.snooze
|
|
270
|
+
assert_equal [:ready], bufs
|
|
271
|
+
|
|
272
|
+
machine.write(@wfd, 'foo')
|
|
273
|
+
machine.snooze
|
|
274
|
+
assert_equal [:ready, 'foo'], bufs
|
|
275
|
+
|
|
276
|
+
machine.write(@wfd, 'barb')
|
|
277
|
+
machine.snooze
|
|
278
|
+
assert_equal [:ready, 'foo', 'barb'], bufs
|
|
279
|
+
|
|
280
|
+
machine.close(@wfd)
|
|
281
|
+
machine.snooze
|
|
282
|
+
assert_equal [:ready, 'foo', 'barb', :done], bufs
|
|
283
|
+
ensure
|
|
284
|
+
machine.terminate(f)
|
|
285
|
+
machine.join(f)
|
|
286
|
+
end
|
|
244
287
|
end
|
|
245
288
|
|
|
246
289
|
class StreamRespTest < StreamBaseTest
|
|
@@ -593,3 +636,36 @@ class StreamModeTest < UMBaseTest
|
|
|
593
636
|
sock2&.close rescue nil
|
|
594
637
|
end
|
|
595
638
|
end
|
|
639
|
+
|
|
640
|
+
class StreamByteCountsTest < StreamBaseTest
|
|
641
|
+
def test_stream_byte_counts
|
|
642
|
+
machine.write(@wfd, "foobar")
|
|
643
|
+
|
|
644
|
+
assert_equal 0, stream.consumed
|
|
645
|
+
assert_equal 0, stream.pending
|
|
646
|
+
|
|
647
|
+
buf = stream.get_string(2)
|
|
648
|
+
assert_equal 'fo', buf
|
|
649
|
+
assert_equal 2, stream.consumed
|
|
650
|
+
assert_equal 4, stream.pending
|
|
651
|
+
|
|
652
|
+
buf = stream.get_string(3)
|
|
653
|
+
assert_equal 'oba', buf
|
|
654
|
+
assert_equal 5, stream.consumed
|
|
655
|
+
assert_equal 1, stream.pending
|
|
656
|
+
|
|
657
|
+
machine.write(@wfd, "abc\ndef")
|
|
658
|
+
machine.snooze
|
|
659
|
+
assert_equal 5, stream.consumed
|
|
660
|
+
assert_equal 1, stream.pending
|
|
661
|
+
|
|
662
|
+
buf = stream.get_line(0)
|
|
663
|
+
assert_equal 'rabc', buf
|
|
664
|
+
assert_equal 10, stream.consumed
|
|
665
|
+
assert_equal 3, stream.pending
|
|
666
|
+
|
|
667
|
+
stream.clear
|
|
668
|
+
assert_equal 10, stream.consumed
|
|
669
|
+
assert_equal 0, stream.pending
|
|
670
|
+
end
|
|
671
|
+
end
|
data/test/test_um.rb
CHANGED
|
@@ -3505,3 +3505,212 @@ class SetChildSubreaperTest < Minitest::Test
|
|
|
3505
3505
|
assert_equal grand_child_pid, res
|
|
3506
3506
|
end
|
|
3507
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
|
|
3560
|
+
|
|
3561
|
+
class TCPHelperMethodsTest < UMBaseTest
|
|
3562
|
+
def setup
|
|
3563
|
+
super
|
|
3564
|
+
@port = assign_port
|
|
3565
|
+
end
|
|
3566
|
+
|
|
3567
|
+
def test_tcp_listen
|
|
3568
|
+
server_fd = machine.tcp_listen('0.0.0.0', @port)
|
|
3569
|
+
assert_kind_of Integer, server_fd
|
|
3570
|
+
|
|
3571
|
+
fd_c = machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
|
3572
|
+
machine.connect(fd_c, '127.0.0.1', @port)
|
|
3573
|
+
|
|
3574
|
+
fd_s = machine.accept(server_fd)
|
|
3575
|
+
assert_equal 3, machine.send(fd_s, 'foo', 3, 0)
|
|
3576
|
+
|
|
3577
|
+
buf = +''
|
|
3578
|
+
machine.recv(fd_c, buf, 8192, 0)
|
|
3579
|
+
assert_equal 'foo', buf
|
|
3580
|
+
ensure
|
|
3581
|
+
machine.close(fd_s) rescue nil
|
|
3582
|
+
machine.close(fd_c) rescue nil
|
|
3583
|
+
machine.close(server_fd) rescue nil
|
|
3584
|
+
end
|
|
3585
|
+
|
|
3586
|
+
def test_tcp_listen_invalid_args
|
|
3587
|
+
assert_raises(TypeError) { machine.tcp_listen('0.0.0.0', :foo) }
|
|
3588
|
+
assert_raises(TypeError) { machine.tcp_listen(1234, 5678) }
|
|
3589
|
+
end
|
|
3590
|
+
|
|
3591
|
+
def test_tcp_connect
|
|
3592
|
+
server_fd = machine.tcp_listen('0.0.0.0', @port)
|
|
3593
|
+
assert_kind_of Integer, server_fd
|
|
3594
|
+
|
|
3595
|
+
fd_c = machine.tcp_connect('127.0.0.1', @port)
|
|
3596
|
+
assert_kind_of Integer, fd_c
|
|
3597
|
+
|
|
3598
|
+
fd_s = machine.accept(server_fd)
|
|
3599
|
+
assert_equal 3, machine.send(fd_s, 'foo', 3, 0)
|
|
3600
|
+
|
|
3601
|
+
buf = +''
|
|
3602
|
+
machine.recv(fd_c, buf, 8192, 0)
|
|
3603
|
+
assert_equal 'foo', buf
|
|
3604
|
+
ensure
|
|
3605
|
+
machine.close(fd_s) rescue nil
|
|
3606
|
+
machine.close(fd_c) rescue nil
|
|
3607
|
+
machine.close(server_fd) rescue nil
|
|
3608
|
+
end
|
|
3609
|
+
|
|
3610
|
+
def test_tcp_connect_invalid_args
|
|
3611
|
+
server_fd = machine.tcp_listen('0.0.0.0', @port)
|
|
3612
|
+
|
|
3613
|
+
assert_raises(Errno::ECONNREFUSED) {
|
|
3614
|
+
machine.tcp_connect('127.0.0.1', @port + 1)
|
|
3615
|
+
}
|
|
3616
|
+
assert_raises(TypeError) { machine.tcp_connect('127.0.0.1', :foo) }
|
|
3617
|
+
assert_raises(TypeError) { machine.tcp_connect(1234, 5678) }
|
|
3618
|
+
ensure
|
|
3619
|
+
machine.close(server_fd)
|
|
3620
|
+
end
|
|
3621
|
+
end
|
|
3622
|
+
|
|
3623
|
+
class SpliceTest < UMBaseTest
|
|
3624
|
+
def test_splice
|
|
3625
|
+
i1, o1 = UM.pipe
|
|
3626
|
+
i2, o2 = UM.pipe
|
|
3627
|
+
len = nil
|
|
3628
|
+
|
|
3629
|
+
f = machine.spin do
|
|
3630
|
+
len = machine.splice(i1, o2, 1000)
|
|
3631
|
+
ensure
|
|
3632
|
+
machine.close(o2)
|
|
3633
|
+
end
|
|
3634
|
+
|
|
3635
|
+
machine.write(o1, 'foobar')
|
|
3636
|
+
buf = +''
|
|
3637
|
+
machine.read(i2, buf, 1000)
|
|
3638
|
+
|
|
3639
|
+
assert_equal 'foobar', buf
|
|
3640
|
+
assert_equal 6, len
|
|
3641
|
+
ensure
|
|
3642
|
+
machine.terminate(f)
|
|
3643
|
+
machine.join(f)
|
|
3644
|
+
[i1, o1, i2, o2].each { machine.close(it) rescue nil }
|
|
3645
|
+
end
|
|
3646
|
+
|
|
3647
|
+
def test_splice_bad_args
|
|
3648
|
+
fn = "/tmp/um_#{SecureRandom.hex}"
|
|
3649
|
+
IO.write(fn, 'foo')
|
|
3650
|
+
f1 = machine.open(fn, UM::O_RDWR)
|
|
3651
|
+
f2 = machine.open(fn, UM::O_RDWR)
|
|
3652
|
+
assert_raises(Errno::EINVAL) { machine.splice(f1, f2, 1000) }
|
|
3653
|
+
|
|
3654
|
+
i1, o1 = UM.pipe
|
|
3655
|
+
i2, o2 = UM.pipe
|
|
3656
|
+
machine.write(o1, 'foo')
|
|
3657
|
+
|
|
3658
|
+
assert_raises(Errno::EBADF) { machine.splice(i1, o2 + 1, 1000) }
|
|
3659
|
+
ensure
|
|
3660
|
+
[f1, f2, i1, o1, i2, o2].each { machine.close(it) rescue nil }
|
|
3661
|
+
end
|
|
3662
|
+
end
|
|
3663
|
+
|
|
3664
|
+
class TeeTest < UMBaseTest
|
|
3665
|
+
def test_tee
|
|
3666
|
+
i_src, o_src = UM.pipe
|
|
3667
|
+
i_dest1, o_dest1 = UM.pipe
|
|
3668
|
+
i_dest2, o_dest2 = UM.pipe
|
|
3669
|
+
|
|
3670
|
+
len1 = len2 = nil
|
|
3671
|
+
|
|
3672
|
+
f = machine.spin do
|
|
3673
|
+
len1 = machine.tee(i_src, o_dest1, 1000)
|
|
3674
|
+
len2 = machine.splice(i_src, o_dest2, 1000)
|
|
3675
|
+
ensure
|
|
3676
|
+
machine.close(o_dest1)
|
|
3677
|
+
machine.close(o_dest2)
|
|
3678
|
+
end
|
|
3679
|
+
|
|
3680
|
+
machine.write(o_src, 'foobar')
|
|
3681
|
+
machine.close(o_src)
|
|
3682
|
+
result1 = +''
|
|
3683
|
+
machine.read(i_dest1, result1, 1000)
|
|
3684
|
+
result2 = +''
|
|
3685
|
+
machine.read(i_dest2, result2, 1000)
|
|
3686
|
+
|
|
3687
|
+
assert_equal 'foobar', result1
|
|
3688
|
+
assert_equal 6, len1
|
|
3689
|
+
|
|
3690
|
+
assert_equal 'foobar', result2
|
|
3691
|
+
assert_equal 6, len2
|
|
3692
|
+
ensure
|
|
3693
|
+
machine.terminate(f)
|
|
3694
|
+
machine.join(f)
|
|
3695
|
+
[i_src, o_src, i_dest1, o_dest1, i_dest2, o_dest2].each {
|
|
3696
|
+
machine.close(it) rescue nil
|
|
3697
|
+
}
|
|
3698
|
+
end
|
|
3699
|
+
end
|
|
3700
|
+
|
|
3701
|
+
class FsyncTest < UMBaseTest
|
|
3702
|
+
def test_fsync
|
|
3703
|
+
fn = "/tmp/um_#{SecureRandom.hex}"
|
|
3704
|
+
fd = machine.open(fn, UM::O_CREAT | UM::O_WRONLY)
|
|
3705
|
+
|
|
3706
|
+
machine.write(fd, 'foo')
|
|
3707
|
+
assert_equal 0, machine.fsync(fd)
|
|
3708
|
+
ensure
|
|
3709
|
+
machine.close(fd)
|
|
3710
|
+
FileUtils.rm(fn) rescue nil
|
|
3711
|
+
end
|
|
3712
|
+
|
|
3713
|
+
def test_fsync_bad_args
|
|
3714
|
+
assert_raises(Errno::EINVAL) { machine.fsync(1) }
|
|
3715
|
+
end
|
|
3716
|
+
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.
|
|
4
|
+
version: 0.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sharon Rosner
|
|
@@ -58,10 +58,12 @@ files:
|
|
|
58
58
|
- benchmark/README.md
|
|
59
59
|
- benchmark/bm_io_pipe.rb
|
|
60
60
|
- benchmark/bm_io_socketpair.rb
|
|
61
|
+
- benchmark/bm_io_ssl.rb
|
|
61
62
|
- benchmark/bm_mutex_cpu.rb
|
|
62
63
|
- benchmark/bm_mutex_io.rb
|
|
63
64
|
- benchmark/bm_pg_client.rb
|
|
64
65
|
- benchmark/bm_queue.rb
|
|
66
|
+
- benchmark/bm_redis_client.rb
|
|
65
67
|
- benchmark/chart_all.png
|
|
66
68
|
- benchmark/chart_bm_io_pipe_x.png
|
|
67
69
|
- benchmark/common.rb
|
|
@@ -119,6 +121,7 @@ files:
|
|
|
119
121
|
- ext/um/um_stream_class.c
|
|
120
122
|
- ext/um/um_sync.c
|
|
121
123
|
- ext/um/um_utils.c
|
|
124
|
+
- grant-2025/final-report.md
|
|
122
125
|
- grant-2025/interim-report.md
|
|
123
126
|
- grant-2025/journal.md
|
|
124
127
|
- grant-2025/tasks.md
|