polyphony 0.43.8 → 0.45.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/.rubocop.yml +7 -1
- data/CHANGELOG.md +38 -0
- data/Gemfile.lock +13 -11
- data/README.md +20 -5
- data/Rakefile +1 -1
- data/TODO.md +16 -14
- data/bin/stress.rb +28 -0
- data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
- data/docs/api-reference/thread.md +1 -1
- data/docs/getting-started/overview.md +14 -14
- data/docs/getting-started/tutorial.md +1 -1
- data/examples/adapters/sequel_mysql.rb +23 -0
- data/examples/adapters/sequel_mysql_pool.rb +33 -0
- data/examples/core/{xx-agent.rb → xx-backend.rb} +5 -5
- data/examples/core/xx-channels.rb +4 -2
- data/examples/core/xx-using-a-mutex.rb +2 -1
- data/examples/io/xx-pry.rb +18 -0
- data/examples/io/xx-rack_server.rb +71 -0
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
- data/ext/polyphony/backend.h +41 -0
- data/ext/polyphony/event.c +86 -0
- data/ext/polyphony/extconf.rb +1 -1
- data/ext/polyphony/fiber.c +0 -5
- data/ext/polyphony/{libev_agent.c → libev_backend.c} +234 -228
- data/ext/polyphony/polyphony.c +4 -0
- data/ext/polyphony/polyphony.h +16 -16
- data/ext/polyphony/polyphony_ext.c +4 -2
- data/ext/polyphony/queue.c +52 -12
- data/ext/polyphony/thread.c +55 -42
- data/lib/polyphony.rb +25 -39
- data/lib/polyphony/adapters/irb.rb +2 -17
- data/lib/polyphony/adapters/mysql2.rb +19 -0
- data/lib/polyphony/adapters/postgres.rb +5 -5
- data/lib/polyphony/adapters/process.rb +2 -2
- data/lib/polyphony/adapters/readline.rb +17 -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/global_api.rb +11 -6
- data/lib/polyphony/core/resource_pool.rb +22 -71
- data/lib/polyphony/core/sync.rb +48 -9
- data/lib/polyphony/core/throttler.rb +1 -1
- data/lib/polyphony/extensions/core.rb +37 -19
- data/lib/polyphony/extensions/fiber.rb +5 -1
- data/lib/polyphony/extensions/io.rb +7 -8
- data/lib/polyphony/extensions/openssl.rb +6 -6
- data/lib/polyphony/extensions/socket.rb +12 -22
- data/lib/polyphony/extensions/thread.rb +6 -5
- data/lib/polyphony/net.rb +2 -1
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +6 -3
- data/test/helper.rb +1 -1
- data/test/{test_agent.rb → test_backend.rb} +22 -22
- data/test/test_event.rb +1 -0
- data/test/test_fiber.rb +21 -5
- data/test/test_io.rb +1 -1
- data/test/test_kernel.rb +5 -0
- data/test/test_queue.rb +20 -0
- data/test/test_resource_pool.rb +34 -43
- data/test/test_signal.rb +5 -29
- data/test/test_sync.rb +52 -0
- metadata +74 -30
- data/.gitbook.yaml +0 -4
- data/lib/polyphony/event.rb +0 -17
@@ -16,7 +16,7 @@ if Object.constants.include?(:Reline)
|
|
16
16
|
fiber.cancel
|
17
17
|
end
|
18
18
|
read_ios.each do |io|
|
19
|
-
Thread.current.
|
19
|
+
Thread.current.backend.wait_io(io, false)
|
20
20
|
return [io]
|
21
21
|
end
|
22
22
|
rescue Polyphony::Cancel
|
@@ -26,22 +26,7 @@ if Object.constants.include?(:Reline)
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
else
|
29
|
-
|
30
|
-
# thread pool. That way, the reactor loop can keep running while waiting for
|
31
|
-
# readline to return
|
32
|
-
module ::Readline
|
33
|
-
alias_method :orig_readline, :readline
|
34
|
-
|
35
|
-
Workers = Polyphony::ThreadPool.new
|
36
|
-
|
37
|
-
def readline(*args)
|
38
|
-
p :readline
|
39
|
-
# caller.each do |l|
|
40
|
-
# STDOUT.orig_puts l
|
41
|
-
# end
|
42
|
-
Workers.process { orig_readline(*args) }
|
43
|
-
end
|
44
|
-
end
|
29
|
+
require_relative './readline'
|
45
30
|
|
46
31
|
# RubyLex patches
|
47
32
|
class ::RubyLex
|
@@ -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.backend.wait_io(@io, false)
|
17
|
+
async_result
|
18
|
+
end
|
19
|
+
end)
|
@@ -15,8 +15,8 @@ module ::PG
|
|
15
15
|
res = conn.connect_poll
|
16
16
|
case res
|
17
17
|
when PGRES_POLLING_FAILED then raise Error, conn.error_message
|
18
|
-
when PGRES_POLLING_READING then Thread.current.
|
19
|
-
when PGRES_POLLING_WRITING then Thread.current.
|
18
|
+
when PGRES_POLLING_READING then Thread.current.backend.wait_io(socket_io, false)
|
19
|
+
when PGRES_POLLING_WRITING then Thread.current.backend.wait_io(socket_io, true)
|
20
20
|
when PGRES_POLLING_OK then return conn.setnonblocking(true)
|
21
21
|
end
|
22
22
|
end
|
@@ -42,7 +42,7 @@ class ::PG::Connection
|
|
42
42
|
|
43
43
|
def get_result(&block)
|
44
44
|
while is_busy
|
45
|
-
Thread.current.
|
45
|
+
Thread.current.backend.wait_io(socket_io, false)
|
46
46
|
consume_input
|
47
47
|
end
|
48
48
|
orig_get_result(&block)
|
@@ -59,7 +59,7 @@ class ::PG::Connection
|
|
59
59
|
|
60
60
|
def block(_timeout = 0)
|
61
61
|
while is_busy
|
62
|
-
Thread.current.
|
62
|
+
Thread.current.backend.wait_io(socket_io, false)
|
63
63
|
consume_input
|
64
64
|
end
|
65
65
|
end
|
@@ -97,7 +97,7 @@ class ::PG::Connection
|
|
97
97
|
return move_on_after(timeout) { wait_for_notify(&block) } if timeout
|
98
98
|
|
99
99
|
loop do
|
100
|
-
Thread.current.
|
100
|
+
Thread.current.backend.wait_io(socket_io, false)
|
101
101
|
consume_input
|
102
102
|
notice = notifies
|
103
103
|
next unless notice
|
@@ -7,7 +7,7 @@ module Polyphony
|
|
7
7
|
def watch(cmd = nil, &block)
|
8
8
|
terminated = nil
|
9
9
|
pid = cmd ? Kernel.spawn(cmd) : Polyphony.fork(&block)
|
10
|
-
Thread.current.
|
10
|
+
Thread.current.backend.waitpid(pid)
|
11
11
|
terminated = true
|
12
12
|
ensure
|
13
13
|
kill_process(pid) unless terminated || pid.nil?
|
@@ -23,7 +23,7 @@ module Polyphony
|
|
23
23
|
|
24
24
|
def kill_and_await(sig, pid)
|
25
25
|
::Process.kill(sig, pid)
|
26
|
-
Thread.current.
|
26
|
+
Thread.current.backend.waitpid(pid)
|
27
27
|
rescue SystemCallError
|
28
28
|
# ignore
|
29
29
|
puts 'SystemCallError in kill_and_await'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'polyphony'
|
4
|
+
require 'readline'
|
5
|
+
|
6
|
+
# readline blocks the current thread, so we offload it to the blocking-ops
|
7
|
+
# thread pool. That way, the reactor loop can keep running while waiting for
|
8
|
+
# readline to return
|
9
|
+
module ::Readline
|
10
|
+
alias_method :orig_readline, :readline
|
11
|
+
|
12
|
+
Worker = Polyphony::ThreadPool.new(1)
|
13
|
+
|
14
|
+
def readline(*args)
|
15
|
+
Worker.process { orig_readline(*args) }
|
16
|
+
end
|
17
|
+
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
|
@@ -19,13 +19,18 @@ module Polyphony
|
|
19
19
|
fiber = ::Fiber.current
|
20
20
|
canceller = spin do
|
21
21
|
sleep interval
|
22
|
-
exception = with_exception
|
23
|
-
with_exception.new : RuntimeError.new(with_exception)
|
22
|
+
exception = cancel_exception(with_exception)
|
24
23
|
fiber.schedule exception
|
25
24
|
end
|
26
25
|
block ? cancel_after_wrap_block(canceller, &block) : canceller
|
27
26
|
end
|
28
27
|
|
28
|
+
def cancel_exception(exception)
|
29
|
+
return exception.new if exception.is_a?(Class)
|
30
|
+
|
31
|
+
RuntimeError.new(exception)
|
32
|
+
end
|
33
|
+
|
29
34
|
def cancel_after_wrap_block(canceller, &block)
|
30
35
|
block.call
|
31
36
|
ensure
|
@@ -50,7 +55,7 @@ module Polyphony
|
|
50
55
|
next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + interval
|
51
56
|
loop do
|
52
57
|
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
53
|
-
Thread.current.
|
58
|
+
Thread.current.backend.sleep(next_time - now)
|
54
59
|
yield
|
55
60
|
loop do
|
56
61
|
next_time += interval
|
@@ -98,14 +103,14 @@ module Polyphony
|
|
98
103
|
def sleep(duration = nil)
|
99
104
|
return sleep_forever unless duration
|
100
105
|
|
101
|
-
Thread.current.
|
106
|
+
Thread.current.backend.sleep duration
|
102
107
|
end
|
103
108
|
|
104
109
|
def sleep_forever
|
105
|
-
Thread.current.
|
110
|
+
Thread.current.backend.ref
|
106
111
|
loop { sleep 60 }
|
107
112
|
ensure
|
108
|
-
Thread.current.
|
113
|
+
Thread.current.backend.unref
|
109
114
|
end
|
110
115
|
|
111
116
|
def throttled_loop(rate, count: nil, &block)
|
@@ -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.delete fiber
|
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
41
|
|
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
|
-
|
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,57 @@ 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
|
22
|
+
end
|
23
|
+
|
24
|
+
def conditional_release
|
25
|
+
@store << @token
|
26
|
+
@token = nil
|
27
|
+
@holding_fiber = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def conditional_reacquire
|
31
|
+
@token = @store.shift
|
32
|
+
@holding_fiber = Fiber.current
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Implements a fiber-aware ConditionVariable
|
37
|
+
class ConditionVariable
|
38
|
+
def initialize
|
39
|
+
@queue = Polyphony::Queue.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait(mutex, _timeout = nil)
|
43
|
+
mutex.conditional_release
|
44
|
+
@queue << Fiber.current
|
45
|
+
Thread.current.backend.wait_event(true)
|
46
|
+
mutex.conditional_reacquire
|
47
|
+
end
|
48
|
+
|
49
|
+
def signal
|
50
|
+
fiber = @queue.shift
|
51
|
+
fiber.schedule
|
52
|
+
end
|
53
|
+
|
54
|
+
def broadcast
|
55
|
+
while (fiber = @queue.shift)
|
56
|
+
fiber.schedule
|
57
|
+
end
|
19
58
|
end
|
20
59
|
end
|
21
60
|
end
|