uringmachine 0.26.0 → 0.28.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.
data/ext/um/um_op.c CHANGED
@@ -18,9 +18,11 @@ const char * um_op_kind_name(enum um_op_kind kind) {
18
18
  case OP_STATX: return "OP_STATX";
19
19
  case OP_ACCEPT: return "OP_ACCEPT";
20
20
  case OP_RECV: return "OP_RECV";
21
+ case OP_RECVMSG: return "OP_RECVMSG";
21
22
  case OP_SEND: return "OP_SEND";
22
- case OP_SENDV: return "OP_SENDV";
23
23
  case OP_SEND_BUNDLE: return "OP_SEND_BUNDLE";
24
+ case OP_SENDMSG: return "OP_SENDMSG";
25
+ case OP_SENDV: return "OP_SENDV";
24
26
  case OP_SOCKET: return "OP_SOCKET";
25
27
  case OP_CONNECT: return "OP_CONNECT";
26
28
  case OP_BIND: return "OP_BIND";
data/ext/um/um_stream.c CHANGED
@@ -19,6 +19,7 @@ static inline void stream_check_truncate_buffer(struct um_stream *stream) {
19
19
  }
20
20
  }
21
21
 
22
+ // returns true if eof
22
23
  int stream_read_more(struct um_stream *stream) {
23
24
  stream_check_truncate_buffer(stream);
24
25
 
@@ -104,6 +105,16 @@ VALUE stream_get_string(struct um_stream *stream, VALUE buf, ssize_t len) {
104
105
  return buf;
105
106
  }
106
107
 
108
+ VALUE stream_skip(struct um_stream *stream, size_t len) {
109
+ while (stream->len - stream->pos < len)
110
+ if (!stream_read_more(stream)) {
111
+ return Qnil;
112
+ }
113
+
114
+ stream->pos += len;
115
+ return NUM2INT(len);
116
+ }
117
+
107
118
  VALUE resp_get_line(struct um_stream *stream, VALUE out_buffer) {
108
119
  char *start = RSTRING_PTR(stream->buffer) + stream->pos;
109
120
  while (true) {
@@ -65,6 +65,14 @@ VALUE Stream_get_string(VALUE self, VALUE buf, VALUE len) {
65
65
  return stream_get_string(stream, buf, NUM2LONG(len));
66
66
  }
67
67
 
68
+ // skips `len` bytes in the stream. This function returns nil if eof is
69
+ // encountered.
70
+ VALUE Stream_skip(VALUE self, VALUE len) {
71
+ struct um_stream *stream = Stream_data(self);
72
+
73
+ return stream_skip(stream, NUM2LONG(len));
74
+ }
75
+
68
76
  VALUE Stream_resp_decode(VALUE self) {
69
77
  struct um_stream *stream = Stream_data(self);
70
78
  if (unlikely(stream->eof)) return Qnil;
@@ -104,6 +112,7 @@ void Init_Stream(void) {
104
112
 
105
113
  rb_define_method(cStream, "get_line", Stream_get_line, 2);
106
114
  rb_define_method(cStream, "get_string", Stream_get_string, 2);
115
+ rb_define_method(cStream, "skip", Stream_skip, 1);
107
116
 
108
117
  rb_define_method(cStream, "resp_decode", Stream_resp_decode, 0);
109
118
 
data/grant-2025/tasks.md CHANGED
@@ -129,10 +129,10 @@
129
129
 
130
130
  - [v] Postgres test
131
131
 
132
- - [ ] Ruby Fiber::Scheduler interface
132
+ - [v] Ruby Fiber::Scheduler interface
133
133
  - [v] Make a PR for resetting the scheduler and resetting the fiber non-blocking flag.
134
134
  - [v] hook for close
135
- - [ ] hooks for send/recv/sendmsg/recvmsg
135
+ - [x] hooks for send/recv/sendmsg/recvmsg (Ruby core PR pending for send/recv)
136
136
 
137
137
  - [v] SSL
138
138
  - [v] setup custom BIO
@@ -147,22 +147,9 @@
147
147
  - [ ] docs (similar to papercraft docs)
148
148
 
149
149
  - [ ] Uma - web server
150
- - [ ] child process workers
151
- - [ ] reforking (following https://github.com/Shopify/pitchfork)
152
- see also: https://byroot.github.io/ruby/performance/2025/03/04/the-pitchfork-story.html
153
- - Monitor worker memory usage - how much is shared
154
- - Choose worker with most served request count as "mold" for next generation
155
- - Perform GC out of band, preferably when there are no active requests
156
- https://railsatscale.com/2024-10-23-next-generation-oob-gc/
157
- - When a worker is promoted to "mold", it:
158
- - Stops `accept`ing requests
159
- - When finally idle, calls `Process.warmup`
160
- - Starts replacing sibling workers with forked workers
161
- see also: https://www.youtube.com/watch?v=kAW5O2dkSU8
162
- - [ ] Each worker is single-threaded (except for worker threads)
163
150
  - [ ] Rack 3.0-compatible
164
151
  see: https://github.com/socketry/protocol-rack
165
152
  - [ ] Rails integration (Railtie)
166
153
  see: https://github.com/socketry/falcon
167
154
  - [ ] Benchmarks
168
- - [ ] Add to the TechEmpower bencchmarks
155
+ - [ ] Add to the TechEmpower benchmarks
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '0.26.0'
4
+ VERSION = '0.28.0'
5
5
  end
data/lib/uringmachine.rb CHANGED
@@ -36,6 +36,11 @@ class UringMachine
36
36
  fiber
37
37
  end
38
38
 
39
+ def terminate(*fibers)
40
+ exception = UM::Terminate.new
41
+ fibers.each { schedule(it, exception) }
42
+ end
43
+
39
44
  # Runs the given block in the given fiber. This method is used to run fibers
40
45
  # indirectly.
41
46
  #
@@ -60,7 +60,7 @@ class AsyncOpTest < UMBaseTest
60
60
  assert_equal true, @op.cancelled?
61
61
  end
62
62
 
63
- class TOError < RuntimeError; end
63
+ class TOError < StandardError; end
64
64
 
65
65
  def test_async_op_await_with_timeout
66
66
  e = nil
data/test/test_stream.rb CHANGED
@@ -98,6 +98,23 @@ class StreamTest < StreamBaseTest
98
98
  ret = @stream.get_string(nil, -4)
99
99
  assert_nil ret
100
100
  end
101
+
102
+ def test_skip
103
+ machine.write(@wfd, "foobar")
104
+ machine.close(@wfd)
105
+
106
+ ret = @stream.get_string(nil, 2)
107
+ assert_equal 'fo', ret
108
+
109
+ ret = @stream.skip(2)
110
+ assert_equal 2, ret
111
+
112
+ ret = @stream.get_string(nil, 2)
113
+ assert_equal 'ar', ret
114
+
115
+ ret = @stream.skip(2)
116
+ assert_nil ret
117
+ end
101
118
  end
102
119
 
103
120
  class StreamRespTest < StreamBaseTest
data/test/test_um.rb CHANGED
@@ -238,7 +238,38 @@ class ScheduleTest < UMBaseTest
238
238
  assert_in_range 0..0.1, t1 - t0
239
239
  end
240
240
 
241
- class TOError < RuntimeError; end
241
+ def test_terminate
242
+ e = nil
243
+ f = machine.spin do
244
+ machine.sleep(1)
245
+ rescue UM::Terminate => e
246
+ end
247
+
248
+ machine.snooze
249
+ machine.terminate(f)
250
+ machine.snooze
251
+ assert_kind_of UM::Terminate, e
252
+ assert_equal true, f.done?
253
+ end
254
+
255
+ def test_terminate_multi
256
+ f1 = machine.spin { machine.sleep(1) }
257
+ f2 = machine.spin { machine.sleep(1) }
258
+ f3 = machine.spin { machine.sleep(1) }
259
+
260
+ machine.snooze
261
+ machine.terminate(f1, f2)
262
+ machine.snooze
263
+ assert_equal true, f1.done?
264
+ assert_equal true, f2.done?
265
+ assert_nil f3.done?
266
+
267
+ machine.terminate(f3)
268
+ machine.snooze
269
+ assert_equal true, f3.done?
270
+ end
271
+
272
+ class TOError < StandardError; end
242
273
 
243
274
  def test_timeout
244
275
  buf = []
@@ -299,8 +330,8 @@ class ScheduleTest < UMBaseTest
299
330
  assert_equal 0, machine.metrics[:ops_pending]
300
331
  end
301
332
 
302
- class TO2Error < RuntimeError; end
303
- class TO3Error < RuntimeError; end
333
+ class TO2Error < StandardError; end
334
+ class TO3Error < StandardError; end
304
335
 
305
336
  def test_timeout_nested
306
337
  e = nil
@@ -351,7 +382,7 @@ class SleepTest < UMBaseTest
351
382
  assert_kind_of C, ret
352
383
  end
353
384
 
354
- class D < RuntimeError; end
385
+ class D < StandardError; end
355
386
 
356
387
  def test_sleep_with_timeout
357
388
  t0 = monotonic_clock
@@ -733,7 +764,7 @@ class ReadEachTest < UMBaseTest
733
764
  class TOError < StandardError; end
734
765
 
735
766
  def test_read_each_timeout
736
- r, w = IO.pipe
767
+ r, _w = IO.pipe
737
768
  bgid = machine.setup_buffer_ring(4096, 1024)
738
769
 
739
770
  bufs = []
@@ -955,7 +986,7 @@ class WritevTest < UMBaseTest
955
986
  machine.join(f)
956
987
  end
957
988
 
958
- class TOError < RuntimeError; end
989
+ class TOError < StandardError; end
959
990
 
960
991
  def test_writev_timeout
961
992
  r, w = UM.pipe
@@ -1344,7 +1375,7 @@ class AcceptEachTest < UMBaseTest
1344
1375
  def test_accept_each_closed
1345
1376
  count = 0
1346
1377
  done = nil
1347
- f = @machine.spin do
1378
+ @machine.spin do
1348
1379
  machine.accept_each(@server.fileno) do |fd|
1349
1380
  count += 1
1350
1381
  end
@@ -1639,8 +1670,6 @@ class SendvTest < UMBaseTest
1639
1670
  end
1640
1671
 
1641
1672
  def test_sendv
1642
- skip "Unavailable on kernel version < 6.17" if UM.kernel_version < 617
1643
-
1644
1673
  ret = machine.sendv(@s1, 'foo', 'bar', 'baz')
1645
1674
  assert_equal 9, ret
1646
1675
 
@@ -1653,8 +1682,6 @@ class SendvTest < UMBaseTest
1653
1682
  end
1654
1683
 
1655
1684
  def test_sendv_io_buffer
1656
- skip "Unavailable on kernel version < 6.17" if UM.kernel_version < 617
1657
-
1658
1685
  buf1 = IO::Buffer.new(6)
1659
1686
  buf1.set_string('foobar')
1660
1687
 
@@ -1673,8 +1700,6 @@ class SendvTest < UMBaseTest
1673
1700
  end
1674
1701
 
1675
1702
  def test_sendv_invalid_buffer
1676
- skip "Unavailable on kernel version < 6.17" if UM.kernel_version < 617
1677
-
1678
1703
  assert_raises(UM::Error) { machine.sendv(@s1, [], 'abc') }
1679
1704
  assert_equal 0, machine.metrics[:ops_pending]
1680
1705
  assert_equal 0, machine.metrics[:ops_free]
@@ -1816,7 +1841,7 @@ class RecvEachTest < UMBaseTest
1816
1841
 
1817
1842
  def test_recv_each_timeout
1818
1843
  t = Thread.new do
1819
- conn = @server.accept
1844
+ @server.accept
1820
1845
  sleep
1821
1846
  end
1822
1847
 
@@ -1847,7 +1872,7 @@ class RecvEachTest < UMBaseTest
1847
1872
 
1848
1873
  def test_recv_each_shutdown
1849
1874
  t = Thread.new do
1850
- conn = @server.accept
1875
+ @server.accept
1851
1876
  sleep
1852
1877
  end
1853
1878
 
@@ -1861,7 +1886,7 @@ class RecvEachTest < UMBaseTest
1861
1886
  bufs = []
1862
1887
  e = nil
1863
1888
 
1864
- f = machine.spin {
1889
+ machine.spin {
1865
1890
  machine.sleep(0.01)
1866
1891
  machine.shutdown(fd, UM::SHUT_RDWR)
1867
1892
  }
@@ -1881,6 +1906,244 @@ class RecvEachTest < UMBaseTest
1881
1906
  end
1882
1907
  end
1883
1908
 
1909
+ class SendRecvFdTest < UMBaseTest
1910
+ def setup
1911
+ @s1_fd, @s2_fd = UM.socketpair(UM::AF_UNIX, UM::SOCK_STREAM, 0)
1912
+ super
1913
+ end
1914
+
1915
+ def teardown
1916
+ [@s1_fd, @s2_fd].each { machine.close(it) rescue nil }
1917
+ super
1918
+ end
1919
+
1920
+ class TOError < StandardError; end
1921
+
1922
+ def test_send_recv_fd
1923
+ r_fd, w_fd = UM.pipe
1924
+
1925
+ res = machine.send_fd(@s1_fd, w_fd)
1926
+ assert_equal w_fd, res
1927
+
1928
+ fd = machine.recv_fd(@s2_fd)
1929
+ assert_kind_of Integer, fd
1930
+ refute_equal w_fd, fd
1931
+
1932
+ machine.close(w_fd)
1933
+ machine.write(fd, 'foobar')
1934
+ machine.close(fd)
1935
+
1936
+ buf = +''
1937
+ res = machine.read(r_fd, buf, 12)
1938
+ assert_equal 6, res
1939
+ assert_equal 'foobar', buf
1940
+ end
1941
+
1942
+ def test_send_recv_fd_multi
1943
+ r_fd, w_fd = UM.pipe
1944
+
1945
+ res = machine.send_fd(@s1_fd, w_fd)
1946
+ assert_equal w_fd, res
1947
+
1948
+ w_fd2 = machine.recv_fd(@s2_fd)
1949
+ assert_kind_of Integer, w_fd2
1950
+ refute_equal w_fd, w_fd2
1951
+
1952
+ res = machine.send_fd(@s1_fd, r_fd)
1953
+ assert_equal r_fd, res
1954
+
1955
+ r_fd2 = machine.recv_fd(@s2_fd)
1956
+ assert_kind_of Integer, r_fd2
1957
+ refute_equal r_fd, r_fd2
1958
+
1959
+ machine.close(w_fd)
1960
+ machine.write(w_fd2, 'foobar')
1961
+ machine.close(w_fd2)
1962
+
1963
+ buf = +''
1964
+ res = machine.read(r_fd, buf, 12)
1965
+ assert_equal 6, res
1966
+ assert_equal 'foobar', buf
1967
+ ensure
1968
+ machine.close(r_fd) if r_fd rescue nil
1969
+ machine.close(w_fd) if w_fd rescue nil
1970
+ machine.close(r_fd2) if r_fd2 rescue nil
1971
+ machine.close(w_fd2) if w_fd2 rescue nil
1972
+
1973
+ end
1974
+
1975
+ def test_send_recv_fd_mixed_multi
1976
+ r_fd, w_fd = UM.pipe
1977
+
1978
+ machine.send(@s1_fd, 'hi', 2, 0)
1979
+ buf = +''
1980
+ ret = machine.recv(@s2_fd, buf, 2, 0)
1981
+ assert_equal 2, ret
1982
+ assert_equal 'hi', buf
1983
+
1984
+ res = machine.send_fd(@s1_fd, w_fd)
1985
+ assert_equal w_fd, res
1986
+
1987
+ w_fd2 = machine.recv_fd(@s2_fd)
1988
+ assert_kind_of Integer, w_fd2
1989
+ refute_equal w_fd, w_fd2
1990
+
1991
+ res = machine.send_fd(@s1_fd, r_fd)
1992
+ assert_equal r_fd, res
1993
+
1994
+ r_fd2 = machine.recv_fd(@s2_fd)
1995
+ assert_kind_of Integer, r_fd2
1996
+ refute_equal r_fd, r_fd2
1997
+
1998
+ machine.close(w_fd)
1999
+ machine.write(w_fd2, 'foobar')
2000
+ machine.close(w_fd2)
2001
+
2002
+ buf = +''
2003
+ res = machine.read(r_fd, buf, 12)
2004
+ assert_equal 6, res
2005
+ assert_equal 'foobar', buf
2006
+ ensure
2007
+ machine.close(r_fd) if r_fd rescue nil
2008
+ machine.close(w_fd) if w_fd rescue nil
2009
+ machine.close(r_fd2) if r_fd2 rescue nil
2010
+ machine.close(w_fd2) if w_fd2 rescue nil
2011
+
2012
+ end
2013
+
2014
+ def test_send_recv_fd_inverse
2015
+ r_fd, w_fd = UM.pipe
2016
+
2017
+ res = machine.send_fd(@s2_fd, w_fd)
2018
+ assert_equal w_fd, res
2019
+
2020
+ fd = machine.recv_fd(@s1_fd)
2021
+ assert_kind_of Integer, fd
2022
+ refute_equal w_fd, fd
2023
+
2024
+ machine.close(w_fd)
2025
+ machine.write(fd, 'foobar')
2026
+ machine.close(fd)
2027
+
2028
+ buf = +''
2029
+ res = machine.read(r_fd, buf, 12)
2030
+ assert_equal 6, res
2031
+ assert_equal 'foobar', buf
2032
+ end
2033
+
2034
+ def test_send_recv_fd_fork
2035
+ pid = fork do
2036
+ m = UM.new
2037
+ fd = m.recv_fd(@s2_fd)
2038
+ m.write(fd, 'Hello!')
2039
+ ensure
2040
+ m.close(fd) rescue nil
2041
+ end
2042
+
2043
+ r_fd, w_fd = UM.pipe
2044
+
2045
+ res = machine.send_fd(@s1_fd, w_fd)
2046
+ assert_equal w_fd, res
2047
+ Process.wait(pid)
2048
+ pid = nil
2049
+ machine.close(w_fd)
2050
+
2051
+ buf = +''
2052
+ len = machine.timeout(1, TOError) { machine.read(r_fd, buf, 12) }
2053
+ assert_equal 6, len
2054
+ assert_equal 'Hello!', buf
2055
+ ensure
2056
+ if pid
2057
+ Process.kill('KILL', pid) rescue nil
2058
+ Process.wait(pid) rescue nil
2059
+ end
2060
+ machine.close(r_fd) rescue nil
2061
+ machine.close(w_fd) rescue nil
2062
+ end
2063
+
2064
+ def test_send_recv_fd_fork_mixed_msgs
2065
+ skip
2066
+
2067
+ pid = fork do
2068
+ m = UM.new
2069
+ buf = +''
2070
+ ret = m.recv(@s2_fd, buf, 128, 0)
2071
+ p forked_recv: buf, ret: ret
2072
+ ret = m.send(@s2_fd, buf, buf.bytesize, 0)
2073
+ p forked_send: buf, ret: ret
2074
+ fd = m.recv_fd(@s2_fd)
2075
+ p forked_recv_fd: fd
2076
+ ret = m.send(@s2_fd, buf, buf.bytesize, 0)
2077
+ p forked_send: buf, ret: ret
2078
+
2079
+ m.write(fd, 'foo')
2080
+ ensure
2081
+ m.close(fd) if fd rescue nil
2082
+ end
2083
+
2084
+ buf = +''
2085
+
2086
+ machine.send(@s1_fd, 'coocoo', 6, 0)
2087
+ machine.recv(@s1_fd, buf, 128, 0)
2088
+ assert_equal 'coocoo', buf
2089
+
2090
+ r_fd, w_fd = UM.pipe
2091
+ machine.send_fd(@s1_fd, w_fd)
2092
+ machine.recv(@s1_fd, buf, 128, 0)
2093
+ assert_equal 'coocoo', buf
2094
+
2095
+ machine.read(r_fd, buf, 128)
2096
+ assert_equal 'foo', buf
2097
+ ensure
2098
+ if pid
2099
+ Process.kill('KILL', pid) rescue nil
2100
+ Process.wait(pid) rescue nil
2101
+ end
2102
+ machine.close(r_fd) rescue nil
2103
+ machine.close(w_fd) rescue nil
2104
+ end
2105
+
2106
+ def test_send_recv_fd_fork_inverse
2107
+ pid = fork do
2108
+ m = UM.new
2109
+ r, w = UM.pipe
2110
+ m.send_fd(@s2_fd, r)
2111
+
2112
+ buf = +''
2113
+ m.read(r, buf, 128)
2114
+ m.send(@s2_fd, buf)
2115
+ end
2116
+
2117
+ assert_raises(Errno::EINVAL) { machine.recv_fd(@s1_fd) }
2118
+ ensure
2119
+ if pid
2120
+ Process.kill('KILL', pid) rescue nil
2121
+ Process.wait(pid) rescue nil
2122
+ end
2123
+ machine.close(w) rescue nil
2124
+ end
2125
+
2126
+ def test_send_fd_bad_sock_fd
2127
+ _r_fd, w_fd = UM.pipe
2128
+ assert_raises(Errno::ENOTSOCK) { machine.send_fd(0, w_fd) }
2129
+ end
2130
+
2131
+ def test_send_fd_bad_fd
2132
+ assert_raises(TypeError) { machine.send_fd(@s1_fd, nil) }
2133
+ assert_raises(Errno::EBADF) { machine.send_fd(@s1_fd, 1111) }
2134
+ end
2135
+
2136
+ def test_recv_fd_bad_msg
2137
+ buf = "\0" * 1000
2138
+ machine.write(@s1_fd, buf)
2139
+
2140
+ assert_raises(Errno::EINVAL) {
2141
+ res = machine.recv_fd(@s2_fd)
2142
+ p res: res
2143
+ }
2144
+ end
2145
+ end
2146
+
1884
2147
  class BindTest < UMBaseTest
1885
2148
  def setup
1886
2149
  super
@@ -2836,23 +3099,6 @@ class SendBundleTest < UMBaseTest
2836
3099
  end
2837
3100
  end
2838
3101
 
2839
- class NonBlockTest < UMBaseTest
2840
- def test_io_nonblock?
2841
- assert_equal false, UM.io_nonblock?(STDIN)
2842
- end
2843
-
2844
- def test_io_set_nonblock
2845
- r, _w = IO.pipe
2846
- assert_equal true, UM.io_nonblock?(r)
2847
-
2848
- UM.io_set_nonblock(r, false)
2849
- assert_equal false, UM.io_nonblock?(r)
2850
-
2851
- UM.io_set_nonblock(r, true)
2852
- assert_equal true, UM.io_nonblock?(r)
2853
- end
2854
- end
2855
-
2856
3102
  class MetricsTest < UMBaseTest
2857
3103
  def test_metrics_empty
2858
3104
  assert_equal({
@@ -3159,3 +3405,39 @@ class FileWatchTest < UMBaseTest
3159
3405
  machine.join(f)
3160
3406
  end
3161
3407
  end
3408
+
3409
+ class SetChildSubreaperTest < Minitest::Test
3410
+ def test_pr_set_child_subreaper
3411
+ r, w = IO.pipe
3412
+ UM.pr_set_child_subreaper(true)
3413
+
3414
+ child_pid = fork {
3415
+ r2, w2 = IO.pipe
3416
+ pid = fork {
3417
+ r.close
3418
+ w.close
3419
+ w2.close
3420
+ r2.read
3421
+ r2.close
3422
+ sleep(0.01)
3423
+ }
3424
+ w << pid
3425
+ w.close
3426
+ r2.close
3427
+ w2 << 'done'
3428
+ w2.close
3429
+ }
3430
+ Process.wait(child_pid)
3431
+
3432
+ w.close
3433
+ msg = r.read
3434
+ r.close
3435
+
3436
+ refute msg.empty?
3437
+ grand_child_pid = msg.to_i
3438
+ refute_equal 0, grand_child_pid
3439
+
3440
+ res = Process.wait(grand_child_pid)
3441
+ assert_equal grand_child_pid, res
3442
+ end
3443
+ 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.26.0
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner