uringmachine 0.29.2 → 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 +6 -0
- data/TODO.md +24 -85
- 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/ext/um/um.c +51 -0
- data/ext/um/um.h +11 -5
- 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 +33 -1
- data/ext/um/um_stream_class.c +14 -0
- data/grant-2025/final-report.md +267 -0
- data/lib/uringmachine/version.rb +1 -1
- data/lib/uringmachine.rb +41 -0
- data/test/test_ssl.rb +27 -0
- data/test/test_stream.rb +33 -0
- data/test/test_um.rb +157 -0
- metadata +4 -1
data/ext/um/um_ssl.c
CHANGED
|
@@ -75,7 +75,7 @@ void um_ssl_set_bio(struct um *machine, VALUE ssl_obj)
|
|
|
75
75
|
SSL_set0_wbio(ssl, bio);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
int um_ssl_read_raw(struct um *machine, VALUE ssl_obj, char *ptr,
|
|
78
|
+
int um_ssl_read_raw(struct um *machine, VALUE ssl_obj, char *ptr, size_t maxlen) {
|
|
79
79
|
SSL *ssl = RTYPEDDATA_GET_DATA(ssl_obj);
|
|
80
80
|
|
|
81
81
|
int ret = SSL_read(ssl, ptr, maxlen);
|
|
@@ -84,22 +84,23 @@ int um_ssl_read_raw(struct um *machine, VALUE ssl_obj, char *ptr, int maxlen) {
|
|
|
84
84
|
return ret;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
int um_ssl_read(struct um *machine, VALUE ssl_obj, VALUE buf,
|
|
87
|
+
int um_ssl_read(struct um *machine, VALUE ssl_obj, VALUE buf, size_t maxlen) {
|
|
88
88
|
void *ptr = um_prepare_read_buffer(buf, maxlen, 0);
|
|
89
89
|
int ret = um_ssl_read_raw(machine, ssl_obj, ptr, maxlen);
|
|
90
90
|
um_update_read_buffer(buf, 0, ret);
|
|
91
91
|
return ret;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
int um_ssl_write(struct um *machine, VALUE ssl_obj, VALUE buf,
|
|
94
|
+
int um_ssl_write(struct um *machine, VALUE ssl_obj, VALUE buf, size_t len) {
|
|
95
95
|
SSL *ssl = RTYPEDDATA_GET_DATA(ssl_obj);
|
|
96
96
|
const void *base;
|
|
97
97
|
size_t size;
|
|
98
98
|
um_get_buffer_bytes_for_writing(buf, &base, &size, true);
|
|
99
|
-
if (
|
|
99
|
+
if (!len || (len > size)) len = size;
|
|
100
|
+
if (len > INT_MAX) len = INT_MAX;
|
|
100
101
|
if (unlikely(!len)) return INT2NUM(0);
|
|
101
102
|
|
|
102
|
-
int ret = SSL_write(ssl, base, len);
|
|
103
|
+
int ret = SSL_write(ssl, base, (int)len);
|
|
103
104
|
if (ret <= 0) rb_raise(eUMError, "Failed to write");
|
|
104
105
|
|
|
105
106
|
return ret;
|
data/ext/um/um_stream.c
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#include "um.h"
|
|
2
1
|
#include <stdlib.h>
|
|
2
|
+
#include <ruby/io/buffer.h>
|
|
3
|
+
#include "um.h"
|
|
3
4
|
|
|
4
5
|
inline void stream_add_segment(struct um_stream *stream, struct um_segment *segment) {
|
|
5
6
|
segment->next = NULL;
|
|
@@ -234,6 +235,13 @@ inline void stream_shift_head(struct um_stream *stream) {
|
|
|
234
235
|
stream->pos = 0;
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
inline VALUE make_segment_io_buffer(struct um_segment *segment, size_t pos) {
|
|
239
|
+
return rb_io_buffer_new(
|
|
240
|
+
segment->ptr + pos, segment->len - pos,
|
|
241
|
+
RB_IO_BUFFER_LOCKED|RB_IO_BUFFER_READONLY
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
237
245
|
inline void stream_skip(struct um_stream *stream, size_t inc, int safe_inc) {
|
|
238
246
|
if (unlikely(stream->eof && !stream->head)) return;
|
|
239
247
|
if (safe_inc && !stream->tail && !stream_get_more_segments(stream)) return;
|
|
@@ -254,6 +262,30 @@ inline void stream_skip(struct um_stream *stream, size_t inc, int safe_inc) {
|
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
|
|
265
|
+
inline void stream_each(struct um_stream *stream) {
|
|
266
|
+
if (unlikely(stream->eof && !stream->head)) return;
|
|
267
|
+
if (!stream->tail && !stream_get_more_segments(stream)) return;
|
|
268
|
+
|
|
269
|
+
struct um_segment *current = stream->head;
|
|
270
|
+
size_t pos = stream->pos;
|
|
271
|
+
|
|
272
|
+
VALUE buffer = Qnil;
|
|
273
|
+
while (true) {
|
|
274
|
+
struct um_segment *next = current->next;
|
|
275
|
+
buffer = make_segment_io_buffer(current, pos);
|
|
276
|
+
rb_yield(buffer);
|
|
277
|
+
rb_io_buffer_free_locked(buffer);
|
|
278
|
+
stream_shift_head(stream);
|
|
279
|
+
|
|
280
|
+
if (!next) {
|
|
281
|
+
if (!stream_get_more_segments(stream)) return;
|
|
282
|
+
}
|
|
283
|
+
current = stream->head;
|
|
284
|
+
pos = 0;
|
|
285
|
+
}
|
|
286
|
+
RB_GC_GUARD(buffer);
|
|
287
|
+
}
|
|
288
|
+
|
|
257
289
|
inline void stream_copy(struct um_stream *stream, char *dest, size_t len) {
|
|
258
290
|
while (len) {
|
|
259
291
|
char *segment_ptr = stream->head->ptr + stream->pos;
|
data/ext/um/um_stream_class.c
CHANGED
|
@@ -194,6 +194,19 @@ VALUE Stream_skip(VALUE self, VALUE len) {
|
|
|
194
194
|
return len;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/* call-seq:
|
|
198
|
+
* stream.each { |data| } -> stream
|
|
199
|
+
*
|
|
200
|
+
* Reads from the target, passing each chunk to the given block.
|
|
201
|
+
*
|
|
202
|
+
* @return [UringMachine::Stream] stream
|
|
203
|
+
*/
|
|
204
|
+
VALUE Stream_each(VALUE self) {
|
|
205
|
+
struct um_stream *stream = um_get_stream(self);
|
|
206
|
+
stream_each(stream);
|
|
207
|
+
return self;
|
|
208
|
+
}
|
|
209
|
+
|
|
197
210
|
/* call-seq:
|
|
198
211
|
* stream.resp_decode -> obj
|
|
199
212
|
*
|
|
@@ -286,6 +299,7 @@ void Init_Stream(void) {
|
|
|
286
299
|
rb_define_method(cStream, "get_line", Stream_get_line, 1);
|
|
287
300
|
rb_define_method(cStream, "get_string", Stream_get_string, 1);
|
|
288
301
|
rb_define_method(cStream, "skip", Stream_skip, 1);
|
|
302
|
+
rb_define_method(cStream, "each", Stream_each, 0);
|
|
289
303
|
|
|
290
304
|
rb_define_method(cStream, "resp_decode", Stream_resp_decode, 0);
|
|
291
305
|
rb_define_singleton_method(cStream, "resp_encode", Stream_resp_encode, 2);
|
|
@@ -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,23 @@ 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
|
|
197
214
|
def stream(target, mode = nil)
|
|
198
215
|
stream = UM::Stream.new(self, target, mode)
|
|
199
216
|
return stream if !block_given?
|
|
@@ -203,6 +220,30 @@ class UringMachine
|
|
|
203
220
|
res
|
|
204
221
|
end
|
|
205
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
|
+
|
|
206
247
|
private
|
|
207
248
|
|
|
208
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
|
@@ -251,6 +251,39 @@ class StreamTest < StreamBaseTest
|
|
|
251
251
|
assert_equal 9, received.size
|
|
252
252
|
assert_equal data, received.join
|
|
253
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
|
|
254
287
|
end
|
|
255
288
|
|
|
256
289
|
class StreamRespTest < StreamBaseTest
|
data/test/test_um.rb
CHANGED
|
@@ -3556,4 +3556,161 @@ class StreamMethodTest < UMBaseTest
|
|
|
3556
3556
|
assert_equal ['foo', 'bar'], bufs
|
|
3557
3557
|
assert_equal :foo, res
|
|
3558
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
|
|
3559
3716
|
end
|