polyphony 0.46.0 → 0.47.3

Sign up to get free protection for your applications and to get access to all the features.
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