polyphony 0.36 → 0.42
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 +11 -2
- data/.gitignore +2 -2
- data/.rubocop.yml +30 -0
- data/CHANGELOG.md +28 -2
- data/Gemfile +0 -11
- data/Gemfile.lock +15 -14
- data/README.md +2 -1
- data/Rakefile +7 -3
- data/TODO.md +28 -95
- data/docs/_config.yml +56 -7
- data/docs/_sass/custom/custom.scss +0 -30
- data/docs/_sass/overrides.scss +0 -46
- data/docs/{user-guide → _user-guide}/all-about-timers.md +0 -0
- data/docs/_user-guide/index.md +9 -0
- data/docs/{user-guide → _user-guide}/web-server.md +0 -0
- data/docs/api-reference/fiber.md +2 -2
- data/docs/api-reference/index.md +9 -0
- data/docs/api-reference/polyphony-process.md +1 -1
- data/docs/api-reference/thread.md +1 -1
- data/docs/faq.md +21 -11
- data/docs/getting-started/index.md +10 -0
- data/docs/getting-started/installing.md +2 -6
- data/docs/getting-started/overview.md +507 -0
- data/docs/getting-started/tutorial.md +27 -19
- data/docs/index.md +3 -2
- data/docs/main-concepts/concurrency.md +0 -5
- data/docs/main-concepts/design-principles.md +69 -21
- data/docs/main-concepts/extending.md +1 -1
- data/docs/main-concepts/index.md +9 -0
- data/examples/core/01-spinning-up-fibers.rb +1 -0
- data/examples/core/03-interrupting.rb +4 -1
- data/examples/core/04-handling-signals.rb +19 -0
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-fork-cleanup.rb +22 -0
- data/examples/core/xx-sleeping.rb +14 -6
- data/examples/io/tunnel.rb +48 -0
- data/examples/io/xx-irb.rb +1 -1
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +13 -36
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
- data/examples/performance/xx-array.rb +11 -0
- data/examples/performance/xx-fiber-switch.rb +9 -0
- data/examples/performance/xx-snooze.rb +15 -0
- data/ext/{gyro → polyphony}/extconf.rb +2 -2
- data/ext/{gyro → polyphony}/fiber.c +18 -22
- data/ext/{gyro → polyphony}/libev.c +0 -0
- data/ext/{gyro → polyphony}/libev.h +0 -0
- data/ext/polyphony/libev_agent.c +718 -0
- data/ext/polyphony/libev_queue.c +216 -0
- data/ext/{gyro/gyro.c → polyphony/polyphony.c} +16 -46
- data/ext/{gyro/gyro.h → polyphony/polyphony.h} +25 -39
- data/ext/polyphony/polyphony_ext.c +23 -0
- data/ext/{gyro → polyphony}/socket.c +21 -18
- data/ext/polyphony/thread.c +206 -0
- data/ext/{gyro → polyphony}/tracing.c +1 -1
- data/lib/polyphony.rb +40 -44
- data/lib/polyphony/adapters/fs.rb +1 -4
- data/lib/polyphony/adapters/irb.rb +1 -1
- data/lib/polyphony/adapters/postgres.rb +6 -5
- data/lib/polyphony/adapters/process.rb +27 -23
- data/lib/polyphony/adapters/trace.rb +110 -105
- data/lib/polyphony/core/channel.rb +35 -35
- data/lib/polyphony/core/exceptions.rb +29 -29
- data/lib/polyphony/core/global_api.rb +94 -91
- data/lib/polyphony/core/resource_pool.rb +83 -83
- data/lib/polyphony/core/sync.rb +16 -16
- data/lib/polyphony/core/thread_pool.rb +49 -37
- data/lib/polyphony/core/throttler.rb +30 -23
- data/lib/polyphony/event.rb +27 -0
- data/lib/polyphony/extensions/core.rb +25 -17
- data/lib/polyphony/extensions/fiber.rb +269 -267
- data/lib/polyphony/extensions/io.rb +56 -26
- data/lib/polyphony/extensions/openssl.rb +5 -9
- data/lib/polyphony/extensions/socket.rb +29 -10
- data/lib/polyphony/extensions/thread.rb +19 -12
- data/lib/polyphony/net.rb +64 -60
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +4 -7
- data/test/helper.rb +14 -1
- data/test/stress.rb +17 -12
- data/test/test_agent.rb +124 -0
- data/test/{test_async.rb → test_event.rb} +15 -7
- data/test/test_ext.rb +25 -4
- data/test/test_fiber.rb +19 -10
- data/test/test_global_api.rb +4 -4
- data/test/test_io.rb +46 -24
- data/test/test_queue.rb +74 -0
- data/test/test_signal.rb +3 -40
- data/test/test_socket.rb +33 -0
- data/test/test_thread.rb +38 -16
- data/test/test_thread_pool.rb +2 -2
- data/test/test_throttler.rb +0 -1
- data/test/test_trace.rb +6 -5
- metadata +41 -57
- data/docs/_includes/nav.html +0 -51
- data/docs/_includes/prevnext.html +0 -17
- data/docs/_layouts/default.html +0 -106
- data/docs/api-reference.md +0 -11
- data/docs/api-reference/gyro-async.md +0 -57
- data/docs/api-reference/gyro-child.md +0 -29
- data/docs/api-reference/gyro-queue.md +0 -44
- data/docs/api-reference/gyro-timer.md +0 -51
- data/docs/api-reference/gyro.md +0 -25
- data/docs/getting-started.md +0 -10
- data/docs/main-concepts.md +0 -10
- data/docs/user-guide.md +0 -10
- data/examples/core/forever_sleep.rb +0 -19
- data/ext/gyro/async.c +0 -148
- data/ext/gyro/child.c +0 -127
- data/ext/gyro/gyro_ext.c +0 -33
- data/ext/gyro/io.c +0 -474
- data/ext/gyro/queue.c +0 -142
- data/ext/gyro/selector.c +0 -205
- data/ext/gyro/signal.c +0 -118
- data/ext/gyro/thread.c +0 -298
- data/ext/gyro/timer.c +0 -134
- data/test/test_timer.rb +0 -56
@@ -1,107 +1,107 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
3
|
+
module Polyphony
|
4
|
+
# Implements a limited resource pool
|
5
|
+
class ResourcePool
|
6
|
+
attr_reader :limit, :size
|
7
|
+
|
8
|
+
# Initializes a new resource pool
|
9
|
+
# @param opts [Hash] options
|
10
|
+
# @param &block [Proc] allocator block
|
11
|
+
def initialize(opts, &block)
|
12
|
+
@allocator = block
|
13
|
+
|
14
|
+
@stock = []
|
15
|
+
@queue = []
|
16
|
+
|
17
|
+
@limit = opts[:limit] || 4
|
18
|
+
@size = 0
|
19
|
+
end
|
14
20
|
|
15
|
-
|
16
|
-
|
21
|
+
def available
|
22
|
+
@stock.size
|
23
|
+
end
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
def acquire
|
26
|
+
Thread.current.agent.ref
|
27
|
+
resource = wait_for_resource
|
28
|
+
return unless resource
|
21
29
|
|
22
|
-
|
23
|
-
|
24
|
-
|
30
|
+
yield resource
|
31
|
+
ensure
|
32
|
+
Thread.current.agent.unref
|
33
|
+
release(resource) if resource
|
34
|
+
end
|
25
35
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
36
|
+
def wait_for_resource
|
37
|
+
fiber = Fiber.current
|
38
|
+
@queue << fiber
|
39
|
+
ready_resource = from_stock
|
40
|
+
return ready_resource if ready_resource
|
30
41
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
42
|
+
suspend
|
43
|
+
ensure
|
44
|
+
@queue.delete(fiber)
|
45
|
+
end
|
36
46
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
47
|
+
def release(resource)
|
48
|
+
if resource.__discarded__
|
49
|
+
@size -= 1
|
50
|
+
elsif resource
|
51
|
+
return_to_stock(resource)
|
52
|
+
dequeue
|
53
|
+
end
|
54
|
+
end
|
42
55
|
|
43
|
-
|
44
|
-
|
45
|
-
@queue.delete(fiber)
|
46
|
-
end
|
56
|
+
def dequeue
|
57
|
+
return if @queue.empty? || @stock.empty?
|
47
58
|
|
48
|
-
|
49
|
-
if resource.__discarded__
|
50
|
-
@size -= 1
|
51
|
-
elsif resource
|
52
|
-
return_to_stock(resource)
|
53
|
-
dequeue
|
59
|
+
@queue.shift.schedule(@stock.shift)
|
54
60
|
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def dequeue
|
58
|
-
return if @queue.empty? || @stock.empty?
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
+
def return_to_stock(resource)
|
63
|
+
@stock << resource
|
64
|
+
end
|
62
65
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
+
def from_stock
|
67
|
+
@stock.shift || (@size < @limit && allocate)
|
68
|
+
end
|
66
69
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
+
def method_missing(sym, *args, &block)
|
71
|
+
acquire { |r| r.send(sym, *args, &block) }
|
72
|
+
end
|
70
73
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
+
def respond_to_missing?(*_args)
|
75
|
+
true
|
76
|
+
end
|
74
77
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
+
# Extension to allow discarding of resources
|
79
|
+
module ResourceExtensions
|
80
|
+
def __discarded__
|
81
|
+
@__discarded__
|
82
|
+
end
|
78
83
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@__discarded__
|
84
|
+
def __discard__
|
85
|
+
@__discarded__ = true
|
86
|
+
end
|
83
87
|
end
|
84
88
|
|
85
|
-
|
86
|
-
|
89
|
+
# Allocates a resource
|
90
|
+
# @return [any] allocated resource
|
91
|
+
def allocate
|
92
|
+
@size += 1
|
93
|
+
@allocator.().tap { |r| r.extend ResourceExtensions }
|
87
94
|
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# Allocates a resource
|
91
|
-
# @return [any] allocated resource
|
92
|
-
def allocate
|
93
|
-
@size += 1
|
94
|
-
@allocator.().tap { |r| r.extend ResourceExtensions }
|
95
|
-
end
|
96
95
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
96
|
+
def <<(resource)
|
97
|
+
@size += 1
|
98
|
+
resource.extend ResourceExtensions
|
99
|
+
@stock << resource
|
100
|
+
dequeue
|
101
|
+
end
|
103
102
|
|
104
|
-
|
105
|
-
|
103
|
+
def preheat!
|
104
|
+
(@limit - @size).times { @stock << allocate }
|
105
|
+
end
|
106
106
|
end
|
107
107
|
end
|
data/lib/polyphony/core/sync.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module Polyphony
|
4
|
+
# Implements mutex lock for synchronizing access to a shared resource
|
5
|
+
class Mutex
|
6
|
+
def initialize
|
7
|
+
@waiting_fibers = Polyphony::Queue.new
|
8
|
+
end
|
4
9
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
yield
|
16
|
-
ensure
|
17
|
-
@waiting_fibers.delete(fiber)
|
18
|
-
@waiting_fibers.first&.schedule
|
19
|
-
snooze
|
10
|
+
def synchronize
|
11
|
+
fiber = Fiber.current
|
12
|
+
@waiting_fibers << fiber
|
13
|
+
suspend if @waiting_fibers.size > 1
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
@waiting_fibers.delete(fiber)
|
17
|
+
@waiting_fibers.first&.schedule
|
18
|
+
snooze
|
19
|
+
end
|
20
20
|
end
|
21
21
|
end
|
@@ -1,52 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
export_default :ThreadPool
|
4
|
-
|
5
3
|
require 'etc'
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
module Polyphony
|
6
|
+
# Implements a pool of threads
|
7
|
+
class ThreadPool
|
8
|
+
attr_reader :size
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def self.process(&block)
|
11
|
+
@default_pool ||= new
|
12
|
+
@default_pool.process(&block)
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
@task_queue = ::Queue.new
|
19
|
-
@threads = (1..@size).map { Thread.new { thread_loop } }
|
20
|
-
end
|
15
|
+
def self.reset
|
16
|
+
return unless @default_pool
|
21
17
|
|
22
|
-
|
23
|
-
|
18
|
+
@default_pool.stop
|
19
|
+
@default_pool = nil
|
20
|
+
end
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
22
|
+
def initialize(size = Etc.nprocessors)
|
23
|
+
@size = size
|
24
|
+
@task_queue = Polyphony::Queue.new
|
25
|
+
@threads = (1..@size).map { Thread.new { thread_loop } }
|
26
|
+
end
|
29
27
|
|
30
|
-
|
31
|
-
|
28
|
+
def process(&block)
|
29
|
+
setup unless @task_queue
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
watcher = Fiber.current.auto_watcher
|
32
|
+
@task_queue << [block, watcher]
|
33
|
+
watcher.await
|
34
|
+
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
end
|
36
|
+
def cast(&block)
|
37
|
+
setup unless @task_queue
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
@task_queue << [block, nil]
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def busy?
|
44
|
+
!@task_queue.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def thread_loop
|
48
|
+
loop { run_queued_task }
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_queued_task
|
52
|
+
(block, watcher) = @task_queue.pop
|
53
|
+
result = block.()
|
54
|
+
watcher&.signal(result)
|
55
|
+
rescue Exception => e
|
56
|
+
watcher ? watcher.signal(e) : raise(e)
|
57
|
+
end
|
44
58
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
rescue Exception => e
|
50
|
-
watcher ? watcher.signal(e) : raise(e)
|
59
|
+
def stop
|
60
|
+
@threads.each(&:kill)
|
61
|
+
@threads.each(&:join)
|
62
|
+
end
|
51
63
|
end
|
52
64
|
end
|
@@ -1,34 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module Polyphony
|
4
|
+
# Implements general-purpose throttling
|
5
|
+
class Throttler
|
6
|
+
def initialize(rate)
|
7
|
+
@rate = rate_from_argument(rate)
|
8
|
+
@min_dt = 1.0 / @rate
|
9
|
+
@next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
10
|
+
end
|
4
11
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
12
|
+
def call
|
13
|
+
now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
14
|
+
delta = @next_time - now
|
15
|
+
Thread.current.agent.sleep(delta) if delta > 0
|
16
|
+
yield self
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
+
loop do
|
19
|
+
@next_time += @min_dt
|
20
|
+
break if @next_time > now
|
21
|
+
end
|
22
|
+
end
|
23
|
+
alias_method :process, :call
|
18
24
|
|
19
|
-
|
20
|
-
|
21
|
-
|
25
|
+
def stop
|
26
|
+
@stop = true
|
27
|
+
end
|
22
28
|
|
23
|
-
|
29
|
+
private
|
24
30
|
|
25
|
-
|
26
|
-
|
31
|
+
def rate_from_argument(arg)
|
32
|
+
return arg if arg.is_a?(Numeric)
|
27
33
|
|
28
|
-
|
29
|
-
|
30
|
-
|
34
|
+
if arg.is_a?(Hash)
|
35
|
+
return 1.0 / arg[:interval] if arg[:interval]
|
36
|
+
return arg[:rate] if arg[:rate]
|
37
|
+
end
|
38
|
+
raise "Invalid rate argument #{arg.inspect}"
|
31
39
|
end
|
32
|
-
raise "Invalid rate argument #{arg.inspect}"
|
33
40
|
end
|
34
41
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Polyphony
|
4
|
+
# Event watcher for thread-safe synchronisation
|
5
|
+
class Event
|
6
|
+
def initialize
|
7
|
+
@i, @o = IO.pipe
|
8
|
+
end
|
9
|
+
|
10
|
+
def await
|
11
|
+
Thread.current.agent.read(@i, +'', 8192, false)
|
12
|
+
raise @value if @value.is_a?(Exception)
|
13
|
+
|
14
|
+
@value
|
15
|
+
end
|
16
|
+
|
17
|
+
def await_no_raise
|
18
|
+
Thread.current.agent.read(@i, +'', 8192, false)
|
19
|
+
@value
|
20
|
+
end
|
21
|
+
|
22
|
+
def signal(value = nil)
|
23
|
+
@value = value
|
24
|
+
Thread.current.agent.write(@o, '1')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -4,12 +4,10 @@ require 'fiber'
|
|
4
4
|
require 'timeout'
|
5
5
|
require 'open3'
|
6
6
|
|
7
|
-
|
7
|
+
require_relative '../core/exceptions'
|
8
8
|
|
9
9
|
# Exeption overrides
|
10
10
|
class ::Exception
|
11
|
-
EXIT_EXCEPTION_CLASSES = [::Interrupt, ::SystemExit].freeze
|
12
|
-
|
13
11
|
class << self
|
14
12
|
attr_accessor :__disable_sanitized_backtrace__
|
15
13
|
end
|
@@ -22,9 +20,9 @@ class ::Exception
|
|
22
20
|
orig_initialize(*args)
|
23
21
|
end
|
24
22
|
|
25
|
-
|
23
|
+
alias_method :orig_backtrace, :backtrace
|
26
24
|
def backtrace
|
27
|
-
unless @first_backtrace_call
|
25
|
+
unless @first_backtrace_call
|
28
26
|
@first_backtrace_call = true
|
29
27
|
return orig_backtrace
|
30
28
|
end
|
@@ -52,10 +50,13 @@ end
|
|
52
50
|
|
53
51
|
# Overrides for Process
|
54
52
|
module ::Process
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
53
|
+
class << self
|
54
|
+
alias_method :orig_detach, :detach
|
55
|
+
def detach(pid)
|
56
|
+
fiber = spin { Thread.current.agent.waitpid(pid) }
|
57
|
+
fiber.define_singleton_method(:pid) { pid }
|
58
|
+
fiber
|
59
|
+
end
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
@@ -67,10 +68,9 @@ module ::Kernel
|
|
67
68
|
def `(cmd)
|
68
69
|
Open3.popen3(cmd) do |i, o, e, _t|
|
69
70
|
i.close
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
o.read
|
71
|
+
err = e.read
|
72
|
+
$stderr << err if err
|
73
|
+
o.read || ''
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
@@ -91,6 +91,7 @@ module ::Kernel
|
|
91
91
|
def gets(*_args)
|
92
92
|
if !ARGV.empty? || @gets_fiber
|
93
93
|
@gets_fiber ||= Fiber.new(&ARGV_GETS_LOOP)
|
94
|
+
@gets_fiber.thread = Thread.current
|
94
95
|
result = @gets_fiber.alive? && @gets_fiber.safe_transfer(Fiber.current)
|
95
96
|
return result if result
|
96
97
|
|
@@ -104,21 +105,28 @@ module ::Kernel
|
|
104
105
|
def system(*args)
|
105
106
|
Open3.popen2(*args) do |i, o, _t|
|
106
107
|
i.close
|
107
|
-
|
108
|
-
$stdout << l
|
109
|
-
end
|
108
|
+
pipe_to_eof(o, $stdout)
|
110
109
|
end
|
111
110
|
true
|
112
111
|
rescue SystemCallError
|
113
112
|
nil
|
114
113
|
end
|
114
|
+
|
115
|
+
def pipe_to_eof(src, dest)
|
116
|
+
loop do
|
117
|
+
data = src.readpartial(8192)
|
118
|
+
dest << data
|
119
|
+
rescue EOFError
|
120
|
+
break
|
121
|
+
end
|
122
|
+
end
|
115
123
|
end
|
116
124
|
|
117
125
|
# Override Timeout to use cancel scope
|
118
126
|
module ::Timeout
|
119
127
|
def self.timeout(sec, klass = nil, message = nil, &block)
|
120
128
|
cancel_after(sec, &block)
|
121
|
-
rescue
|
129
|
+
rescue Polyphony::Cancel => e
|
122
130
|
error = klass ? klass.new(message) : ::Timeout::Error.new
|
123
131
|
error.set_backtrace(e.backtrace)
|
124
132
|
raise error
|