polyphony 0.43.6 → 0.44.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|