polyphony 0.43.2 → 0.43.8
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/.github/workflows/test.yml +2 -2
- data/CHANGELOG.md +43 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -0
- data/TODO.md +2 -3
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/title.html +1 -0
- data/docs/_user-guide/web-server.md +11 -11
- data/docs/getting-started/overview.md +4 -4
- data/docs/index.md +4 -3
- data/docs/main-concepts/design-principles.md +23 -34
- data/docs/main-concepts/fiber-scheduling.md +1 -1
- data/docs/polyphony-logo.png +0 -0
- data/examples/adapters/concurrent-ruby.rb +9 -0
- data/examples/core/xx-daemon.rb +14 -0
- data/examples/io/xx-happy-eyeballs.rb +21 -22
- data/examples/io/xx-zip.rb +19 -0
- data/examples/performance/fiber_transfer.rb +47 -0
- data/examples/performance/mem-usage.rb +34 -28
- data/examples/performance/messaging.rb +29 -0
- data/examples/performance/multi_snooze.rb +11 -9
- data/examples/xx-spin.rb +32 -0
- data/ext/polyphony/libev_agent.c +181 -24
- data/ext/polyphony/polyphony.c +0 -2
- data/ext/polyphony/polyphony.h +14 -7
- data/ext/polyphony/polyphony_ext.c +2 -2
- data/ext/polyphony/queue.c +168 -0
- data/ext/polyphony/ring_buffer.c +96 -0
- data/ext/polyphony/ring_buffer.h +28 -0
- data/ext/polyphony/thread.c +16 -8
- data/lib/polyphony.rb +28 -12
- data/lib/polyphony/core/global_api.rb +5 -3
- data/lib/polyphony/core/resource_pool.rb +19 -9
- data/lib/polyphony/core/thread_pool.rb +1 -1
- data/lib/polyphony/event.rb +5 -15
- data/lib/polyphony/extensions/core.rb +40 -0
- data/lib/polyphony/extensions/fiber.rb +9 -14
- data/lib/polyphony/extensions/io.rb +17 -16
- data/lib/polyphony/extensions/openssl.rb +8 -0
- data/lib/polyphony/extensions/socket.rb +12 -0
- data/lib/polyphony/version.rb +1 -1
- data/test/helper.rb +1 -1
- data/test/q.rb +24 -0
- data/test/test_agent.rb +3 -3
- data/test/test_event.rb +11 -0
- data/test/test_fiber.rb +3 -3
- data/test/test_global_api.rb +48 -15
- data/test/test_io.rb +24 -2
- data/test/test_queue.rb +39 -1
- data/test/test_resource_pool.rb +12 -0
- data/test/test_throttler.rb +6 -5
- data/test/test_trace.rb +18 -17
- metadata +15 -4
- data/ext/polyphony/libev_queue.c +0 -217
data/lib/polyphony.rb
CHANGED
@@ -4,8 +4,11 @@ require 'fiber'
|
|
4
4
|
require_relative './polyphony_ext'
|
5
5
|
|
6
6
|
module Polyphony
|
7
|
-
#
|
8
|
-
|
7
|
+
# replace core Queue class with our own
|
8
|
+
verbose = $VERBOSE
|
9
|
+
$VERBOSE = nil
|
10
|
+
Object.const_set(:Queue, Polyphony::Queue)
|
11
|
+
$VERBOSE = verbose
|
9
12
|
end
|
10
13
|
|
11
14
|
require_relative './polyphony/extensions/core'
|
@@ -94,17 +97,12 @@ module Polyphony
|
|
94
97
|
Polyphony::Process.watch(cmd, &block)
|
95
98
|
end
|
96
99
|
|
97
|
-
def emit_signal_exception(exception, fiber = Thread.main.main_fiber)
|
98
|
-
Thread.current.break_out_of_ev_loop(fiber, exception)
|
99
|
-
end
|
100
|
-
|
101
|
-
def install_terminating_signal_handler(signal, exception_class)
|
102
|
-
trap(signal) { emit_signal_exception(exception_class.new) }
|
103
|
-
end
|
104
|
-
|
105
100
|
def install_terminating_signal_handlers
|
106
|
-
|
107
|
-
|
101
|
+
trap('SIGTERM', SystemExit)
|
102
|
+
orig_trap('SIGINT') do
|
103
|
+
orig_trap('SIGINT') { exit! }
|
104
|
+
Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, Interrupt.new)
|
105
|
+
end
|
108
106
|
end
|
109
107
|
|
110
108
|
def terminate_threads
|
@@ -114,7 +112,25 @@ module Polyphony
|
|
114
112
|
threads.each(&:kill)
|
115
113
|
threads.each(&:join)
|
116
114
|
end
|
115
|
+
|
116
|
+
attr_accessor :original_pid
|
117
|
+
|
118
|
+
def install_at_exit_handler
|
119
|
+
@original_pid = ::Process.pid
|
120
|
+
|
121
|
+
# This at_exit handler is needed only when the original process exits. Due to
|
122
|
+
# the behaviour of fibers on fork (and especially on exit from forked
|
123
|
+
# processes,) we use a separate mechanism to terminate fibers in forked
|
124
|
+
# processes (see Polyphony.fork).
|
125
|
+
at_exit do
|
126
|
+
next unless @original_pid == ::Process.pid
|
127
|
+
|
128
|
+
Polyphony.terminate_threads
|
129
|
+
Fiber.current.shutdown_all_children
|
130
|
+
end
|
131
|
+
end
|
117
132
|
end
|
118
133
|
end
|
119
134
|
|
120
135
|
Polyphony.install_terminating_signal_handlers
|
136
|
+
Polyphony.install_at_exit_handler
|
@@ -15,11 +15,13 @@ module Polyphony
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
def cancel_after(interval, &block)
|
18
|
+
def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
|
19
19
|
fiber = ::Fiber.current
|
20
20
|
canceller = spin do
|
21
21
|
sleep interval
|
22
|
-
|
22
|
+
exception = with_exception.is_a?(Class) ?
|
23
|
+
with_exception.new : RuntimeError.new(with_exception)
|
24
|
+
fiber.schedule exception
|
23
25
|
end
|
24
26
|
block ? cancel_after_wrap_block(canceller, &block) : canceller
|
25
27
|
end
|
@@ -101,7 +103,7 @@ module Polyphony
|
|
101
103
|
|
102
104
|
def sleep_forever
|
103
105
|
Thread.current.agent.ref
|
104
|
-
|
106
|
+
loop { sleep 60 }
|
105
107
|
ensure
|
106
108
|
Thread.current.agent.unref
|
107
109
|
end
|
@@ -13,6 +13,7 @@ module Polyphony
|
|
13
13
|
|
14
14
|
@stock = []
|
15
15
|
@queue = []
|
16
|
+
@acquired_resources = {}
|
16
17
|
|
17
18
|
@limit = opts[:limit] || 4
|
18
19
|
@size = 0
|
@@ -23,16 +24,25 @@ module Polyphony
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def acquire
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
fiber = Fiber.current
|
28
|
+
if @acquired_resources[fiber]
|
29
|
+
yield @acquired_resources[fiber]
|
30
|
+
else
|
31
|
+
begin
|
32
|
+
Thread.current.agent.ref
|
33
|
+
resource = wait_for_resource
|
34
|
+
return unless resource
|
35
|
+
|
36
|
+
@acquired_resources[fiber] = resource
|
37
|
+
yield resource
|
38
|
+
ensure
|
39
|
+
@acquired_resources.delete fiber
|
40
|
+
Thread.current.agent.unref
|
41
|
+
release(resource) if resource
|
42
|
+
end
|
43
|
+
end
|
34
44
|
end
|
35
|
-
|
45
|
+
|
36
46
|
def wait_for_resource
|
37
47
|
fiber = Fiber.current
|
38
48
|
@queue << fiber
|
data/lib/polyphony/event.rb
CHANGED
@@ -3,25 +3,15 @@
|
|
3
3
|
module Polyphony
|
4
4
|
# Event watcher for thread-safe synchronisation
|
5
5
|
class Event
|
6
|
-
def initialize
|
7
|
-
@i, @o = IO.pipe
|
8
|
-
end
|
9
|
-
|
10
6
|
def await
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@value
|
15
|
-
end
|
16
|
-
|
17
|
-
def await_no_raise
|
18
|
-
Thread.current.agent.read(@i, +'', 8192, false)
|
19
|
-
@value
|
7
|
+
@fiber = Fiber.current
|
8
|
+
Thread.current.agent.wait_event(true)
|
20
9
|
end
|
21
10
|
|
22
11
|
def signal(value = nil)
|
23
|
-
@value
|
24
|
-
|
12
|
+
@fiber&.schedule(value)
|
13
|
+
ensure
|
14
|
+
@fiber = nil
|
25
15
|
end
|
26
16
|
end
|
27
17
|
end
|
@@ -57,6 +57,12 @@ module ::Process
|
|
57
57
|
fiber.define_singleton_method(:pid) { pid }
|
58
58
|
fiber
|
59
59
|
end
|
60
|
+
|
61
|
+
alias_method :orig_daemon, :daemon
|
62
|
+
def daemon(*args)
|
63
|
+
orig_daemon(*args)
|
64
|
+
Polyphony.original_pid = Process.pid
|
65
|
+
end
|
60
66
|
end
|
61
67
|
end
|
62
68
|
|
@@ -101,6 +107,15 @@ module ::Kernel
|
|
101
107
|
$stdin.gets
|
102
108
|
end
|
103
109
|
|
110
|
+
alias_method :orig_p, :p
|
111
|
+
def p(*args)
|
112
|
+
strs = args.inject([]) do |m, a|
|
113
|
+
m << a.inspect << "\n"
|
114
|
+
end
|
115
|
+
STDOUT.write *strs
|
116
|
+
args.size == 1 ? args.first : args
|
117
|
+
end
|
118
|
+
|
104
119
|
alias_method :orig_system, :system
|
105
120
|
def system(*args)
|
106
121
|
Open3.popen2(*args) do |i, o, _t|
|
@@ -120,6 +135,31 @@ module ::Kernel
|
|
120
135
|
break
|
121
136
|
end
|
122
137
|
end
|
138
|
+
|
139
|
+
alias_method :orig_trap, :trap
|
140
|
+
def trap(sig, command = nil, &block)
|
141
|
+
return orig_trap(sig, command) if command.is_a? String
|
142
|
+
|
143
|
+
block = command if command.respond_to?(:call) && !block
|
144
|
+
exception = command.is_a?(Class) && command.new
|
145
|
+
|
146
|
+
# The signal trap can be invoked at any time, including while the system
|
147
|
+
# agent is blocking while polling for events. In order to deal with this
|
148
|
+
# correctly, we spin a fiber that will run the signal handler code, then
|
149
|
+
# call break_out_of_ev_loop, which will put the fiber at the front of the
|
150
|
+
# run queue, then wake up the system agent.
|
151
|
+
#
|
152
|
+
# If the command argument is an exception class however, it will be raised
|
153
|
+
# directly in the context of the main fiber.
|
154
|
+
orig_trap(sig) do
|
155
|
+
if exception
|
156
|
+
Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
|
157
|
+
else
|
158
|
+
fiber = spin { snooze; block.call }
|
159
|
+
Thread.current.break_out_of_ev_loop(fiber, nil)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
123
163
|
end
|
124
164
|
|
125
165
|
# Override Timeout to use cancel scope
|
@@ -189,7 +189,7 @@ module Polyphony
|
|
189
189
|
end
|
190
190
|
|
191
191
|
def receive_pending
|
192
|
-
@mailbox.
|
192
|
+
@mailbox.shift_all
|
193
193
|
end
|
194
194
|
end
|
195
195
|
|
@@ -221,7 +221,14 @@ module Polyphony
|
|
221
221
|
def await_all_children
|
222
222
|
return unless @children && !@children.empty?
|
223
223
|
|
224
|
-
|
224
|
+
@results = @children.dup
|
225
|
+
@on_child_done = proc do |c, r|
|
226
|
+
@results[c] = r
|
227
|
+
self.schedule if @children.empty?
|
228
|
+
end
|
229
|
+
suspend
|
230
|
+
@on_child_done = nil
|
231
|
+
@results.values
|
225
232
|
end
|
226
233
|
|
227
234
|
def shutdown_all_children
|
@@ -370,15 +377,3 @@ class ::Fiber
|
|
370
377
|
end
|
371
378
|
|
372
379
|
Fiber.current.setup_main_fiber
|
373
|
-
|
374
|
-
# This at_exit handler is needed only when the original process exits. Due to
|
375
|
-
# the behaviour of fibers on fork (and especially on exit from forked
|
376
|
-
# processes,) we use a separate mechanism to terminate fibers in forked
|
377
|
-
# processes (see Polyphony.fork).
|
378
|
-
orig_pid = Process.pid
|
379
|
-
at_exit do
|
380
|
-
next unless orig_pid == Process.pid
|
381
|
-
|
382
|
-
Polyphony.terminate_threads
|
383
|
-
Fiber.current.shutdown_all_children
|
384
|
-
end
|
@@ -97,7 +97,7 @@ class ::IO
|
|
97
97
|
# end
|
98
98
|
|
99
99
|
alias_method :orig_read, :read
|
100
|
-
def read(len =
|
100
|
+
def read(len = nil)
|
101
101
|
@read_buffer ||= +''
|
102
102
|
result = Thread.current.agent.read(self, @read_buffer, len, true)
|
103
103
|
return nil unless result
|
@@ -108,19 +108,23 @@ class ::IO
|
|
108
108
|
end
|
109
109
|
|
110
110
|
alias_method :orig_readpartial, :read
|
111
|
-
def readpartial(len)
|
111
|
+
def readpartial(len, str = nil)
|
112
112
|
@read_buffer ||= +''
|
113
113
|
result = Thread.current.agent.read(self, @read_buffer, len, false)
|
114
114
|
raise EOFError unless result
|
115
115
|
|
116
|
-
|
116
|
+
if str
|
117
|
+
str << @read_buffer
|
118
|
+
else
|
119
|
+
str = @read_buffer
|
120
|
+
end
|
117
121
|
@read_buffer = +''
|
118
|
-
|
122
|
+
str
|
119
123
|
end
|
120
124
|
|
121
125
|
alias_method :orig_write, :write
|
122
|
-
def write(str)
|
123
|
-
Thread.current.agent.write(self, str)
|
126
|
+
def write(str, *args)
|
127
|
+
Thread.current.agent.write(self, str, *args)
|
124
128
|
end
|
125
129
|
|
126
130
|
alias_method :orig_write_chevron, :<<
|
@@ -166,16 +170,13 @@ class ::IO
|
|
166
170
|
return
|
167
171
|
end
|
168
172
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
str << a
|
175
|
-
str << "\n" unless a =~ /\n$/
|
176
|
-
end
|
173
|
+
strs = args.inject([]) do |m, a|
|
174
|
+
a = a.to_s
|
175
|
+
m << a
|
176
|
+
m << "\n" unless a =~ /\n$/
|
177
|
+
m
|
177
178
|
end
|
178
|
-
write
|
179
|
+
write *strs
|
179
180
|
nil
|
180
181
|
end
|
181
182
|
|
@@ -193,7 +194,7 @@ class ::IO
|
|
193
194
|
|
194
195
|
alias_method :orig_write_nonblock, :write_nonblock
|
195
196
|
def write_nonblock(string, _options = {})
|
196
|
-
write(string
|
197
|
+
write(string)
|
197
198
|
end
|
198
199
|
|
199
200
|
alias_method :orig_read_nonblock, :read_nonblock
|
@@ -5,6 +5,12 @@ require_relative './socket'
|
|
5
5
|
|
6
6
|
# Open ssl socket helper methods (to make it compatible with Socket API)
|
7
7
|
class ::OpenSSL::SSL::SSLSocket
|
8
|
+
alias_method :orig_initialize, :initialize
|
9
|
+
def initialize(socket, context = nil)
|
10
|
+
socket = socket.respond_to?(:io) ? socket.io || socket : socket
|
11
|
+
context ? orig_initialize(socket, context) : orig_initialize(socket)
|
12
|
+
end
|
13
|
+
|
8
14
|
def dont_linger
|
9
15
|
io.dont_linger
|
10
16
|
end
|
@@ -35,6 +41,7 @@ class ::OpenSSL::SSL::SSLSocket
|
|
35
41
|
loop do
|
36
42
|
case (result = read_nonblock(maxlen, buf, exception: false))
|
37
43
|
when :wait_readable then Thread.current.agent.wait_io(io, false)
|
44
|
+
when :wait_writable then Thread.current.agent.wait_io(io, true)
|
38
45
|
else return result
|
39
46
|
end
|
40
47
|
end
|
@@ -44,6 +51,7 @@ class ::OpenSSL::SSL::SSLSocket
|
|
44
51
|
def syswrite(buf)
|
45
52
|
loop do
|
46
53
|
case (result = write_nonblock(buf, exception: false))
|
54
|
+
when :wait_readable then Thread.current.agent.wait_io(io, false)
|
47
55
|
when :wait_writable then Thread.current.agent.wait_io(io, true)
|
48
56
|
else
|
49
57
|
return result
|
@@ -5,6 +5,16 @@ require 'socket'
|
|
5
5
|
require_relative './io'
|
6
6
|
require_relative '../core/thread_pool'
|
7
7
|
|
8
|
+
class ::BasicSocket
|
9
|
+
def write_nonblock(string, _options = {})
|
10
|
+
write(string)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_nonblock(maxlen, str = nil, _options = {})
|
14
|
+
readpartial(maxlen, str)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
8
18
|
# Socket overrides (eventually rewritten in C)
|
9
19
|
class ::Socket
|
10
20
|
def accept
|
@@ -77,6 +87,8 @@ end
|
|
77
87
|
class ::TCPSocket
|
78
88
|
NO_EXCEPTION = { exception: false }.freeze
|
79
89
|
|
90
|
+
attr_reader :io
|
91
|
+
|
80
92
|
def initialize(remote_host, remote_port, local_host = nil, local_port = nil)
|
81
93
|
@io = Socket.new Socket::AF_INET, Socket::SOCK_STREAM
|
82
94
|
if local_host && local_port
|
data/lib/polyphony/version.rb
CHANGED
data/test/helper.rb
CHANGED
data/test/q.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'fiber'
|
5
|
+
require_relative '../lib/polyphony_ext'
|
6
|
+
|
7
|
+
queue = Polyphony::LibevQueue.new
|
8
|
+
|
9
|
+
queue.push :a
|
10
|
+
queue.push :b
|
11
|
+
queue.push :c
|
12
|
+
p [queue.shift_no_wait]
|
13
|
+
queue.push :d
|
14
|
+
p [queue.shift_no_wait]
|
15
|
+
p [queue.shift_no_wait]
|
16
|
+
p [queue.shift_no_wait]
|
17
|
+
p [queue.shift_no_wait]
|
18
|
+
|
19
|
+
queue.unshift :e
|
20
|
+
p [queue.shift_no_wait]
|
21
|
+
|
22
|
+
queue.push :f
|
23
|
+
queue.push :g
|
24
|
+
p [queue.shift_no_wait]
|
data/test/test_agent.rb
CHANGED
@@ -97,7 +97,7 @@ class AgentTest < MiniTest::Test
|
|
97
97
|
o.close
|
98
98
|
|
99
99
|
# read_loop will snooze after every read
|
100
|
-
|
100
|
+
6.times { snooze }
|
101
101
|
|
102
102
|
assert_equal [:ready, 'foo', 'bar', :done], buf
|
103
103
|
end
|
@@ -111,12 +111,12 @@ class AgentTest < MiniTest::Test
|
|
111
111
|
end
|
112
112
|
|
113
113
|
c1 = TCPSocket.new('127.0.0.1', 1234)
|
114
|
-
snooze
|
114
|
+
10.times { snooze }
|
115
115
|
|
116
116
|
assert_equal 1, clients.size
|
117
117
|
|
118
118
|
c2 = TCPSocket.new('127.0.0.1', 1234)
|
119
|
-
|
119
|
+
10.times { snooze }
|
120
120
|
|
121
121
|
assert_equal 2, clients.size
|
122
122
|
|