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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/TODO.md +12 -8
  5. data/docs/README.md +2 -2
  6. data/docs/summary.md +3 -3
  7. data/docs/technical-overview/concurrency.md +4 -6
  8. data/docs/technical-overview/design-principles.md +8 -8
  9. data/docs/technical-overview/exception-handling.md +1 -1
  10. data/examples/core/{01-spinning-up-coprocesses.rb → 01-spinning-up-fibers.rb} +1 -1
  11. data/examples/core/{02-awaiting-coprocesses.rb → 02-awaiting-fibers.rb} +3 -3
  12. data/examples/core/xx-erlang-style-genserver.rb +10 -10
  13. data/examples/core/xx-extended_fibers.rb +150 -0
  14. data/examples/core/xx-sleeping.rb +9 -0
  15. data/examples/core/xx-supervisors.rb +1 -1
  16. data/examples/interfaces/pg_pool.rb +3 -3
  17. data/examples/performance/mem-usage.rb +19 -4
  18. data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -5
  19. data/ext/gyro/gyro.c +9 -15
  20. data/lib/polyphony/core/cancel_scope.rb +0 -2
  21. data/lib/polyphony/core/exceptions.rb +2 -2
  22. data/lib/polyphony/core/global_api.rb +7 -8
  23. data/lib/polyphony/core/supervisor.rb +25 -31
  24. data/lib/polyphony/extensions/core.rb +4 -78
  25. data/lib/polyphony/extensions/fiber.rb +166 -0
  26. data/lib/polyphony/extensions/io.rb +2 -1
  27. data/lib/polyphony/version.rb +1 -1
  28. data/lib/polyphony.rb +6 -8
  29. data/test/test_async.rb +2 -2
  30. data/test/test_cancel_scope.rb +6 -6
  31. data/test/test_fiber.rb +382 -0
  32. data/test/test_global_api.rb +49 -50
  33. data/test/test_gyro.rb +1 -1
  34. data/test/test_io.rb +30 -29
  35. data/test/test_kernel.rb +2 -2
  36. data/test/test_signal.rb +1 -1
  37. data/test/test_supervisor.rb +27 -27
  38. data/test/test_timer.rb +2 -2
  39. metadata +7 -7
  40. data/examples/core/04-no-auto-run.rb +0 -16
  41. data/lib/polyphony/core/coprocess.rb +0 -168
  42. 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
- Coprocess.new(&block).run
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.coprocess.receive
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
- Coprocess = import('./coprocess')
6
- Exceptions = import('./exceptions')
5
+ import '../extensions/fiber'
6
+ Exceptions = import './exceptions'
7
7
 
8
- # Implements a supervision mechanism for controlling multiple coprocesses
8
+ # Implements a supervision mechanism for controlling multiple fibers
9
9
  class Supervisor
10
10
  def initialize
11
- @coprocesses = []
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
- @coprocesses.map(&:result)
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
- @select_coproc = nil
30
+ @select_fiber = nil
31
31
  @supervisor_fiber = Fiber.current
32
32
  block&.(self)
33
33
  suspend
34
- [@select_coproc.result, @select_coproc]
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(coproc = nil, &block)
56
- coproc = Coprocess.new(&(coproc || block)) unless coproc.is_a?(Coprocess)
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(coproc)
65
- @coprocesses << coproc
66
- @pending[coproc] = true
67
- coproc.when_done { task_completed(coproc) }
68
- coproc.run unless coproc.alive?
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(coprocess)
93
- return unless @pending[coprocess]
86
+ def task_completed(fiber)
87
+ return unless @pending[fiber]
94
88
 
95
- @pending.delete(coprocess)
96
- return unless @pending.empty? || (@mode == :select && !@select_coproc)
89
+ @pending.delete(fiber)
90
+ return unless @pending.empty? || (@mode == :select && !@select_fiber)
97
91
 
98
- @select_coproc = coprocess if @mode == :select
92
+ @select_fiber = fiber if @mode == :select
99
93
  @supervisor_fiber&.schedule
100
94
  end
101
95
  end
102
96
 
103
- # Extension for Coprocess class
104
- class Coprocess
97
+ # Supervision extensions for Fiber class
98
+ class ::Fiber
105
99
  class << self
106
- def await(*coprocs)
100
+ def await(*fibers)
107
101
  supervisor = Supervisor.new
108
- coprocs.each { |cp| supervisor << cp }
102
+ fibers.each { |f| supervisor << f }
109
103
  supervisor.await
110
104
  end
111
105
  alias_method :join, :await
112
106
 
113
- def select(*coprocs)
107
+ def select(*fibers)
114
108
  supervisor = Supervisor.new
115
- coprocs.each { |cp| supervisor << cp }
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
- Coprocess = import('../core/coprocess')
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 do
124
- Gyro::Child.new(pid).await
125
- end.tap { |coproc| coproc.define_singleton_method(:pid) { pid } }
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(name, sep = $/, limit = nil, getline_args = EMPTY_HASH, &block)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.24'
4
+ VERSION = '0.25'
5
5
  end
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 = exceptions::Cancel
20
- MoveOn = exceptions::MoveOn
20
+ Cancel = exceptions::Cancel
21
+ MoveOn = exceptions::MoveOn
21
22
 
22
- Coprocess = import './polyphony/core/coprocess'
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
- Coprocess.map.clear
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.set_root_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(1, count)
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(1, count)
38
+ assert_equal 1, count
39
39
  end
40
40
  end
@@ -14,10 +14,10 @@ class CancelScopeTest < MiniTest::Test
14
14
  assert_equal [1], buffer
15
15
  end
16
16
 
17
- def test_that_cancel_scope_can_cancel_multiple_coprocesses
17
+ def test_that_cancel_scope_can_cancel_multiple_fibers
18
18
  buffer = []
19
19
  scope = Polyphony::CancelScope.new
20
- coprocs = (1..3).map { |i|
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 test_that_cancel_scope_can_cancel_multiple_coprocs_with_timeout
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
- coprocs = (1..3).map { |i|
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
- Polyphony::Coprocess.await(*coprocs)
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.010
80
+ sleep 0.008
81
81
  }
82
82
 
83
83
  assert !scope.cancelled?