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