polyphony 0.45.5 → 0.47.2

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/.gitmodules +0 -0
  4. data/CHANGELOG.md +23 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +3 -3
  7. data/Rakefile +1 -1
  8. data/TODO.md +21 -22
  9. data/bin/test +4 -0
  10. data/examples/core/enumerable.rb +64 -0
  11. data/examples/performance/fiber_resume.rb +43 -0
  12. data/examples/performance/fiber_transfer.rb +13 -4
  13. data/examples/performance/thread-vs-fiber/compare.rb +59 -0
  14. data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
  15. data/examples/performance/thread-vs-fiber/polyphony_server.rb +10 -21
  16. data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
  17. data/examples/performance/thread_switch.rb +44 -0
  18. data/ext/liburing/liburing.h +585 -0
  19. data/ext/liburing/liburing/README.md +4 -0
  20. data/ext/liburing/liburing/barrier.h +73 -0
  21. data/ext/liburing/liburing/compat.h +15 -0
  22. data/ext/liburing/liburing/io_uring.h +343 -0
  23. data/ext/liburing/queue.c +333 -0
  24. data/ext/liburing/register.c +187 -0
  25. data/ext/liburing/setup.c +210 -0
  26. data/ext/liburing/syscall.c +54 -0
  27. data/ext/liburing/syscall.h +18 -0
  28. data/ext/polyphony/backend.h +0 -14
  29. data/ext/polyphony/backend_common.h +129 -0
  30. data/ext/polyphony/backend_io_uring.c +995 -0
  31. data/ext/polyphony/backend_io_uring_context.c +74 -0
  32. data/ext/polyphony/backend_io_uring_context.h +53 -0
  33. data/ext/polyphony/{libev_backend.c → backend_libev.c} +304 -294
  34. data/ext/polyphony/event.c +1 -1
  35. data/ext/polyphony/extconf.rb +31 -13
  36. data/ext/polyphony/fiber.c +35 -24
  37. data/ext/polyphony/libev.c +4 -0
  38. data/ext/polyphony/libev.h +8 -2
  39. data/ext/polyphony/liburing.c +8 -0
  40. data/ext/polyphony/playground.c +51 -0
  41. data/ext/polyphony/polyphony.c +8 -5
  42. data/ext/polyphony/polyphony.h +23 -19
  43. data/ext/polyphony/polyphony_ext.c +10 -4
  44. data/ext/polyphony/queue.c +100 -35
  45. data/ext/polyphony/thread.c +10 -10
  46. data/lib/polyphony/adapters/trace.rb +2 -2
  47. data/lib/polyphony/core/exceptions.rb +0 -4
  48. data/lib/polyphony/core/global_api.rb +45 -21
  49. data/lib/polyphony/core/resource_pool.rb +12 -1
  50. data/lib/polyphony/extensions/core.rb +9 -15
  51. data/lib/polyphony/extensions/debug.rb +13 -0
  52. data/lib/polyphony/extensions/fiber.rb +8 -4
  53. data/lib/polyphony/extensions/openssl.rb +6 -0
  54. data/lib/polyphony/extensions/socket.rb +73 -10
  55. data/lib/polyphony/version.rb +1 -1
  56. data/test/helper.rb +36 -4
  57. data/test/io_uring_test.rb +55 -0
  58. data/test/stress.rb +4 -1
  59. data/test/test_backend.rb +63 -6
  60. data/test/test_ext.rb +1 -2
  61. data/test/test_fiber.rb +55 -20
  62. data/test/test_global_api.rb +107 -35
  63. data/test/test_queue.rb +117 -0
  64. data/test/test_resource_pool.rb +21 -0
  65. data/test/test_socket.rb +2 -2
  66. data/test/test_throttler.rb +3 -6
  67. data/test/test_trace.rb +7 -5
  68. metadata +28 -3
@@ -12,7 +12,7 @@ class ::Exception
12
12
  attr_accessor :__disable_sanitized_backtrace__
13
13
  end
14
14
 
15
- attr_accessor :source_fiber
15
+ attr_accessor :source_fiber, :__raising_fiber__
16
16
 
17
17
  alias_method :orig_initialize, :initialize
18
18
  def initialize(*args)
@@ -22,8 +22,8 @@ class ::Exception
22
22
 
23
23
  alias_method :orig_backtrace, :backtrace
24
24
  def backtrace
25
- unless @first_backtrace_call
26
- @first_backtrace_call = true
25
+ unless @backtrace_called
26
+ @backtrace_called = true
27
27
  return orig_backtrace
28
28
  end
29
29
 
@@ -31,12 +31,10 @@ class ::Exception
31
31
  end
32
32
 
33
33
  def sanitized_backtrace
34
- if @__raising_fiber__
35
- backtrace = orig_backtrace || []
36
- sanitize(backtrace + @__raising_fiber__.caller)
37
- else
38
- sanitize(orig_backtrace)
39
- end
34
+ return sanitize(orig_backtrace) unless @__raising_fiber__
35
+
36
+ backtrace = orig_backtrace || []
37
+ sanitize(backtrace + @__raising_fiber__.caller)
40
38
  end
41
39
 
42
40
  POLYPHONY_DIR = File.expand_path(File.join(__dir__, '..'))
@@ -164,11 +162,7 @@ end
164
162
 
165
163
  # Override Timeout to use cancel scope
166
164
  module ::Timeout
167
- def self.timeout(sec, klass = nil, message = nil, &block)
168
- cancel_after(sec, &block)
169
- rescue Polyphony::Cancel => e
170
- error = klass ? klass.new(message) : ::Timeout::Error.new
171
- error.set_backtrace(e.backtrace)
172
- raise error
165
+ def self.timeout(sec, klass = Timeout::Error, message = 'execution expired', &block)
166
+ cancel_after(sec, with_exception: [klass, message], &block)
173
167
  end
174
168
  end
@@ -0,0 +1,13 @@
1
+ module ::Kernel
2
+ def trace(*args)
3
+ STDOUT.orig_write(format_trace(args))
4
+ end
5
+
6
+ def format_trace(args)
7
+ if args.size > 1 && args.first.is_a?(String)
8
+ format("%s: %p\n", args.shift, args.size == 1 ? args.first : args)
9
+ else
10
+ format("%p\n", args.size == 1 ? args.first : args)
11
+ end
12
+ end
13
+ end
@@ -175,9 +175,9 @@ module Polyphony
175
175
  f = Fiber.new do
176
176
  block.call
177
177
  rescue Exception => e
178
- Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, e)
178
+ Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
179
179
  end
180
- Thread.current.break_out_of_ev_loop(f, nil)
180
+ Thread.current.schedule_and_wakeup(f, nil)
181
181
  end
182
182
  end
183
183
 
@@ -328,14 +328,18 @@ class ::Fiber
328
328
  extend Polyphony::FiberControlClassMethods
329
329
 
330
330
  attr_accessor :tag, :thread, :parent
331
- attr_reader :result, :mailbox
331
+ attr_reader :result
332
332
 
333
333
  def running?
334
334
  @running
335
335
  end
336
336
 
337
337
  def inspect
338
- "#<Fiber:#{object_id} #{location} (#{state})>"
338
+ if @tag
339
+ "#<Fiber #{tag}:#{object_id} #{location} (#{state})>"
340
+ else
341
+ "#<Fiber:#{object_id} #{location} (#{state})>"
342
+ end
339
343
  end
340
344
  alias_method :to_s, :inspect
341
345
 
@@ -36,6 +36,12 @@ class ::OpenSSL::SSL::SSLSocket
36
36
  end
37
37
  end
38
38
 
39
+ def accept_loop
40
+ loop do
41
+ yield accept
42
+ end
43
+ end
44
+
39
45
  alias_method :orig_sysread, :sysread
40
46
  def sysread(maxlen, buf = +'')
41
47
  loop do
@@ -19,18 +19,24 @@ class ::Socket
19
19
  end
20
20
 
21
21
  def recv(maxlen, flags = 0, outbuf = nil)
22
- outbuf ||= +''
23
- loop do
24
- result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
25
- case result
26
- when nil then raise IOError
27
- when :wait_readable then Thread.current.backend.wait_io(self, false)
28
- else
29
- return result
30
- end
31
- end
22
+ Thread.current.backend.recv(self, buf || +'', maxlen)
23
+ # outbuf ||= +''
24
+ # loop do
25
+ # result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
26
+ # case result
27
+ # when nil then raise IOError
28
+ # when :wait_readable then Thread.current.backend.wait_io(self, false)
29
+ # else
30
+ # return result
31
+ # end
32
+ # end
32
33
  end
33
34
 
35
+ def recv_loop(&block)
36
+ Thread.current.backend.recv_loop(self, &block)
37
+ end
38
+ alias_method :read_loop, :recv_loop
39
+
34
40
  def recvfrom(maxlen, flags = 0)
35
41
  @read_buffer ||= +''
36
42
  loop do
@@ -44,6 +50,23 @@ class ::Socket
44
50
  end
45
51
  end
46
52
 
53
+ def send(mesg, flags = 0)
54
+ Thread.current.backend.send(self, mesg)
55
+ end
56
+
57
+ def write(str, *args)
58
+ if args.empty?
59
+ Thread.current.backend.send(self, str)
60
+ else
61
+ Thread.current.backend.send(self, str + args.join)
62
+ end
63
+ end
64
+ alias_method :<<, :write
65
+
66
+ def readpartial(maxlen, str = +'')
67
+ Thread.current.backend.recv(self, str, maxlen)
68
+ end
69
+
47
70
  ZERO_LINGER = [0, 0].pack('ii').freeze
48
71
 
49
72
  def dont_linger
@@ -120,6 +143,42 @@ class ::TCPSocket
120
143
  setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
121
144
  end
122
145
 
146
+ def recv(maxlen, flags = 0, outbuf = nil)
147
+ Thread.current.backend.recv(self, buf || +'', maxlen)
148
+ end
149
+
150
+ def recv_loop(&block)
151
+ Thread.current.backend.recv_loop(self, &block)
152
+ end
153
+ alias_method :read_loop, :recv_loop
154
+
155
+ def send(mesg, flags = 0)
156
+ Thread.current.backend.send(self, mesg)
157
+ end
158
+
159
+ def write(str, *args)
160
+ if args.empty?
161
+ Thread.current.backend.send(self, str)
162
+ else
163
+ Thread.current.backend.send(self, str + args.join)
164
+ end
165
+ end
166
+ alias_method :<<, :write
167
+
168
+ def readpartial(maxlen, str = nil)
169
+ @read_buffer ||= +''
170
+ result = Thread.current.backend.recv(self, @read_buffer, maxlen)
171
+ raise EOFError unless result
172
+
173
+ if str
174
+ str << @read_buffer
175
+ else
176
+ str = @read_buffer
177
+ end
178
+ @read_buffer = +''
179
+ str
180
+ end
181
+
123
182
  def read_nonblock(len, str = nil, exception: true)
124
183
  @io.read_nonblock(len, str, exception: exception)
125
184
  end
@@ -142,6 +201,10 @@ class ::TCPServer
142
201
  @io.accept
143
202
  end
144
203
 
204
+ def accept_loop(&block)
205
+ Thread.current.backend.accept_loop(@io, &block)
206
+ end
207
+
145
208
  alias_method :orig_close, :close
146
209
  def close
147
210
  @io.close
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.45.5'
4
+ VERSION = '0.47.2'
5
5
  end
@@ -22,29 +22,52 @@ class ::Fiber
22
22
  attr_writer :auto_watcher
23
23
  end
24
24
 
25
+ module ::Kernel
26
+ def trace(*args)
27
+ STDOUT.orig_write(format_trace(args))
28
+ end
29
+
30
+ def format_trace(args)
31
+ if args.first.is_a?(String)
32
+ if args.size > 1
33
+ format("%s: %p\n", args.shift, args)
34
+ else
35
+ format("%s\n", args.first)
36
+ end
37
+ else
38
+ format("%p\n", args.size == 1 ? args.first : args)
39
+ end
40
+ end
41
+ end
42
+
25
43
  class MiniTest::Test
26
44
  def setup
27
- # puts "* setup #{self.name}"
45
+ # trace "* setup #{self.name}"
28
46
  if Fiber.current.children.size > 0
29
47
  puts "Children left: #{Fiber.current.children.inspect}"
30
48
  exit!
31
49
  end
32
50
  Fiber.current.setup_main_fiber
33
51
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
52
+ Thread.current.backend.finalize
34
53
  Thread.current.backend = Polyphony::Backend.new
35
- sleep 0 # apparently this helps with timer accuracy
54
+ sleep 0
36
55
  end
37
56
 
38
57
  def teardown
39
- # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
58
+ # trace "* teardown #{self.name}"
40
59
  Fiber.current.terminate_all_children
41
60
  Fiber.current.await_all_children
42
- Fiber.current.auto_watcher = nil
61
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
43
62
  rescue => e
44
63
  puts e
45
64
  puts e.backtrace.join("\n")
46
65
  exit!
47
66
  end
67
+
68
+ def fiber_tree(fiber)
69
+ { fiber: fiber, children: fiber.children.map { |f| fiber_tree(f) } }
70
+ end
48
71
  end
49
72
 
50
73
  module Kernel
@@ -54,3 +77,12 @@ module Kernel
54
77
  e
55
78
  end
56
79
  end
80
+
81
+ module Minitest::Assertions
82
+ def assert_in_range exp_range, act
83
+ msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
84
+ assert exp_range.include?(act), msg
85
+ end
86
+ end
87
+
88
+ puts "Polyphony backend: #{Thread.current.backend.kind}"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ ::Exception.__disable_sanitized_backtrace__ = true
6
+
7
+ module ::Kernel
8
+ def trace(*args)
9
+ STDOUT.orig_write(format_trace(args))
10
+ end
11
+
12
+ def format_trace(args)
13
+ if args.first.is_a?(String)
14
+ if args.size > 1
15
+ format("%s: %p\n", args.shift, args)
16
+ else
17
+ format("%s\n", args.first)
18
+ end
19
+ else
20
+ format("%p\n", args.size == 1 ? args.first : args)
21
+ end
22
+ end
23
+ end
24
+
25
+ i, o = IO.pipe
26
+
27
+ buf = []
28
+ f = spin do
29
+ buf << :ready
30
+ # loop do
31
+ # s = Thread.current.backend.read(i, +'', 6, false)
32
+ # trace read_result: s
33
+ # break if s.nil?
34
+ # buf << s
35
+ # rescue Exception => e
36
+ # trace exception: e
37
+ # raise e
38
+ # end
39
+ Thread.current.backend.read_loop(i) { |d| buf << d }
40
+ buf << :done
41
+ end
42
+
43
+ # writing always causes snoozing
44
+ o << 'foo'
45
+ o << 'bar'
46
+ trace '...closing'
47
+ o.close
48
+ trace '...closed'
49
+
50
+ f.await
51
+
52
+ raise "Bad result: #{buf.inspect}" unless buf == [:ready, 'foo', 'bar', :done]
53
+
54
+ puts '-' * 80
55
+ p buf
@@ -6,6 +6,7 @@ TEST_CMD = 'ruby test/run.rb'
6
6
 
7
7
  def run_test(count)
8
8
  puts "#{count}: running tests..."
9
+ # sleep 1
9
10
  system(TEST_CMD)
10
11
  return if $?.exitstatus == 0
11
12
 
@@ -15,7 +16,9 @@ end
15
16
 
16
17
  trap('INT') { exit! }
17
18
  t0 = Time.now
18
- count.times { |i| run_test(i + 1) }
19
+ count.times do |i|
20
+ run_test(i + 1)
21
+ end
19
22
  elapsed = Time.now - t0
20
23
  puts format(
21
24
  "Successfully ran %d tests in %f seconds (%f per test)",
@@ -85,7 +85,7 @@ class BackendTest < MiniTest::Test
85
85
  i, o = IO.pipe
86
86
 
87
87
  buf = []
88
- spin do
88
+ f = spin do
89
89
  buf << :ready
90
90
  @backend.read_loop(i) { |d| buf << d }
91
91
  buf << :done
@@ -96,9 +96,7 @@ class BackendTest < MiniTest::Test
96
96
  o << 'bar'
97
97
  o.close
98
98
 
99
- # read_loop will snooze after every read
100
- 6.times { snooze }
101
-
99
+ f.await
102
100
  assert_equal [:ready, 'foo', 'bar', :done], buf
103
101
  end
104
102
 
@@ -111,12 +109,12 @@ class BackendTest < MiniTest::Test
111
109
  end
112
110
 
113
111
  c1 = TCPSocket.new('127.0.0.1', 1234)
114
- 10.times { snooze }
112
+ sleep 0.01
115
113
 
116
114
  assert_equal 1, clients.size
117
115
 
118
116
  c2 = TCPSocket.new('127.0.0.1', 1234)
119
- 10.times { snooze }
117
+ sleep 0.01
120
118
 
121
119
  assert_equal 2, clients.size
122
120
 
@@ -127,4 +125,63 @@ class BackendTest < MiniTest::Test
127
125
  snooze
128
126
  server&.close
129
127
  end
128
+
129
+ def test_timer_loop
130
+ i = 0
131
+ f = spin do
132
+ @backend.timer_loop(0.01) { i += 1 }
133
+ end
134
+ @backend.sleep(0.05)
135
+ f.stop
136
+ f.await # TODO: check why this test sometimes segfaults if we don't a<wait fiber
137
+ assert_in_range 4..6, i
138
+ end
139
+
140
+ class MyTimeoutException < Exception
141
+ end
142
+
143
+ def test_timeout
144
+ buffer = []
145
+ assert_raises(Polyphony::TimeoutException) do
146
+ @backend.timeout(0.01, Polyphony::TimeoutException) do
147
+ buffer << 1
148
+ sleep 0.02
149
+ buffer << 2
150
+ end
151
+ end
152
+ assert_equal [1], buffer
153
+
154
+ buffer = []
155
+ assert_raises(MyTimeoutException) do
156
+ @backend.timeout(0.01, MyTimeoutException) do
157
+ buffer << 1
158
+ sleep 1
159
+ buffer << 2
160
+ end
161
+ end
162
+ assert_equal [1], buffer
163
+
164
+ buffer = []
165
+ result = @backend.timeout(0.01, nil, 42) do
166
+ buffer << 1
167
+ sleep 1
168
+ buffer << 2
169
+ end
170
+ assert_equal 42, result
171
+ assert_equal [1], buffer
172
+ end
173
+
174
+ def test_nested_timeout
175
+ buffer = []
176
+ assert_raises(MyTimeoutException) do
177
+ @backend.timeout(0.01, MyTimeoutException) do
178
+ @backend.timeout(0.02, nil) do
179
+ buffer << 1
180
+ sleep 1
181
+ buffer << 2
182
+ end
183
+ end
184
+ end
185
+ assert_equal [1], buffer
186
+ end
130
187
  end