polyphony 0.24 → 0.25
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 +6 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +12 -8
- data/docs/README.md +2 -2
- data/docs/summary.md +3 -3
- data/docs/technical-overview/concurrency.md +4 -6
- data/docs/technical-overview/design-principles.md +8 -8
- data/docs/technical-overview/exception-handling.md +1 -1
- data/examples/core/{01-spinning-up-coprocesses.rb → 01-spinning-up-fibers.rb} +1 -1
- data/examples/core/{02-awaiting-coprocesses.rb → 02-awaiting-fibers.rb} +3 -3
- data/examples/core/xx-erlang-style-genserver.rb +10 -10
- data/examples/core/xx-extended_fibers.rb +150 -0
- data/examples/core/xx-sleeping.rb +9 -0
- data/examples/core/xx-supervisors.rb +1 -1
- data/examples/interfaces/pg_pool.rb +3 -3
- data/examples/performance/mem-usage.rb +19 -4
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -5
- data/ext/gyro/gyro.c +9 -15
- data/lib/polyphony/core/cancel_scope.rb +0 -2
- data/lib/polyphony/core/exceptions.rb +2 -2
- data/lib/polyphony/core/global_api.rb +7 -8
- data/lib/polyphony/core/supervisor.rb +25 -31
- data/lib/polyphony/extensions/core.rb +4 -78
- data/lib/polyphony/extensions/fiber.rb +166 -0
- data/lib/polyphony/extensions/io.rb +2 -1
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +6 -8
- data/test/test_async.rb +2 -2
- data/test/test_cancel_scope.rb +6 -6
- data/test/test_fiber.rb +382 -0
- data/test/test_global_api.rb +49 -50
- data/test/test_gyro.rb +1 -1
- data/test/test_io.rb +30 -29
- data/test/test_kernel.rb +2 -2
- data/test/test_signal.rb +1 -1
- data/test/test_supervisor.rb +27 -27
- data/test/test_timer.rb +2 -2
- metadata +7 -7
- data/examples/core/04-no-auto-run.rb +0 -16
- data/lib/polyphony/core/coprocess.rb +0 -168
- data/test/test_coprocess.rb +0 -440
@@ -3,8 +3,8 @@
|
|
3
3
|
export_default :API
|
4
4
|
|
5
5
|
import '../extensions/core'
|
6
|
+
import '../extensions/fiber'
|
6
7
|
|
7
|
-
Coprocess = import '../core/coprocess'
|
8
8
|
Exceptions = import '../core/exceptions'
|
9
9
|
Supervisor = import '../core/supervisor'
|
10
10
|
Throttler = import '../core/throttler'
|
@@ -29,13 +29,10 @@ module API
|
|
29
29
|
canceller.stop
|
30
30
|
end
|
31
31
|
|
32
|
-
def defer(&block)
|
33
|
-
::Fiber.new(&block).schedule
|
34
|
-
end
|
35
|
-
|
36
32
|
def spin(&block)
|
37
|
-
|
33
|
+
Fiber.spin(caller, &block)
|
38
34
|
end
|
35
|
+
alias_method :defer, :spin
|
39
36
|
|
40
37
|
def spin_loop(&block)
|
41
38
|
spin { loop(&block) }
|
@@ -63,10 +60,12 @@ module API
|
|
63
60
|
end
|
64
61
|
|
65
62
|
def receive
|
66
|
-
Fiber.current.
|
63
|
+
Fiber.current.receive
|
67
64
|
end
|
68
65
|
|
69
|
-
def sleep(duration)
|
66
|
+
def sleep(duration = nil)
|
67
|
+
return suspend unless duration
|
68
|
+
|
70
69
|
timer = Gyro::Timer.new(duration, 0)
|
71
70
|
timer.await
|
72
71
|
end
|
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
export_default :Supervisor
|
4
4
|
|
5
|
-
|
6
|
-
Exceptions
|
5
|
+
import '../extensions/fiber'
|
6
|
+
Exceptions = import './exceptions'
|
7
7
|
|
8
|
-
# Implements a supervision mechanism for controlling multiple
|
8
|
+
# Implements a supervision mechanism for controlling multiple fibers
|
9
9
|
class Supervisor
|
10
10
|
def initialize
|
11
|
-
@
|
11
|
+
@fibers = []
|
12
12
|
@pending = {}
|
13
13
|
end
|
14
14
|
|
@@ -17,7 +17,7 @@ class Supervisor
|
|
17
17
|
@supervisor_fiber = Fiber.current
|
18
18
|
block&.(self)
|
19
19
|
suspend
|
20
|
-
@
|
20
|
+
@fibers.map(&:result)
|
21
21
|
rescue Exceptions::MoveOn => e
|
22
22
|
e.value
|
23
23
|
ensure
|
@@ -27,11 +27,11 @@ class Supervisor
|
|
27
27
|
|
28
28
|
def select(&block)
|
29
29
|
@mode = :select
|
30
|
-
@
|
30
|
+
@select_fiber = nil
|
31
31
|
@supervisor_fiber = Fiber.current
|
32
32
|
block&.(self)
|
33
33
|
suspend
|
34
|
-
[@
|
34
|
+
[@select_fiber.result, @select_fiber]
|
35
35
|
rescue Exceptions::MoveOn => e
|
36
36
|
e.value
|
37
37
|
ensure
|
@@ -52,21 +52,15 @@ class Supervisor
|
|
52
52
|
@supervisor_fiber = nil
|
53
53
|
end
|
54
54
|
|
55
|
-
def spin(
|
56
|
-
|
57
|
-
@coprocesses << coproc
|
58
|
-
@pending[coproc] = true
|
59
|
-
coproc.when_done { task_completed(coproc) }
|
60
|
-
coproc.run unless coproc.alive?
|
61
|
-
coproc
|
55
|
+
def spin(orig_caller = caller, &block)
|
56
|
+
add Fiber.spin(orig_caller, &block)
|
62
57
|
end
|
63
58
|
|
64
|
-
def add(
|
65
|
-
@
|
66
|
-
@pending[
|
67
|
-
|
68
|
-
|
69
|
-
coproc
|
59
|
+
def add(fiber)
|
60
|
+
@fibers << fiber
|
61
|
+
@pending[fiber] = true
|
62
|
+
fiber.when_done { task_completed(fiber) }
|
63
|
+
fiber
|
70
64
|
end
|
71
65
|
alias_method :<<, :add
|
72
66
|
|
@@ -89,30 +83,30 @@ class Supervisor
|
|
89
83
|
end
|
90
84
|
end
|
91
85
|
|
92
|
-
def task_completed(
|
93
|
-
return unless @pending[
|
86
|
+
def task_completed(fiber)
|
87
|
+
return unless @pending[fiber]
|
94
88
|
|
95
|
-
@pending.delete(
|
96
|
-
return unless @pending.empty? || (@mode == :select && !@
|
89
|
+
@pending.delete(fiber)
|
90
|
+
return unless @pending.empty? || (@mode == :select && !@select_fiber)
|
97
91
|
|
98
|
-
@
|
92
|
+
@select_fiber = fiber if @mode == :select
|
99
93
|
@supervisor_fiber&.schedule
|
100
94
|
end
|
101
95
|
end
|
102
96
|
|
103
|
-
#
|
104
|
-
class
|
97
|
+
# Supervision extensions for Fiber class
|
98
|
+
class ::Fiber
|
105
99
|
class << self
|
106
|
-
def await(*
|
100
|
+
def await(*fibers)
|
107
101
|
supervisor = Supervisor.new
|
108
|
-
|
102
|
+
fibers.each { |f| supervisor << f }
|
109
103
|
supervisor.await
|
110
104
|
end
|
111
105
|
alias_method :join, :await
|
112
106
|
|
113
|
-
def select(*
|
107
|
+
def select(*fibers)
|
114
108
|
supervisor = Supervisor.new
|
115
|
-
|
109
|
+
fibers.each { |f| supervisor << f }
|
116
110
|
supervisor.select
|
117
111
|
end
|
118
112
|
end
|
@@ -4,81 +4,7 @@ require 'fiber'
|
|
4
4
|
require 'timeout'
|
5
5
|
require 'open3'
|
6
6
|
|
7
|
-
|
8
|
-
Exceptions = import('../core/exceptions')
|
9
|
-
Supervisor = import('../core/supervisor')
|
10
|
-
Throttler = import('../core/throttler')
|
11
|
-
|
12
|
-
# Fiber extensions
|
13
|
-
class ::Fiber
|
14
|
-
attr_accessor :__calling_fiber__
|
15
|
-
attr_accessor :__caller__
|
16
|
-
attr_accessor :__location__
|
17
|
-
attr_writer :cancelled
|
18
|
-
attr_accessor :coprocess
|
19
|
-
|
20
|
-
def location
|
21
|
-
__location__ || (__caller__ && __caller__[0])
|
22
|
-
end
|
23
|
-
|
24
|
-
def inspect
|
25
|
-
"#<Fiber:#{object_id} #{location} (#{state})"
|
26
|
-
end
|
27
|
-
alias_method :to_s, :inspect
|
28
|
-
|
29
|
-
def set_calling_context(location, calling_fiber, fiber_caller)
|
30
|
-
@__location__ = location
|
31
|
-
@__calling_fiber__ = calling_fiber
|
32
|
-
@__caller__ = fiber_caller
|
33
|
-
self
|
34
|
-
end
|
35
|
-
|
36
|
-
class << self
|
37
|
-
alias_method :orig_new, :new
|
38
|
-
def new(location = nil, &block)
|
39
|
-
fiber = create_fiber_with_block(&block)
|
40
|
-
fiber.set_calling_context(location, Fiber.current, caller)
|
41
|
-
fiber
|
42
|
-
end
|
43
|
-
|
44
|
-
def create_fiber_with_block(&block)
|
45
|
-
fiber = orig_new do |v|
|
46
|
-
block.call(v)
|
47
|
-
rescue Exception => e
|
48
|
-
__calling_fiber__.transfer e if __calling_fiber__.alive?
|
49
|
-
ensure
|
50
|
-
fiber.mark_as_done!
|
51
|
-
Gyro.run
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def root
|
56
|
-
@root_fiber
|
57
|
-
end
|
58
|
-
|
59
|
-
def set_root_fiber
|
60
|
-
@root_fiber = current
|
61
|
-
|
62
|
-
# Associate a (pseudo-)coprocess with the root fiber
|
63
|
-
Coprocess.map[current] = current.coprocess = Coprocess.new(current)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def caller
|
68
|
-
@__caller__ ||= []
|
69
|
-
if @__calling_fiber__
|
70
|
-
@__caller__ + @__calling_fiber__.caller
|
71
|
-
else
|
72
|
-
@__caller__
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def cancelled?
|
77
|
-
@cancelled
|
78
|
-
end
|
79
|
-
|
80
|
-
set_root_fiber
|
81
|
-
end
|
7
|
+
Exceptions = import('../core/exceptions')
|
82
8
|
|
83
9
|
# Exeption overrides
|
84
10
|
class ::Exception
|
@@ -120,9 +46,9 @@ end
|
|
120
46
|
# Overrides for Process
|
121
47
|
module ::Process
|
122
48
|
def self.detach(pid)
|
123
|
-
spin
|
124
|
-
|
125
|
-
|
49
|
+
fiber = spin { Gyro::Child.new(pid).await }
|
50
|
+
fiber.define_singleton_method(:pid) { pid }
|
51
|
+
fiber
|
126
52
|
end
|
127
53
|
end
|
128
54
|
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
Exceptions = import '../core/exceptions'
|
6
|
+
|
7
|
+
# Fiber control API
|
8
|
+
module FiberControl
|
9
|
+
def await
|
10
|
+
if @running == false
|
11
|
+
return @result.is_a?(Exception) ? (raise @result) : @result
|
12
|
+
end
|
13
|
+
|
14
|
+
@waiting_fiber = Fiber.current
|
15
|
+
suspend
|
16
|
+
ensure
|
17
|
+
@waiting_fiber = nil
|
18
|
+
end
|
19
|
+
alias_method :join, :await
|
20
|
+
|
21
|
+
def interrupt(value = nil)
|
22
|
+
return if @running == false
|
23
|
+
|
24
|
+
schedule Exceptions::MoveOn.new(nil, value)
|
25
|
+
snooze
|
26
|
+
end
|
27
|
+
alias_method :stop, :interrupt
|
28
|
+
|
29
|
+
def cancel!
|
30
|
+
return if @running == false
|
31
|
+
|
32
|
+
schedule Exceptions::Cancel.new
|
33
|
+
snooze
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Messaging functionality
|
38
|
+
module FiberMessaging
|
39
|
+
def <<(value)
|
40
|
+
if @receive_waiting && @running
|
41
|
+
schedule value
|
42
|
+
else
|
43
|
+
@queued_messages ||= []
|
44
|
+
@queued_messages << value
|
45
|
+
end
|
46
|
+
snooze
|
47
|
+
end
|
48
|
+
|
49
|
+
def receive
|
50
|
+
if !@queued_messages || @queued_messages&.empty?
|
51
|
+
wait_for_message
|
52
|
+
else
|
53
|
+
value = @queued_messages.shift
|
54
|
+
snooze
|
55
|
+
value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def wait_for_message
|
60
|
+
Gyro.ref
|
61
|
+
@receive_waiting = true
|
62
|
+
suspend
|
63
|
+
ensure
|
64
|
+
Gyro.unref
|
65
|
+
@receive_waiting = nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Fiber extensions
|
70
|
+
class ::Fiber
|
71
|
+
include FiberControl
|
72
|
+
include FiberMessaging
|
73
|
+
|
74
|
+
# map of currently running fibers
|
75
|
+
def self.root
|
76
|
+
@root_fiber
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.reset!
|
80
|
+
@root_fiber = current
|
81
|
+
@running_fibers_map = { @root_fiber => true }
|
82
|
+
end
|
83
|
+
|
84
|
+
reset!
|
85
|
+
|
86
|
+
def self.map
|
87
|
+
@running_fibers_map
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.list
|
91
|
+
@running_fibers_map.keys
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.count
|
95
|
+
@running_fibers_map.size
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.spin(orig_caller = caller, &block)
|
99
|
+
f = new { |v| f.run(v) }
|
100
|
+
f.setup(block, orig_caller)
|
101
|
+
f
|
102
|
+
end
|
103
|
+
|
104
|
+
def setup(block, caller)
|
105
|
+
@calling_fiber = Fiber.current
|
106
|
+
@caller = caller
|
107
|
+
@block = block
|
108
|
+
schedule
|
109
|
+
end
|
110
|
+
|
111
|
+
def run(first_value)
|
112
|
+
raise first_value if first_value.is_a?(Exception)
|
113
|
+
|
114
|
+
@running = true
|
115
|
+
self.class.map[self] = true
|
116
|
+
result = @block.(first_value)
|
117
|
+
finish_execution(result)
|
118
|
+
rescue Exceptions::MoveOn => e
|
119
|
+
finish_execution(e.value)
|
120
|
+
rescue Exception => e
|
121
|
+
finish_execution(e, true)
|
122
|
+
end
|
123
|
+
|
124
|
+
def finish_execution(result, uncaught_exception = false)
|
125
|
+
@result = result
|
126
|
+
@running = false
|
127
|
+
self.class.map.delete(self)
|
128
|
+
@when_done&.(result)
|
129
|
+
@waiting_fiber&.schedule(result)
|
130
|
+
|
131
|
+
return unless uncaught_exception && !@waiting_fiber
|
132
|
+
|
133
|
+
parent_fiber = @calling_fiber.running? ? @calling_fiber : Fiber.root
|
134
|
+
parent_fiber.schedule(result)
|
135
|
+
ensure
|
136
|
+
Gyro.run
|
137
|
+
end
|
138
|
+
|
139
|
+
attr_reader :result
|
140
|
+
|
141
|
+
def running?
|
142
|
+
@running
|
143
|
+
end
|
144
|
+
|
145
|
+
def when_done(&block)
|
146
|
+
@when_done = block
|
147
|
+
end
|
148
|
+
|
149
|
+
def inspect
|
150
|
+
"#<Fiber:#{object_id} #{location} (#{state})>"
|
151
|
+
end
|
152
|
+
alias_method :to_s, :inspect
|
153
|
+
|
154
|
+
def location
|
155
|
+
@caller ? @caller[0] : '(root)'
|
156
|
+
end
|
157
|
+
|
158
|
+
def caller
|
159
|
+
@caller ||= []
|
160
|
+
if @calling_fiber
|
161
|
+
@caller + @calling_fiber.caller
|
162
|
+
else
|
163
|
+
@caller
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -24,7 +24,8 @@ class ::IO
|
|
24
24
|
EMPTY_HASH = {}.freeze
|
25
25
|
|
26
26
|
alias_method :orig_foreach, :foreach
|
27
|
-
def foreach(
|
27
|
+
def foreach(_name, _sep = $/, _limit = nil, _getline_args = EMPTY_HASH,
|
28
|
+
&_block)
|
28
29
|
# IO.orig_read(name).each_line(&block)
|
29
30
|
raise NotImplementedError
|
30
31
|
|
data/lib/polyphony/version.rb
CHANGED
data/lib/polyphony.rb
CHANGED
@@ -8,6 +8,7 @@ require 'fiber'
|
|
8
8
|
require_relative './gyro_ext'
|
9
9
|
|
10
10
|
import './polyphony/extensions/core'
|
11
|
+
import './polyphony/extensions/fiber'
|
11
12
|
import './polyphony/extensions/io'
|
12
13
|
|
13
14
|
# Main Polyphony API
|
@@ -16,11 +17,10 @@ module Polyphony
|
|
16
17
|
::Object.include GlobalAPI
|
17
18
|
|
18
19
|
exceptions = import './polyphony/core/exceptions'
|
19
|
-
Cancel
|
20
|
-
MoveOn
|
20
|
+
Cancel = exceptions::Cancel
|
21
|
+
MoveOn = exceptions::MoveOn
|
21
22
|
|
22
|
-
|
23
|
-
Net = import './polyphony/net'
|
23
|
+
Net = import './polyphony/net'
|
24
24
|
|
25
25
|
auto_import(
|
26
26
|
CancelScope: './polyphony/core/cancel_scope',
|
@@ -67,16 +67,14 @@ module Polyphony
|
|
67
67
|
def reset!
|
68
68
|
# Fiber.root.scheduled_value = nil
|
69
69
|
Gyro.reset!
|
70
|
-
|
71
|
-
Fiber.set_root_fiber
|
70
|
+
Fiber.reset!
|
72
71
|
end
|
73
72
|
|
74
73
|
private
|
75
74
|
|
76
75
|
def setup_forked_process
|
77
|
-
Coprocess.map.delete Fiber.root
|
78
76
|
Gyro.post_fork
|
79
|
-
Fiber.
|
77
|
+
Fiber.reset!
|
80
78
|
end
|
81
79
|
end
|
82
80
|
end
|
data/test/test_async.rb
CHANGED
@@ -16,7 +16,7 @@ class AsyncTest < MiniTest::Test
|
|
16
16
|
a.signal!
|
17
17
|
end
|
18
18
|
suspend
|
19
|
-
assert_equal
|
19
|
+
assert_equal 1, count
|
20
20
|
end
|
21
21
|
|
22
22
|
def test_that_async_watcher_coalesces_signals
|
@@ -35,6 +35,6 @@ class AsyncTest < MiniTest::Test
|
|
35
35
|
3.times { a.signal! }
|
36
36
|
end
|
37
37
|
coproc.await
|
38
|
-
assert_equal
|
38
|
+
assert_equal 1, count
|
39
39
|
end
|
40
40
|
end
|
data/test/test_cancel_scope.rb
CHANGED
@@ -14,10 +14,10 @@ class CancelScopeTest < MiniTest::Test
|
|
14
14
|
assert_equal [1], buffer
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
17
|
+
def test_that_cancel_scope_can_cancel_multiple_fibers
|
18
18
|
buffer = []
|
19
19
|
scope = Polyphony::CancelScope.new
|
20
|
-
|
20
|
+
fibers = (1..3).map { |i|
|
21
21
|
spin {
|
22
22
|
scope.call do
|
23
23
|
buffer << i
|
@@ -52,11 +52,11 @@ class CancelScopeTest < MiniTest::Test
|
|
52
52
|
assert_nil scope.instance_variable_get(:@timeout_waiter)
|
53
53
|
end
|
54
54
|
|
55
|
-
def
|
55
|
+
def test_that_cancel_scope_can_cancel_multiple_fibers_with_timeout
|
56
56
|
buffer = []
|
57
57
|
t0 = Time.now
|
58
58
|
scope = Polyphony::CancelScope.new(timeout: 0.02)
|
59
|
-
|
59
|
+
fibers = (1..3).map { |i|
|
60
60
|
spin {
|
61
61
|
scope.call do
|
62
62
|
buffer << i
|
@@ -65,7 +65,7 @@ class CancelScopeTest < MiniTest::Test
|
|
65
65
|
end
|
66
66
|
}
|
67
67
|
}
|
68
|
-
|
68
|
+
Fiber.await(*fibers)
|
69
69
|
assert Time.now - t0 < 0.05
|
70
70
|
assert_equal [1, 2, 3], buffer
|
71
71
|
end
|
@@ -77,7 +77,7 @@ class CancelScopeTest < MiniTest::Test
|
|
77
77
|
scope.call {
|
78
78
|
sleep 0.005
|
79
79
|
scope.reset_timeout
|
80
|
-
sleep 0.
|
80
|
+
sleep 0.008
|
81
81
|
}
|
82
82
|
|
83
83
|
assert !scope.cancelled?
|