polyphony 0.47.5.1 → 0.50.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE +1 -1
  5. data/TODO.md +37 -17
  6. data/examples/core/nested.rb +21 -0
  7. data/examples/core/suspend.rb +13 -0
  8. data/examples/core/terminate_main_fiber.rb +12 -0
  9. data/examples/io/tcp_proxy.rb +32 -0
  10. data/examples/performance/line_splitting.rb +34 -0
  11. data/examples/performance/loop.rb +32 -0
  12. data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -2
  13. data/ext/polyphony/backend_common.h +3 -3
  14. data/ext/polyphony/backend_io_uring.c +29 -68
  15. data/ext/polyphony/backend_libev.c +18 -59
  16. data/ext/polyphony/event.c +1 -1
  17. data/ext/polyphony/fiber.c +2 -1
  18. data/ext/polyphony/polyphony.c +0 -2
  19. data/ext/polyphony/polyphony.h +6 -4
  20. data/ext/polyphony/queue.c +2 -2
  21. data/ext/polyphony/runqueue.c +6 -0
  22. data/ext/polyphony/runqueue_ring_buffer.c +9 -0
  23. data/ext/polyphony/runqueue_ring_buffer.h +1 -0
  24. data/ext/polyphony/thread.c +23 -28
  25. data/lib/polyphony.rb +2 -1
  26. data/lib/polyphony/adapters/postgres.rb +3 -3
  27. data/lib/polyphony/adapters/process.rb +2 -0
  28. data/lib/polyphony/core/exceptions.rb +1 -0
  29. data/lib/polyphony/core/global_api.rb +15 -3
  30. data/lib/polyphony/core/thread_pool.rb +3 -1
  31. data/lib/polyphony/core/throttler.rb +1 -1
  32. data/lib/polyphony/core/timer.rb +115 -0
  33. data/lib/polyphony/extensions/core.rb +4 -4
  34. data/lib/polyphony/extensions/fiber.rb +30 -13
  35. data/lib/polyphony/extensions/io.rb +8 -14
  36. data/lib/polyphony/extensions/openssl.rb +4 -4
  37. data/lib/polyphony/extensions/socket.rb +1 -1
  38. data/lib/polyphony/extensions/thread.rb +1 -2
  39. data/lib/polyphony/net.rb +3 -6
  40. data/lib/polyphony/version.rb +1 -1
  41. data/polyphony.gemspec +1 -1
  42. data/test/helper.rb +2 -2
  43. data/test/test_backend.rb +26 -1
  44. data/test/test_fiber.rb +95 -1
  45. data/test/test_global_api.rb +30 -0
  46. data/test/test_io.rb +26 -0
  47. data/test/test_signal.rb +1 -2
  48. data/test/test_socket.rb +5 -5
  49. data/test/test_supervise.rb +1 -1
  50. data/test/test_timer.rb +157 -0
  51. metadata +11 -4
  52. data/ext/polyphony/backend.h +0 -26
@@ -45,7 +45,9 @@ module Polyphony
45
45
  end
46
46
 
47
47
  def thread_loop
48
- loop { run_queued_task }
48
+ while true
49
+ run_queued_task
50
+ end
49
51
  end
50
52
 
51
53
  def run_queued_task
@@ -15,7 +15,7 @@ module Polyphony
15
15
  Thread.current.backend.sleep(delta) if delta > 0
16
16
  yield self
17
17
 
18
- loop do
18
+ while true
19
19
  @next_time += @min_dt
20
20
  break if @next_time > now
21
21
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyphony
4
+ # Implements a common timer for running multiple timeouts
5
+ class Timer
6
+ def initialize(resolution:)
7
+ @fiber = spin_loop(interval: resolution) { update }
8
+ @timeouts = {}
9
+ end
10
+
11
+ def stop
12
+ @fiber.stop
13
+ end
14
+
15
+ def sleep(duration)
16
+ fiber = Fiber.current
17
+ @timeouts[fiber] = {
18
+ interval: duration,
19
+ target_stamp: now + duration
20
+ }
21
+ Thread.current.backend.wait_event(true)
22
+ ensure
23
+ @timeouts.delete(fiber)
24
+ end
25
+
26
+ def after(interval, &block)
27
+ spin do
28
+ self.sleep interval
29
+ block.()
30
+ end
31
+ end
32
+
33
+ def every(interval)
34
+ fiber = Fiber.current
35
+ @timeouts[fiber] = {
36
+ interval: interval,
37
+ target_stamp: now + interval,
38
+ recurring: true
39
+ }
40
+ while true
41
+ Thread.current.backend.wait_event(true)
42
+ yield
43
+ end
44
+ ensure
45
+ @timeouts.delete(fiber)
46
+ end
47
+
48
+ def cancel_after(interval, with_exception: Polyphony::Cancel)
49
+ fiber = Fiber.current
50
+ @timeouts[fiber] = {
51
+ interval: interval,
52
+ target_stamp: now + interval,
53
+ exception: with_exception
54
+ }
55
+ yield
56
+ ensure
57
+ @timeouts.delete(fiber)
58
+ end
59
+
60
+ def move_on_after(interval, with_value: nil)
61
+ fiber = Fiber.current
62
+ @timeouts[fiber] = {
63
+ interval: interval,
64
+ target_stamp: now + interval,
65
+ exception: [Polyphony::MoveOn, with_value]
66
+ }
67
+ yield
68
+ rescue Polyphony::MoveOn => e
69
+ e.value
70
+ ensure
71
+ @timeouts.delete(fiber)
72
+ end
73
+
74
+ def reset
75
+ record = @timeouts[Fiber.current]
76
+ return unless record
77
+
78
+ record[:target_stamp] = now + record[:interval]
79
+ end
80
+
81
+ private
82
+
83
+ def now
84
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
85
+ end
86
+
87
+ def timeout_exception(record)
88
+ case (exception = record[:exception])
89
+ when Array
90
+ exception[0].new(exception[1])
91
+ when Class
92
+ exception.new
93
+ else
94
+ RuntimeError.new(exception)
95
+ end
96
+ end
97
+
98
+ def update
99
+ return if @timeouts.empty?
100
+
101
+ @timeouts.each do |fiber, record|
102
+ next if record[:target_stamp] > now
103
+
104
+ value = record[:exception] ? timeout_exception(record) : record[:value]
105
+ fiber.schedule value
106
+
107
+ next unless record[:recurring]
108
+
109
+ while record[:target_stamp] <= now
110
+ record[:target_stamp] += record[:interval]
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -12,11 +12,11 @@ class ::Exception
12
12
  attr_accessor :__disable_sanitized_backtrace__
13
13
  end
14
14
 
15
- attr_accessor :source_fiber, :__raising_fiber__
15
+ attr_accessor :source_fiber, :raising_fiber
16
16
 
17
17
  alias_method :orig_initialize, :initialize
18
18
  def initialize(*args)
19
- @__raising_fiber__ = Fiber.current
19
+ @raising_fiber = Fiber.current
20
20
  orig_initialize(*args)
21
21
  end
22
22
 
@@ -31,10 +31,10 @@ class ::Exception
31
31
  end
32
32
 
33
33
  def sanitized_backtrace
34
- return sanitize(orig_backtrace) unless @__raising_fiber__
34
+ return sanitize(orig_backtrace) unless @raising_fiber
35
35
 
36
36
  backtrace = orig_backtrace || []
37
- sanitize(backtrace + @__raising_fiber__.caller)
37
+ sanitize(backtrace + @raising_fiber.caller)
38
38
  end
39
39
 
40
40
  POLYPHONY_DIR = File.expand_path(File.join(__dir__, '..'))
@@ -34,9 +34,18 @@ module Polyphony
34
34
  schedule Polyphony::Cancel.new
35
35
  end
36
36
 
37
- def terminate
37
+ def graceful_shutdown=(graceful)
38
+ @graceful_shutdown = graceful
39
+ end
40
+
41
+ def graceful_shutdown?
42
+ @graceful_shutdown
43
+ end
44
+
45
+ def terminate(graceful = false)
38
46
  return if @running == false
39
47
 
48
+ @graceful_shutdown = graceful
40
49
  schedule Polyphony::Terminate.new
41
50
  end
42
51
 
@@ -66,7 +75,9 @@ module Polyphony
66
75
  @on_child_done = proc do |fiber, result|
67
76
  self << fiber unless result.is_a?(Exception)
68
77
  end
69
- loop { supervise_perform(opts) }
78
+ while true
79
+ supervise_perform(opts)
80
+ end
70
81
  rescue Polyphony::MoveOn
71
82
  # generated in #supervise_perform to stop supervisor
72
83
  ensure
@@ -210,11 +221,14 @@ module Polyphony
210
221
  @on_child_done&.(child_fiber, result)
211
222
  end
212
223
 
213
- def terminate_all_children
224
+ def terminate_all_children(graceful = false)
214
225
  return unless @children
215
226
 
216
227
  e = Polyphony::Terminate.new
217
- @children.each_key { |c| c.raise e }
228
+ @children.each_key do |c|
229
+ c.graceful_shutdown = true if graceful
230
+ c.raise e
231
+ end
218
232
  end
219
233
 
220
234
  def await_all_children
@@ -230,9 +244,13 @@ module Polyphony
230
244
  results.values
231
245
  end
232
246
 
233
- def shutdown_all_children
234
- terminate_all_children
235
- await_all_children
247
+ def shutdown_all_children(graceful = false)
248
+ return unless @children
249
+
250
+ @children.keys.each do |c|
251
+ c.terminate(graceful)
252
+ c.await
253
+ end
236
254
  end
237
255
  end
238
256
 
@@ -298,6 +316,8 @@ module Polyphony
298
316
  @running = false
299
317
  inform_dependants(result, uncaught_exception)
300
318
  ensure
319
+ # Prevent fiber from being resumed after terminating
320
+ @thread.fiber_unschedule(self)
301
321
  Thread.current.switch_fiber
302
322
  end
303
323
 
@@ -305,13 +325,10 @@ module Polyphony
305
325
  # the children are shut down, it is returned along with the uncaught_exception
306
326
  # flag set. Otherwise, it returns the given arguments.
307
327
  def finalize_children(result, uncaught_exception)
308
- begin
309
- shutdown_all_children
310
- rescue Exception => e
311
- result = e
312
- uncaught_exception = true
313
- end
328
+ shutdown_all_children
314
329
  [result, uncaught_exception]
330
+ rescue Exception => e
331
+ [e, true]
315
332
  end
316
333
 
317
334
  def inform_dependants(result, uncaught_exception)
@@ -119,18 +119,11 @@ class ::IO
119
119
  end
120
120
 
121
121
  alias_method :orig_readpartial, :read
122
- def readpartial(len, str = nil)
123
- @read_buffer ||= +''
124
- result = Thread.current.backend.read(self, @read_buffer, len, false)
122
+ def readpartial(len, str = +'')
123
+ result = Thread.current.backend.read(self, str, len, false)
125
124
  raise EOFError unless result
126
125
 
127
- if str
128
- str << @read_buffer
129
- else
130
- str = @read_buffer
131
- end
132
- @read_buffer = +''
133
- str
126
+ result
134
127
  end
135
128
 
136
129
  alias_method :orig_write, :write
@@ -154,15 +147,16 @@ class ::IO
154
147
 
155
148
  @read_buffer ||= +''
156
149
 
157
- loop do
150
+ while true
158
151
  idx = @read_buffer.index(sep)
159
152
  return @read_buffer.slice!(0, idx + sep_size) if idx
160
153
 
161
- data = readpartial(8192)
154
+ data = readpartial(8192, +'')
155
+ return nil unless data
162
156
  @read_buffer << data
163
- rescue EOFError
164
- return nil
165
157
  end
158
+ rescue EOFError
159
+ return nil
166
160
  end
167
161
 
168
162
  # def print(*args)
@@ -25,7 +25,7 @@ class ::OpenSSL::SSL::SSLSocket
25
25
 
26
26
  alias_method :orig_accept, :accept
27
27
  def accept
28
- loop do
28
+ while true
29
29
  result = accept_nonblock(exception: false)
30
30
  case result
31
31
  when :wait_readable then Thread.current.backend.wait_io(io, false)
@@ -37,14 +37,14 @@ class ::OpenSSL::SSL::SSLSocket
37
37
  end
38
38
 
39
39
  def accept_loop
40
- loop do
40
+ while true
41
41
  yield accept
42
42
  end
43
43
  end
44
44
 
45
45
  alias_method :orig_sysread, :sysread
46
46
  def sysread(maxlen, buf = +'')
47
- loop do
47
+ while true
48
48
  case (result = read_nonblock(maxlen, buf, exception: false))
49
49
  when :wait_readable then Thread.current.backend.wait_io(io, false)
50
50
  when :wait_writable then Thread.current.backend.wait_io(io, true)
@@ -55,7 +55,7 @@ class ::OpenSSL::SSL::SSLSocket
55
55
 
56
56
  alias_method :orig_syswrite, :syswrite
57
57
  def syswrite(buf)
58
- loop do
58
+ while true
59
59
  case (result = write_nonblock(buf, exception: false))
60
60
  when :wait_readable then Thread.current.backend.wait_io(io, false)
61
61
  when :wait_writable then Thread.current.backend.wait_io(io, true)
@@ -33,7 +33,7 @@ class ::Socket
33
33
 
34
34
  def recvfrom(maxlen, flags = 0)
35
35
  @read_buffer ||= +''
36
- loop do
36
+ while true
37
37
  result = recvfrom_nonblock(maxlen, flags, @read_buffer, **NO_EXCEPTION)
38
38
  case result
39
39
  when nil then raise IOError
@@ -41,8 +41,7 @@ class ::Thread
41
41
 
42
42
  def finalize(result)
43
43
  unless Fiber.current.children.empty?
44
- Fiber.current.terminate_all_children
45
- Fiber.current.await_all_children
44
+ Fiber.current.shutdown_all_children
46
45
  end
47
46
  @finalization_mutex.synchronize do
48
47
  @terminated = true
@@ -8,10 +8,7 @@ module Polyphony
8
8
  module Net
9
9
  class << self
10
10
  def tcp_connect(host, port, opts = {})
11
- socket = ::Socket.new(:INET, :STREAM).tap do |s|
12
- addr = ::Socket.sockaddr_in(port, host)
13
- s.connect(addr)
14
- end
11
+ socket = TCPSocket.new(host, port)
15
12
  if opts[:secure_context] || opts[:secure]
16
13
  secure_socket(socket, opts[:secure_context], opts.merge(host: host))
17
14
  else
@@ -23,7 +20,7 @@ module Polyphony
23
20
  host ||= '0.0.0.0'
24
21
  raise 'Port number not specified' unless port
25
22
 
26
- socket = socket_from_options(host, port, opts)
23
+ socket = listening_socket_from_options(host, port, opts)
27
24
  if opts[:secure_context] || opts[:secure]
28
25
  secure_server(socket, opts[:secure_context], opts)
29
26
  else
@@ -31,7 +28,7 @@ module Polyphony
31
28
  end
32
29
  end
33
30
 
34
- def socket_from_options(host, port, opts)
31
+ def listening_socket_from_options(host, port, opts)
35
32
  ::Socket.new(:INET, :STREAM).tap do |s|
36
33
  s.reuse_addr if opts[:reuse_addr]
37
34
  s.dont_linger if opts[:dont_linger]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.47.5.1'
4
+ VERSION = '0.50.0'
5
5
  end
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.licenses = ['MIT']
7
7
  s.summary = 'Fine grained concurrency for Ruby'
8
8
  s.author = 'Sharon Rosner'
9
- s.email = 'ciconia@gmail.com'
9
+ s.email = 'sharon@noteflakes.com'
10
10
  s.files = `git ls-files`.split
11
11
  s.homepage = 'https://digital-fabric.github.io/polyphony'
12
12
  s.metadata = {
@@ -4,6 +4,7 @@ require 'bundler/setup'
4
4
 
5
5
  require_relative './coverage' if ENV['COVERAGE']
6
6
 
7
+ require 'httparty'
7
8
  require 'polyphony'
8
9
 
9
10
  require 'fileutils'
@@ -56,8 +57,7 @@ class MiniTest::Test
56
57
 
57
58
  def teardown
58
59
  # trace "* teardown #{self.name}"
59
- Fiber.current.terminate_all_children
60
- Fiber.current.await_all_children
60
+ Fiber.current.shutdown_all_children
61
61
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
62
62
  rescue => e
63
63
  puts e
@@ -100,12 +100,37 @@ class BackendTest < MiniTest::Test
100
100
  assert_equal [:ready, 'foo', 'bar', :done], buf
101
101
  end
102
102
 
103
+ def test_read_loop_terminate
104
+ i, o = IO.pipe
105
+
106
+ buf = []
107
+ parent = spin do
108
+ f = spin do
109
+ buf << :ready
110
+ @backend.read_loop(i) { |d| buf << d }
111
+ buf << :done
112
+ end
113
+ suspend
114
+ end
115
+
116
+ # writing always causes snoozing
117
+ o << 'foo'
118
+ sleep 0.01
119
+ o << 'bar'
120
+ sleep 0.01
121
+
122
+ parent.stop
123
+
124
+ parent.await
125
+ assert_equal [:ready, 'foo', 'bar'], buf
126
+ end
127
+
103
128
  def test_accept_loop
104
129
  server = TCPServer.new('127.0.0.1', 1234)
105
130
 
106
131
  clients = []
107
132
  server_fiber = spin do
108
- @backend.accept_loop(server) { |c| clients << c }
133
+ @backend.accept_loop(server, TCPSocket) { |c| clients << c }
109
134
  end
110
135
 
111
136
  c1 = TCPSocket.new('127.0.0.1', 1234)