polyphony 0.46.0 → 0.47.3

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/Gemfile.lock +1 -1
  4. data/TODO.md +54 -23
  5. data/bin/test +4 -0
  6. data/examples/core/enumerable.rb +64 -0
  7. data/examples/performance/fiber_resume.rb +43 -0
  8. data/examples/performance/fiber_transfer.rb +13 -4
  9. data/examples/performance/thread-vs-fiber/compare.rb +59 -0
  10. data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
  11. data/examples/performance/thread-vs-fiber/polyphony_server.rb +9 -19
  12. data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
  13. data/examples/performance/thread_switch.rb +44 -0
  14. data/ext/polyphony/backend_common.h +20 -0
  15. data/ext/polyphony/backend_io_uring.c +127 -16
  16. data/ext/polyphony/backend_io_uring_context.c +1 -0
  17. data/ext/polyphony/backend_io_uring_context.h +1 -0
  18. data/ext/polyphony/backend_libev.c +102 -0
  19. data/ext/polyphony/fiber.c +11 -7
  20. data/ext/polyphony/polyphony.c +3 -0
  21. data/ext/polyphony/polyphony.h +7 -7
  22. data/ext/polyphony/queue.c +99 -34
  23. data/ext/polyphony/thread.c +1 -3
  24. data/lib/polyphony/core/exceptions.rb +0 -4
  25. data/lib/polyphony/core/global_api.rb +49 -31
  26. data/lib/polyphony/extensions/core.rb +9 -15
  27. data/lib/polyphony/extensions/fiber.rb +8 -2
  28. data/lib/polyphony/extensions/openssl.rb +6 -0
  29. data/lib/polyphony/extensions/socket.rb +18 -4
  30. data/lib/polyphony/version.rb +1 -1
  31. data/test/helper.rb +1 -1
  32. data/test/stress.rb +1 -1
  33. data/test/test_backend.rb +59 -0
  34. data/test/test_fiber.rb +33 -4
  35. data/test/test_global_api.rb +85 -1
  36. data/test/test_queue.rb +117 -0
  37. data/test/test_signal.rb +18 -0
  38. data/test/test_socket.rb +2 -2
  39. metadata +8 -2
@@ -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
@@ -173,6 +173,7 @@ module Polyphony
173
173
  # signals (see also the patched `Kernel#trap`)
174
174
  def schedule_priority_oob_fiber(&block)
175
175
  f = Fiber.new do
176
+ Fiber.current.setup_raw
176
177
  block.call
177
178
  rescue Exception => e
178
179
  Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
@@ -262,6 +263,7 @@ module Polyphony
262
263
  # allows the fiber to be scheduled and to receive messages.
263
264
  def setup_raw
264
265
  @thread = Thread.current
266
+ @running = true
265
267
  end
266
268
 
267
269
  def setup_main_fiber
@@ -328,14 +330,18 @@ class ::Fiber
328
330
  extend Polyphony::FiberControlClassMethods
329
331
 
330
332
  attr_accessor :tag, :thread, :parent
331
- attr_reader :result, :mailbox
333
+ attr_reader :result
332
334
 
333
335
  def running?
334
336
  @running
335
337
  end
336
338
 
337
339
  def inspect
338
- "#<Fiber:#{object_id} #{location} (#{state})>"
340
+ if @tag
341
+ "#<Fiber #{tag}:#{object_id} #{location} (#{state})>"
342
+ else
343
+ "#<Fiber:#{object_id} #{location} (#{state})>"
344
+ end
339
345
  end
340
346
  alias_method :to_s, :inspect
341
347
 
@@ -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
@@ -35,6 +35,7 @@ class ::Socket
35
35
  def recv_loop(&block)
36
36
  Thread.current.backend.recv_loop(self, &block)
37
37
  end
38
+ alias_method :read_loop, :recv_loop
38
39
 
39
40
  def recvfrom(maxlen, flags = 0)
40
41
  @read_buffer ||= +''
@@ -53,8 +54,12 @@ class ::Socket
53
54
  Thread.current.backend.send(self, mesg)
54
55
  end
55
56
 
56
- def write(str)
57
- Thread.current.backend.send(self, str)
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
58
63
  end
59
64
  alias_method :<<, :write
60
65
 
@@ -145,13 +150,18 @@ class ::TCPSocket
145
150
  def recv_loop(&block)
146
151
  Thread.current.backend.recv_loop(self, &block)
147
152
  end
153
+ alias_method :read_loop, :recv_loop
148
154
 
149
155
  def send(mesg, flags = 0)
150
156
  Thread.current.backend.send(self, mesg)
151
157
  end
152
158
 
153
- def write(str)
154
- Thread.current.backend.send(self, str)
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
155
165
  end
156
166
  alias_method :<<, :write
157
167
 
@@ -191,6 +201,10 @@ class ::TCPServer
191
201
  @io.accept
192
202
  end
193
203
 
204
+ def accept_loop(&block)
205
+ Thread.current.backend.accept_loop(@io, &block)
206
+ end
207
+
194
208
  alias_method :orig_close, :close
195
209
  def close
196
210
  @io.close
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.46.0'
4
+ VERSION = '0.47.3'
5
5
  end
@@ -85,4 +85,4 @@ module Minitest::Assertions
85
85
  end
86
86
  end
87
87
 
88
- puts "Polyphony backend: #{Thread.current.backend.kind}"
88
+ puts "Polyphony backend: #{Thread.current.backend.kind}"
@@ -2,7 +2,7 @@
2
2
 
3
3
  count = ARGV[0] ? ARGV[0].to_i : 100
4
4
 
5
- TEST_CMD = 'ruby test/test_backend.rb' #'ruby test/run.rb'
5
+ TEST_CMD = 'ruby test/run.rb'
6
6
 
7
7
  def run_test(count)
8
8
  puts "#{count}: running tests..."
@@ -125,4 +125,63 @@ class BackendTest < MiniTest::Test
125
125
  snooze
126
126
  server&.close
127
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
128
187
  end
@@ -565,14 +565,14 @@ class FiberTest < MiniTest::Test
565
565
  end
566
566
 
567
567
  def test_inspect
568
- expected = format('#<Fiber:%s (root) (running)>', Fiber.current.object_id)
568
+ expected = format('#<Fiber main:%s (root) (running)>', Fiber.current.object_id)
569
569
  assert_equal expected, Fiber.current.inspect
570
570
 
571
571
  spin_line_no = __LINE__ + 1
572
- f = spin { :foo }
572
+ f = spin(:baz) { :foo }
573
573
 
574
574
  expected = format(
575
- '#<Fiber:%s %s:%d:in `test_inspect\' (runnable)>',
575
+ '#<Fiber baz:%s %s:%d:in `test_inspect\' (runnable)>',
576
576
  f.object_id,
577
577
  __FILE__,
578
578
  spin_line_no
@@ -581,7 +581,7 @@ class FiberTest < MiniTest::Test
581
581
 
582
582
  f.await
583
583
  expected = format(
584
- '#<Fiber:%s %s:%d:in `test_inspect\' (dead)>',
584
+ '#<Fiber baz:%s %s:%d:in `test_inspect\' (dead)>',
585
585
  f.object_id,
586
586
  __FILE__,
587
587
  spin_line_no
@@ -738,6 +738,35 @@ class MailboxTest < MiniTest::Test
738
738
  f&.stop
739
739
  end
740
740
 
741
+ def test_capped_fiber_mailbox
742
+ buffer = []
743
+ a = spin_loop do
744
+ 3.times { snooze }
745
+ buffer << [:receive, receive]
746
+ end
747
+ a.mailbox.cap(1)
748
+
749
+ b = spin do
750
+ (1..3).each do |i|
751
+ a << i
752
+ buffer << [:send, i]
753
+ end
754
+ end
755
+
756
+ (1..10).each do |i|
757
+ snooze
758
+ buffer << [:snooze, i]
759
+ end
760
+
761
+ b.join
762
+
763
+ assert_equal [
764
+ [:snooze, 1], [:send, 1], [:snooze, 2], [:snooze, 3], [:snooze, 4],
765
+ [:receive, 1], [:snooze, 5], [:send, 2], [:snooze, 6], [:snooze, 7],
766
+ [:snooze, 8], [:receive, 2], [:snooze, 9], [:send, 3], [:snooze, 10]
767
+ ], buffer
768
+ end
769
+
741
770
  def test_that_multiple_messages_sent_at_once_arrive_in_order
742
771
  msgs = []
743
772
  f = spin { loop { msgs << receive } }
@@ -150,6 +150,28 @@ class MoveOnAfterTest < MiniTest::Test
150
150
  assert t1 - t0 < 0.1
151
151
  assert_equal 'foo', v
152
152
  end
153
+
154
+ def test_nested_move_on_after
155
+ t0 = Time.now
156
+ o = move_on_after(0.01, with_value: 1) do
157
+ move_on_after(0.02, with_value: 2) do
158
+ sleep 1
159
+ end
160
+ end
161
+ t1 = Time.now
162
+ assert_equal 1, o
163
+ assert_in_range 0.008..0.013, t1 - t0
164
+
165
+ t0 = Time.now
166
+ o = move_on_after(0.02, with_value: 1) do
167
+ move_on_after(0.01, with_value: 2) do
168
+ sleep 1
169
+ end
170
+ end
171
+ t1 = Time.now
172
+ assert_equal 2, o
173
+ assert_in_range 0.008..0.013, t1 - t0
174
+ end
153
175
  end
154
176
 
155
177
  class CancelAfterTest < MiniTest::Test
@@ -188,7 +210,7 @@ class CancelAfterTest < MiniTest::Test
188
210
  sleep 0.007
189
211
  end
190
212
  t1 = Time.now
191
- assert_in_range 0.014..0.02, t1 - t0
213
+ assert_in_range 0.014..0.024, t1 - t0
192
214
  end
193
215
 
194
216
  class CustomException < Exception
@@ -202,6 +224,19 @@ class CancelAfterTest < MiniTest::Test
202
224
  end
203
225
  end
204
226
 
227
+ begin
228
+ err = nil
229
+ cancel_after(0.01, with_exception: [CustomException, 'custom message']) do
230
+ sleep 1
231
+ :foo
232
+ end
233
+ rescue Exception => err
234
+ ensure
235
+ assert_kind_of CustomException, err
236
+ assert_equal 'custom message', err.message
237
+ end
238
+
239
+
205
240
  begin
206
241
  e = nil
207
242
  cancel_after(0.01, with_exception: 'foo') do
@@ -262,6 +297,55 @@ class SpinLoopTest < MiniTest::Test
262
297
  f.stop
263
298
  assert_in_range 1..3, counter
264
299
  end
300
+
301
+ def test_spin_loop_with_interval
302
+ buffer = []
303
+ counter = 0
304
+ t0 = Time.now
305
+ f = spin_loop(interval: 0.01) { buffer << (counter += 1) }
306
+ sleep 0.02
307
+ f.stop
308
+ assert_in_range 1..3, counter
309
+ end
310
+ end
311
+
312
+ class SpinScopeTest < MiniTest::Test
313
+ def test_spin_scope
314
+ queue = Queue.new
315
+ buffer = {}
316
+ spin do
317
+ queue << 1
318
+ snooze
319
+ queue << 2
320
+ end
321
+ f = nil
322
+ result = spin_scope do
323
+ f = Fiber.current
324
+ spin { buffer[:a] = queue.shift }
325
+ spin { buffer[:b] = queue.shift }
326
+ :foo
327
+ end
328
+ assert_equal :foo, result
329
+ assert_kind_of Fiber, f
330
+ assert_equal :dead, f.state
331
+ assert_equal ({a: 1, b: 2}), buffer
332
+ end
333
+
334
+ def test_spin_scope_with_exception
335
+ queue = Queue.new
336
+ buffer = []
337
+ spin do
338
+ spin_scope do
339
+ spin { buffer << queue.shift }
340
+ spin { raise 'foobar' }
341
+ end
342
+ rescue => e
343
+ buffer << e.message
344
+ end
345
+ 6.times { snooze }
346
+ assert_equal 0, Fiber.current.children.size
347
+ assert_equal ['foobar'], buffer
348
+ end
265
349
  end
266
350
 
267
351
  class ThrottledLoopTest < MiniTest::Test
@@ -129,4 +129,121 @@ class QueueTest < MiniTest::Test
129
129
 
130
130
  assert_equal 0, @queue.size
131
131
  end
132
+ end
133
+
134
+ class CappedQueueTest < MiniTest::Test
135
+ def setup
136
+ super
137
+ @queue = Polyphony::Queue.new
138
+ @queue.cap(3)
139
+ end
140
+
141
+ def test_capped?
142
+ q = Polyphony::Queue.new
143
+ assert_nil q.capped?
144
+
145
+ q.cap(3)
146
+ assert_equal 3, q.capped?
147
+ end
148
+
149
+ def test_initalize_with_cap
150
+ q = Polyphony::Queue.new(42)
151
+ assert_equal 42, q.capped?
152
+ end
153
+
154
+ def test_capped_push
155
+ buffer = []
156
+ a = spin do
157
+ (1..5).each do |i|
158
+ @queue.push(i)
159
+ buffer << :"p#{i}"
160
+ end
161
+ @queue.push :stop
162
+ end
163
+
164
+ snooze
165
+
166
+ b = spin_loop do
167
+ i = @queue.shift
168
+ raise Polyphony::Terminate if i == :stop
169
+ buffer << :"s#{i}"
170
+ end
171
+
172
+ Fiber.join(a, b)
173
+ assert_equal [:p1, :p2, :s1, :p3, :s2, :p4, :s3, :p5, :s4, :s5], buffer
174
+ end
175
+
176
+ def test_capped_multi_push
177
+ buffer = []
178
+ a = spin(:a) do
179
+ (1..3).each do |i|
180
+ @queue.push(i)
181
+ buffer << :"p#{i}"
182
+ end
183
+ end
184
+
185
+ buffer = []
186
+ b = spin(:b) do
187
+ (4..6).each do |i|
188
+ @queue.push(i)
189
+ buffer << :"p#{i}"
190
+ end
191
+ @queue.push :stop
192
+ end
193
+
194
+ c = spin_loop do
195
+ i = @queue.shift
196
+ raise Polyphony::Terminate if i == :stop
197
+ buffer << :"s#{i}"
198
+ snooze
199
+ end
200
+
201
+ Fiber.join(a, b, c)
202
+ assert_equal [:p1, :p4, :s1, :p5, :p2, :s4, :p3, :s5, :p6, :s2, :s3, :s6], buffer
203
+ end
204
+
205
+ def test_capped_clear
206
+ buffer = []
207
+ a = spin(:a) do
208
+ (1..5).each do |i|
209
+ @queue.push(i)
210
+ buffer << i
211
+ end
212
+ end
213
+
214
+ snooze while buffer.size < 3
215
+ @queue.clear
216
+ buffer << :clear
217
+
218
+ a.join
219
+ assert_equal [1, 2, 3, :clear, 4, 5], buffer
220
+ end
221
+
222
+ def test_capped_delete
223
+ buffer = []
224
+ a = spin(:a) do
225
+ (1..5).each do |i|
226
+ @queue.push(i)
227
+ buffer << i
228
+ end
229
+ end
230
+
231
+ i = 0
232
+ spin_loop do
233
+ i += 1
234
+ snooze
235
+ end
236
+
237
+ 5.times { snooze }
238
+ assert_equal 5, i
239
+ @queue.delete 1
240
+ buffer << :"d#{i}"
241
+ 3.times { snooze }
242
+ assert_equal 8, i
243
+ @queue.delete 2
244
+ buffer << :"d#{i}"
245
+
246
+ a.join
247
+ assert_equal [1, 2, 3, :d5, 4, :d8, 5], buffer
248
+ end
132
249
  end