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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/Gemfile.lock +1 -1
- data/LICENSE +1 -1
- data/TODO.md +37 -17
- data/examples/core/nested.rb +21 -0
- data/examples/core/suspend.rb +13 -0
- data/examples/core/terminate_main_fiber.rb +12 -0
- data/examples/io/tcp_proxy.rb +32 -0
- data/examples/performance/line_splitting.rb +34 -0
- data/examples/performance/loop.rb +32 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +6 -2
- data/ext/polyphony/backend_common.h +3 -3
- data/ext/polyphony/backend_io_uring.c +29 -68
- data/ext/polyphony/backend_libev.c +18 -59
- data/ext/polyphony/event.c +1 -1
- data/ext/polyphony/fiber.c +2 -1
- data/ext/polyphony/polyphony.c +0 -2
- data/ext/polyphony/polyphony.h +6 -4
- data/ext/polyphony/queue.c +2 -2
- data/ext/polyphony/runqueue.c +6 -0
- data/ext/polyphony/runqueue_ring_buffer.c +9 -0
- data/ext/polyphony/runqueue_ring_buffer.h +1 -0
- data/ext/polyphony/thread.c +23 -28
- data/lib/polyphony.rb +2 -1
- data/lib/polyphony/adapters/postgres.rb +3 -3
- data/lib/polyphony/adapters/process.rb +2 -0
- data/lib/polyphony/core/exceptions.rb +1 -0
- data/lib/polyphony/core/global_api.rb +15 -3
- data/lib/polyphony/core/thread_pool.rb +3 -1
- data/lib/polyphony/core/throttler.rb +1 -1
- data/lib/polyphony/core/timer.rb +115 -0
- data/lib/polyphony/extensions/core.rb +4 -4
- data/lib/polyphony/extensions/fiber.rb +30 -13
- data/lib/polyphony/extensions/io.rb +8 -14
- data/lib/polyphony/extensions/openssl.rb +4 -4
- data/lib/polyphony/extensions/socket.rb +1 -1
- data/lib/polyphony/extensions/thread.rb +1 -2
- data/lib/polyphony/net.rb +3 -6
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +1 -1
- data/test/helper.rb +2 -2
- data/test/test_backend.rb +26 -1
- data/test/test_fiber.rb +95 -1
- data/test/test_global_api.rb +30 -0
- data/test/test_io.rb +26 -0
- data/test/test_signal.rb +1 -2
- data/test/test_socket.rb +5 -5
- data/test/test_supervise.rb +1 -1
- data/test/test_timer.rb +157 -0
- metadata +11 -4
- data/ext/polyphony/backend.h +0 -26
@@ -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, :
|
15
|
+
attr_accessor :source_fiber, :raising_fiber
|
16
16
|
|
17
17
|
alias_method :orig_initialize, :initialize
|
18
18
|
def initialize(*args)
|
19
|
-
@
|
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 @
|
34
|
+
return sanitize(orig_backtrace) unless @raising_fiber
|
35
35
|
|
36
36
|
backtrace = orig_backtrace || []
|
37
|
-
sanitize(backtrace + @
|
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
|
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
|
-
|
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
|
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
|
-
|
235
|
-
|
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
|
-
|
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 =
|
123
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
@@ -41,8 +41,7 @@ class ::Thread
|
|
41
41
|
|
42
42
|
def finalize(result)
|
43
43
|
unless Fiber.current.children.empty?
|
44
|
-
Fiber.current.
|
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
|
data/lib/polyphony/net.rb
CHANGED
@@ -8,10 +8,7 @@ module Polyphony
|
|
8
8
|
module Net
|
9
9
|
class << self
|
10
10
|
def tcp_connect(host, port, opts = {})
|
11
|
-
socket =
|
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 =
|
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
|
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]
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -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 = '
|
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 = {
|
data/test/helper.rb
CHANGED
@@ -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.
|
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
|
data/test/test_backend.rb
CHANGED
@@ -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)
|