polyphony 0.43.3 → 0.43.9
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 +1 -1
- data/CHANGELOG.md +44 -0
- data/Gemfile.lock +1 -1
- data/README.md +21 -4
- data/TODO.md +1 -2
- data/bin/stress.rb +28 -0
- 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/core/xx-channels.rb +4 -2
- data/examples/core/xx-using-a-mutex.rb +2 -1
- 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/event.c +86 -0
- data/ext/polyphony/fiber.c +0 -5
- 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 +4 -2
- data/ext/polyphony/queue.c +187 -0
- data/ext/polyphony/ring_buffer.c +96 -0
- data/ext/polyphony/ring_buffer.h +28 -0
- data/ext/polyphony/thread.c +18 -12
- data/lib/polyphony.rb +5 -14
- data/lib/polyphony/core/channel.rb +3 -34
- data/lib/polyphony/core/global_api.rb +1 -1
- data/lib/polyphony/core/resource_pool.rb +13 -75
- data/lib/polyphony/core/sync.rb +12 -9
- data/lib/polyphony/core/thread_pool.rb +1 -1
- data/lib/polyphony/extensions/core.rb +34 -0
- data/lib/polyphony/extensions/fiber.rb +9 -2
- 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 +1 -1
- data/test/test_event.rb +12 -0
- data/test/test_global_api.rb +2 -2
- data/test/test_io.rb +24 -2
- data/test/test_queue.rb +59 -1
- data/test/test_resource_pool.rb +0 -43
- data/test/test_trace.rb +18 -17
- metadata +15 -5
- data/ext/polyphony/libev_queue.c +0 -217
- data/lib/polyphony/event.rb +0 -27
@@ -107,6 +107,15 @@ module ::Kernel
|
|
107
107
|
$stdin.gets
|
108
108
|
end
|
109
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
|
+
|
110
119
|
alias_method :orig_system, :system
|
111
120
|
def system(*args)
|
112
121
|
Open3.popen2(*args) do |i, o, _t|
|
@@ -126,6 +135,31 @@ module ::Kernel
|
|
126
135
|
break
|
127
136
|
end
|
128
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
|
129
163
|
end
|
130
164
|
|
131
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
|
@@ -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
data/test/test_event.rb
CHANGED
@@ -34,6 +34,7 @@ class EventTest < MiniTest::Test
|
|
34
34
|
}
|
35
35
|
}
|
36
36
|
snooze
|
37
|
+
|
37
38
|
t = Thread.new do
|
38
39
|
orig_sleep 0.001
|
39
40
|
3.times { a.signal }
|
@@ -45,4 +46,15 @@ class EventTest < MiniTest::Test
|
|
45
46
|
t&.kill
|
46
47
|
t&.join
|
47
48
|
end
|
49
|
+
|
50
|
+
def test_exception_while_waiting_for_event
|
51
|
+
e = Polyphony::Event.new
|
52
|
+
|
53
|
+
f = spin { e.await }
|
54
|
+
g = spin { f.raise 'foo' }
|
55
|
+
|
56
|
+
assert_raises(RuntimeError) do
|
57
|
+
f.await
|
58
|
+
end
|
59
|
+
end
|
48
60
|
end
|
data/test/test_global_api.rb
CHANGED
@@ -211,13 +211,13 @@ class SpinLoopTest < MiniTest::Test
|
|
211
211
|
|
212
212
|
def test_spin_loop_location
|
213
213
|
location = /^#{__FILE__}:#{__LINE__ + 1}/
|
214
|
-
f = spin_loop {}
|
214
|
+
f = spin_loop { snooze }
|
215
215
|
|
216
216
|
assert_match location, f.location
|
217
217
|
end
|
218
218
|
|
219
219
|
def test_spin_loop_tag
|
220
|
-
f = spin_loop(:my_loop) {}
|
220
|
+
f = spin_loop(:my_loop) { snooze }
|
221
221
|
|
222
222
|
assert_equal :my_loop, f.tag
|
223
223
|
end
|
data/test/test_io.rb
CHANGED
@@ -30,6 +30,14 @@ class IOTest < MiniTest::Test
|
|
30
30
|
assert_equal 'hello', msg
|
31
31
|
end
|
32
32
|
|
33
|
+
def test_write_multiple_arguments
|
34
|
+
i, o = IO.pipe
|
35
|
+
count = o.write('a', 'b', "\n", 'c')
|
36
|
+
assert_equal 4, count
|
37
|
+
o.close
|
38
|
+
assert_equal "ab\nc", i.read
|
39
|
+
end
|
40
|
+
|
33
41
|
def test_that_double_chevron_method_returns_io
|
34
42
|
assert_equal @o, @o << 'foo'
|
35
43
|
|
@@ -83,6 +91,20 @@ class IOTest < MiniTest::Test
|
|
83
91
|
|
84
92
|
assert_raises(EOFError) { i.readpartial(1) }
|
85
93
|
end
|
94
|
+
|
95
|
+
# see https://github.com/digital-fabric/polyphony/issues/30
|
96
|
+
def test_reopened_tempfile
|
97
|
+
file = Tempfile.new
|
98
|
+
file << 'hello: world'
|
99
|
+
file.close
|
100
|
+
|
101
|
+
buf = nil
|
102
|
+
File.open(file, 'r:bom|utf-8') do |f|
|
103
|
+
buf = f.read(16384)
|
104
|
+
end
|
105
|
+
|
106
|
+
assert_equal 'hello: world', buf
|
107
|
+
end
|
86
108
|
end
|
87
109
|
|
88
110
|
class IOClassMethodsTest < MiniTest::Test
|
@@ -121,7 +143,7 @@ class IOClassMethodsTest < MiniTest::Test
|
|
121
143
|
assert_equal "end\n", lines[-1]
|
122
144
|
end
|
123
145
|
|
124
|
-
def
|
146
|
+
def test_read_class_method
|
125
147
|
s = IO.read(__FILE__)
|
126
148
|
assert_kind_of String, s
|
127
149
|
assert(!s.empty?)
|
@@ -144,7 +166,7 @@ class IOClassMethodsTest < MiniTest::Test
|
|
144
166
|
|
145
167
|
WRITE_DATA = "foo\nbar קוקו"
|
146
168
|
|
147
|
-
def
|
169
|
+
def test_write_class_method
|
148
170
|
fn = '/tmp/test_write'
|
149
171
|
FileUtils.rm(fn) rescue nil
|
150
172
|
|
data/test/test_queue.rb
CHANGED
@@ -8,7 +8,7 @@ class QueueTest < MiniTest::Test
|
|
8
8
|
@queue = Polyphony::Queue.new
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
11
|
+
def test_push_shift
|
12
12
|
spin {
|
13
13
|
@queue << 42
|
14
14
|
}
|
@@ -21,6 +21,18 @@ class QueueTest < MiniTest::Test
|
|
21
21
|
assert_equal [1, 2, 3, 4], buf
|
22
22
|
end
|
23
23
|
|
24
|
+
def test_unshift
|
25
|
+
@queue.push 1
|
26
|
+
@queue.push 2
|
27
|
+
@queue.push 3
|
28
|
+
@queue.unshift 4
|
29
|
+
|
30
|
+
buf = []
|
31
|
+
buf << @queue.shift while !@queue.empty?
|
32
|
+
|
33
|
+
assert_equal [4, 1, 2, 3], buf
|
34
|
+
end
|
35
|
+
|
24
36
|
def test_multiple_waiters
|
25
37
|
a = spin { @queue.shift }
|
26
38
|
b = spin { @queue.shift }
|
@@ -41,6 +53,19 @@ class QueueTest < MiniTest::Test
|
|
41
53
|
buf = []
|
42
54
|
@queue.shift_each { |i| buf << i }
|
43
55
|
assert_equal [1, 2, 3, 4], buf
|
56
|
+
|
57
|
+
buf = []
|
58
|
+
@queue.shift_each { |i| buf << i }
|
59
|
+
assert_equal [], buf
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_shift_all
|
63
|
+
(1..4).each { |i| @queue << i }
|
64
|
+
buf = @queue.shift_all
|
65
|
+
assert_equal [1, 2, 3, 4], buf
|
66
|
+
|
67
|
+
buf = @queue.shift_all
|
68
|
+
assert_equal [], buf
|
44
69
|
end
|
45
70
|
|
46
71
|
def test_empty?
|
@@ -71,4 +96,37 @@ class QueueTest < MiniTest::Test
|
|
71
96
|
assert_nil f2.await
|
72
97
|
assert_equal :bar, f3.await
|
73
98
|
end
|
99
|
+
|
100
|
+
def test_fiber_removal_from_queue_simple
|
101
|
+
f1 = spin { @queue.shift }
|
102
|
+
|
103
|
+
# let fibers run
|
104
|
+
snooze
|
105
|
+
|
106
|
+
f1.stop
|
107
|
+
snooze
|
108
|
+
|
109
|
+
@queue << :foo
|
110
|
+
assert_nil f1.await
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_queue_size
|
114
|
+
assert_equal 0, @queue.size
|
115
|
+
|
116
|
+
@queue.push 1
|
117
|
+
|
118
|
+
assert_equal 1, @queue.size
|
119
|
+
|
120
|
+
@queue.push 2
|
121
|
+
|
122
|
+
assert_equal 2, @queue.size
|
123
|
+
|
124
|
+
@queue.shift
|
125
|
+
|
126
|
+
assert_equal 1, @queue.size
|
127
|
+
|
128
|
+
@queue.shift
|
129
|
+
|
130
|
+
assert_equal 0, @queue.size
|
131
|
+
end
|
74
132
|
end
|
data/test/test_resource_pool.rb
CHANGED
@@ -37,49 +37,6 @@ class ResourcePoolTest < MiniTest::Test
|
|
37
37
|
assert_equal 2, pool.size
|
38
38
|
end
|
39
39
|
|
40
|
-
def test_discard
|
41
|
-
resources = [+'a', +'b']
|
42
|
-
pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
|
43
|
-
|
44
|
-
results = []
|
45
|
-
4.times {
|
46
|
-
spin {
|
47
|
-
snooze
|
48
|
-
pool.acquire { |resource|
|
49
|
-
results << resource
|
50
|
-
resource.__discard__ if resource == 'b'
|
51
|
-
snooze
|
52
|
-
}
|
53
|
-
}
|
54
|
-
}
|
55
|
-
6.times { snooze }
|
56
|
-
|
57
|
-
assert_equal ['a', 'b', 'a', 'a'], results
|
58
|
-
assert_equal 1, pool.size
|
59
|
-
end
|
60
|
-
|
61
|
-
def test_add
|
62
|
-
resources = [+'a', +'b']
|
63
|
-
pool = Polyphony::ResourcePool.new(limit: 2) { resources.shift }
|
64
|
-
|
65
|
-
pool << +'c'
|
66
|
-
|
67
|
-
results = []
|
68
|
-
4.times {
|
69
|
-
spin {
|
70
|
-
snooze
|
71
|
-
pool.acquire { |resource|
|
72
|
-
results << resource
|
73
|
-
resource.__discard__ if resource == 'b'
|
74
|
-
snooze
|
75
|
-
}
|
76
|
-
}
|
77
|
-
}
|
78
|
-
6.times { snooze }
|
79
|
-
|
80
|
-
assert_equal ['c', 'a', 'c', 'a'], results
|
81
|
-
end
|
82
|
-
|
83
40
|
def test_single_resource_limit
|
84
41
|
resources = [+'a', +'b']
|
85
42
|
pool = Polyphony::ResourcePool.new(limit: 1) { resources.shift }
|