polyphony 0.43.6 → 0.44.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 +45 -0
- data/Gemfile.lock +5 -1
- data/README.md +20 -5
- data/TODO.md +10 -14
- data/bin/stress.rb +28 -0
- data/docs/getting-started/overview.md +2 -2
- data/examples/adapters/sequel_mysql.rb +23 -0
- data/examples/adapters/sequel_mysql_pool.rb +33 -0
- data/examples/core/xx-channels.rb +4 -2
- data/examples/core/xx-using-a-mutex.rb +2 -1
- data/examples/performance/fiber_transfer.rb +47 -0
- data/ext/polyphony/agent.h +41 -0
- data/ext/polyphony/event.c +86 -0
- data/ext/polyphony/fiber.c +0 -5
- data/ext/polyphony/libev_agent.c +201 -128
- data/ext/polyphony/polyphony.c +4 -2
- data/ext/polyphony/polyphony.h +24 -24
- data/ext/polyphony/polyphony_ext.c +4 -2
- data/ext/polyphony/queue.c +208 -0
- data/ext/polyphony/ring_buffer.c +0 -24
- data/ext/polyphony/thread.c +53 -38
- data/lib/polyphony.rb +13 -31
- data/lib/polyphony/adapters/mysql2.rb +19 -0
- data/lib/polyphony/adapters/sequel.rb +45 -0
- data/lib/polyphony/core/channel.rb +3 -34
- data/lib/polyphony/core/exceptions.rb +11 -0
- data/lib/polyphony/core/resource_pool.rb +23 -72
- data/lib/polyphony/core/sync.rb +12 -9
- data/lib/polyphony/extensions/core.rb +15 -8
- data/lib/polyphony/extensions/fiber.rb +4 -0
- data/lib/polyphony/extensions/socket.rb +9 -9
- data/lib/polyphony/extensions/thread.rb +1 -1
- data/lib/polyphony/net.rb +2 -1
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +2 -0
- data/test/helper.rb +2 -2
- data/test/test_agent.rb +2 -2
- data/test/test_event.rb +12 -0
- data/test/test_fiber.rb +17 -1
- data/test/test_io.rb +14 -0
- data/test/test_queue.rb +33 -0
- data/test/test_resource_pool.rb +34 -43
- data/test/test_signal.rb +2 -26
- data/test/test_socket.rb +0 -43
- data/test/test_trace.rb +18 -17
- metadata +40 -5
- data/ext/polyphony/libev_queue.c +0 -288
- data/lib/polyphony/event.rb +0 -27
data/lib/polyphony.rb
CHANGED
@@ -4,9 +4,6 @@ require 'fiber'
|
|
4
4
|
require_relative './polyphony_ext'
|
5
5
|
|
6
6
|
module Polyphony
|
7
|
-
# Map Queue to Libev queue implementation
|
8
|
-
Queue = LibevQueue
|
9
|
-
|
10
7
|
# replace core Queue class with our own
|
11
8
|
verbose = $VERBOSE
|
12
9
|
$VERBOSE = nil
|
@@ -20,37 +17,29 @@ require_relative './polyphony/extensions/fiber'
|
|
20
17
|
require_relative './polyphony/extensions/io'
|
21
18
|
|
22
19
|
Thread.current.setup_fiber_scheduling
|
23
|
-
Thread.current.agent = Polyphony::
|
20
|
+
Thread.current.agent = Polyphony::Agent.new
|
24
21
|
|
25
22
|
require_relative './polyphony/core/global_api'
|
26
23
|
require_relative './polyphony/core/resource_pool'
|
27
24
|
require_relative './polyphony/net'
|
28
25
|
require_relative './polyphony/adapters/process'
|
29
|
-
require_relative './polyphony/event'
|
30
26
|
|
31
27
|
# Main Polyphony API
|
32
28
|
module Polyphony
|
33
29
|
class << self
|
34
|
-
def wait_for_signal(sig)
|
35
|
-
raise "should be reimplemented"
|
36
|
-
|
37
|
-
fiber = Fiber.current
|
38
|
-
# Polyphony.ref
|
39
|
-
old_trap = trap(sig) do
|
40
|
-
# Polyphony.unref
|
41
|
-
fiber.schedule(sig)
|
42
|
-
trap(sig, old_trap)
|
43
|
-
end
|
44
|
-
suspend
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
30
|
def fork(&block)
|
49
31
|
Kernel.fork do
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
32
|
+
# A race condition can arise if a TERM or INT signal is received before
|
33
|
+
# the forked process has finished initializing. To prevent this we restore
|
34
|
+
# the default signal handlers, and then reinstall the custom Polyphony
|
35
|
+
# handlers just before running the given block.
|
36
|
+
trap('SIGTERM', 'DEFAULT')
|
37
|
+
trap('SIGINT', 'DEFAULT')
|
38
|
+
|
39
|
+
# Since the fiber doing the fork will become the main fiber of the
|
40
|
+
# forked process, we leave it behind by transferring to a new fiber
|
41
|
+
# created in the context of the forked process, which rescues *all*
|
42
|
+
# exceptions, including Interrupt and SystemExit.
|
54
43
|
spin_forked_block(&block).transfer
|
55
44
|
end
|
56
45
|
end
|
@@ -61,7 +50,7 @@ module Polyphony
|
|
61
50
|
rescue SystemExit
|
62
51
|
# fall through to ensure
|
63
52
|
rescue Exception => e
|
64
|
-
e.full_message
|
53
|
+
warn e.full_message
|
65
54
|
exit!
|
66
55
|
ensure
|
67
56
|
exit_forked_process
|
@@ -69,13 +58,6 @@ module Polyphony
|
|
69
58
|
end
|
70
59
|
|
71
60
|
def run_forked_block(&block)
|
72
|
-
# A race condition can arise if a TERM or INT signal is received before
|
73
|
-
# the forked process has finished initializing. To prevent this we restore
|
74
|
-
# the default signal handlers, and then reinstall the custom Polyphony
|
75
|
-
# handlers just before running the given block.
|
76
|
-
trap('SIGTERM', 'DEFAULT')
|
77
|
-
trap('SIGINT', 'DEFAULT')
|
78
|
-
|
79
61
|
Thread.current.setup
|
80
62
|
Fiber.current.setup_main_fiber
|
81
63
|
Thread.current.agent.post_fork
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../polyphony'
|
4
|
+
require 'mysql2/client'
|
5
|
+
|
6
|
+
# Mysql2::Client overrides
|
7
|
+
Mysql2::Client.prepend(Module.new do
|
8
|
+
def initialize(config)
|
9
|
+
config[:async] = true
|
10
|
+
super
|
11
|
+
@io = ::IO.for_fd(socket)
|
12
|
+
end
|
13
|
+
|
14
|
+
def query(sql, **options)
|
15
|
+
super
|
16
|
+
Thread.current.agent.wait_io(@io, false)
|
17
|
+
async_result
|
18
|
+
end
|
19
|
+
end)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../polyphony'
|
4
|
+
require 'sequel'
|
5
|
+
|
6
|
+
module Polyphony
|
7
|
+
# Sequel ConnectionPool that delegates to Polyphony::ResourcePool.
|
8
|
+
class FiberConnectionPool < Sequel::ConnectionPool
|
9
|
+
def initialize(db, opts = OPTS)
|
10
|
+
super
|
11
|
+
max_size = Integer(opts[:max_connections] || 4)
|
12
|
+
@pool = Polyphony::ResourcePool.new(limit: max_size) { make_new(:default) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def hold(_server = nil)
|
16
|
+
@pool.acquire do |conn|
|
17
|
+
yield conn
|
18
|
+
rescue Polyphony::BaseException
|
19
|
+
# The connection may be in an unrecoverable state if interrupted,
|
20
|
+
# discard the connection from the pool so it isn't reused.
|
21
|
+
@pool.discard!
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def size
|
27
|
+
@pool.size
|
28
|
+
end
|
29
|
+
|
30
|
+
def max_size
|
31
|
+
@pool.limit
|
32
|
+
end
|
33
|
+
|
34
|
+
def preconnect(_concurrent = false)
|
35
|
+
@pool.preheat!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Override Sequel::Database to use FiberConnectionPool by default.
|
40
|
+
Sequel::Database.prepend(Module.new do
|
41
|
+
def connection_pool_default_options
|
42
|
+
{pool_class: FiberConnectionPool}
|
43
|
+
end
|
44
|
+
end)
|
45
|
+
end
|
@@ -5,42 +5,11 @@ require_relative './exceptions'
|
|
5
5
|
module Polyphony
|
6
6
|
# Implements a unidirectional communication channel along the lines of Go
|
7
7
|
# (buffered) channels.
|
8
|
-
class Channel
|
9
|
-
|
10
|
-
@payload_queue = []
|
11
|
-
@waiting_queue = []
|
12
|
-
end
|
8
|
+
class Channel < Polyphony::Queue
|
9
|
+
alias_method :receive, :shift
|
13
10
|
|
14
11
|
def close
|
15
|
-
|
16
|
-
@waiting_queue.slice(0..-1).each { |f| f.schedule(stop) }
|
17
|
-
end
|
18
|
-
|
19
|
-
def <<(value)
|
20
|
-
if @waiting_queue.empty?
|
21
|
-
@payload_queue << value
|
22
|
-
else
|
23
|
-
@waiting_queue.shift&.schedule(value)
|
24
|
-
end
|
25
|
-
snooze
|
26
|
-
end
|
27
|
-
|
28
|
-
def receive
|
29
|
-
Thread.current.agent.ref
|
30
|
-
if @payload_queue.empty?
|
31
|
-
@waiting_queue << Fiber.current
|
32
|
-
suspend
|
33
|
-
else
|
34
|
-
receive_from_queue
|
35
|
-
end
|
36
|
-
ensure
|
37
|
-
Thread.current.agent.unref
|
38
|
-
end
|
39
|
-
|
40
|
-
def receive_from_queue
|
41
|
-
payload = @payload_queue.shift
|
42
|
-
snooze
|
43
|
-
payload
|
12
|
+
flush_waiters(Polyphony::MoveOn.new)
|
44
13
|
end
|
45
14
|
end
|
46
15
|
end
|
@@ -33,4 +33,15 @@ module Polyphony
|
|
33
33
|
|
34
34
|
# Restart is used to restart a fiber
|
35
35
|
class Restart < BaseException; end
|
36
|
+
|
37
|
+
# Interjection is used to run arbitrary code on arbitrary fibers at any point
|
38
|
+
class Interjection < BaseException
|
39
|
+
def initialize(proc)
|
40
|
+
@proc = proc
|
41
|
+
end
|
42
|
+
|
43
|
+
def invoke
|
44
|
+
@proc.call
|
45
|
+
end
|
46
|
+
end
|
36
47
|
end
|
@@ -10,73 +10,35 @@ module Polyphony
|
|
10
10
|
# @param &block [Proc] allocator block
|
11
11
|
def initialize(opts, &block)
|
12
12
|
@allocator = block
|
13
|
-
|
14
|
-
@stock = []
|
15
|
-
@queue = []
|
16
|
-
@acquired_resources = {}
|
17
|
-
|
18
13
|
@limit = opts[:limit] || 4
|
19
14
|
@size = 0
|
15
|
+
@stock = Polyphony::Queue.new
|
16
|
+
@acquired_resources = {}
|
20
17
|
end
|
21
18
|
|
22
19
|
def available
|
23
20
|
@stock.size
|
24
21
|
end
|
25
22
|
|
26
|
-
def acquire
|
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[fiber] = nil
|
40
|
-
Thread.current.agent.unref
|
41
|
-
release(resource) if resource
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def wait_for_resource
|
23
|
+
def acquire(&block)
|
47
24
|
fiber = Fiber.current
|
48
|
-
@
|
49
|
-
ready_resource = from_stock
|
50
|
-
return ready_resource if ready_resource
|
25
|
+
return yield @acquired_resources[fiber] if @acquired_resources[fiber]
|
51
26
|
|
52
|
-
|
53
|
-
ensure
|
54
|
-
@queue.delete(fiber)
|
27
|
+
acquire_from_stock(fiber, &block)
|
55
28
|
end
|
56
29
|
|
57
|
-
def
|
58
|
-
if
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
30
|
+
def acquire_from_stock(fiber)
|
31
|
+
add_to_stock if (@stock.empty? || @stock.pending?) && @size < @limit
|
32
|
+
resource = @stock.shift
|
33
|
+
@acquired_resources[fiber] = resource
|
34
|
+
yield resource
|
35
|
+
ensure
|
36
|
+
if resource && @acquired_resources[fiber] == resource
|
37
|
+
@acquired_resources.delete(fiber)
|
38
|
+
@stock.push resource
|
63
39
|
end
|
64
40
|
end
|
65
|
-
|
66
|
-
def dequeue
|
67
|
-
return if @queue.empty? || @stock.empty?
|
68
|
-
|
69
|
-
@queue.shift.schedule(@stock.shift)
|
70
|
-
end
|
71
|
-
|
72
|
-
def return_to_stock(resource)
|
73
|
-
@stock << resource
|
74
|
-
end
|
75
|
-
|
76
|
-
def from_stock
|
77
|
-
@stock.shift || (@size < @limit && allocate)
|
78
|
-
end
|
79
|
-
|
41
|
+
|
80
42
|
def method_missing(sym, *args, &block)
|
81
43
|
acquire { |r| r.send(sym, *args, &block) }
|
82
44
|
end
|
@@ -85,33 +47,22 @@ module Polyphony
|
|
85
47
|
true
|
86
48
|
end
|
87
49
|
|
88
|
-
# Extension to allow discarding of resources
|
89
|
-
module ResourceExtensions
|
90
|
-
def __discarded__
|
91
|
-
@__discarded__
|
92
|
-
end
|
93
|
-
|
94
|
-
def __discard__
|
95
|
-
@__discarded__ = true
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
50
|
# Allocates a resource
|
100
51
|
# @return [any] allocated resource
|
101
|
-
def
|
52
|
+
def add_to_stock
|
102
53
|
@size += 1
|
103
|
-
@allocator.
|
54
|
+
resource = @allocator.call
|
55
|
+
@stock << resource
|
104
56
|
end
|
105
57
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
@
|
110
|
-
dequeue
|
58
|
+
# Discards the currently-acquired resource
|
59
|
+
# instead of returning it to the pool when done.
|
60
|
+
def discard!
|
61
|
+
@size -= 1 if @acquired_resources.delete(Fiber.current)
|
111
62
|
end
|
112
63
|
|
113
64
|
def preheat!
|
114
|
-
|
65
|
+
add_to_stock while @size < @limit
|
115
66
|
end
|
116
67
|
end
|
117
68
|
end
|
data/lib/polyphony/core/sync.rb
CHANGED
@@ -4,18 +4,21 @@ module Polyphony
|
|
4
4
|
# Implements mutex lock for synchronizing access to a shared resource
|
5
5
|
class Mutex
|
6
6
|
def initialize
|
7
|
-
@
|
7
|
+
@store = Queue.new
|
8
|
+
@store << :token
|
8
9
|
end
|
9
10
|
|
10
11
|
def synchronize
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
return yield if @holding_fiber == Fiber.current
|
13
|
+
|
14
|
+
begin
|
15
|
+
token = @store.shift
|
16
|
+
@holding_fiber = Fiber.current
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
@holding_fiber = nil
|
20
|
+
@store << token if token
|
21
|
+
end
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|
@@ -46,6 +46,10 @@ class ::Exception
|
|
46
46
|
|
47
47
|
backtrace.reject { |l| l[POLYPHONY_DIR] }
|
48
48
|
end
|
49
|
+
|
50
|
+
def invoke
|
51
|
+
Kernel.raise(self)
|
52
|
+
end
|
49
53
|
end
|
50
54
|
|
51
55
|
# Overrides for Process
|
@@ -140,8 +144,16 @@ module ::Kernel
|
|
140
144
|
def trap(sig, command = nil, &block)
|
141
145
|
return orig_trap(sig, command) if command.is_a? String
|
142
146
|
|
143
|
-
block = command if command.respond_to?(:call)
|
144
|
-
|
147
|
+
block = command if !block && command.respond_to?(:call)
|
148
|
+
if block
|
149
|
+
exception = Polyphony::Interjection.new(block)
|
150
|
+
else
|
151
|
+
exception = command.is_a?(Class) && command.new
|
152
|
+
end
|
153
|
+
|
154
|
+
unless exception
|
155
|
+
raise ArgumentError, "Must supply block or exception or callable object"
|
156
|
+
end
|
145
157
|
|
146
158
|
# The signal trap can be invoked at any time, including while the system
|
147
159
|
# agent is blocking while polling for events. In order to deal with this
|
@@ -152,12 +164,7 @@ module ::Kernel
|
|
152
164
|
# If the command argument is an exception class however, it will be raised
|
153
165
|
# directly in the context of the main fiber.
|
154
166
|
orig_trap(sig) do
|
155
|
-
|
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
|
167
|
+
Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
|
161
168
|
end
|
162
169
|
end
|
163
170
|
end
|
@@ -24,15 +24,7 @@ class ::Socket
|
|
24
24
|
NO_EXCEPTION = { exception: false }.freeze
|
25
25
|
|
26
26
|
def connect(remotesockaddr)
|
27
|
-
|
28
|
-
result = connect_nonblock(remotesockaddr, **NO_EXCEPTION)
|
29
|
-
case result
|
30
|
-
when 0 then return
|
31
|
-
when :wait_writable then Thread.current.agent.wait_io(self, true)
|
32
|
-
else
|
33
|
-
raise IOError
|
34
|
-
end
|
35
|
-
end
|
27
|
+
Thread.current.agent.connect(self, remotesockaddr.ip_address, remotesockaddr.ip_port)
|
36
28
|
end
|
37
29
|
|
38
30
|
def recv(maxlen, flags = 0, outbuf = nil)
|
@@ -75,6 +67,10 @@ class ::Socket
|
|
75
67
|
setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
|
76
68
|
end
|
77
69
|
|
70
|
+
def reuse_port
|
71
|
+
setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
72
|
+
end
|
73
|
+
|
78
74
|
class << self
|
79
75
|
alias_method :orig_getaddrinfo, :getaddrinfo
|
80
76
|
def getaddrinfo(*args)
|
@@ -128,6 +124,10 @@ class ::TCPSocket
|
|
128
124
|
def reuse_addr
|
129
125
|
setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
|
130
126
|
end
|
127
|
+
|
128
|
+
def reuse_port
|
129
|
+
setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
130
|
+
end
|
131
131
|
end
|
132
132
|
|
133
133
|
# Override stock TCPServer code by encapsulating a Socket instance.
|