polyphony 0.79 → 0.81.1

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 +19 -0
  3. data/Gemfile.lock +2 -1
  4. data/examples/core/raw_buffer_test.rb +8 -0
  5. data/examples/core/zlib_stream.rb +16 -0
  6. data/ext/polyphony/backend_common.c +2 -1
  7. data/ext/polyphony/backend_common.h +7 -2
  8. data/ext/polyphony/backend_io_uring.c +69 -30
  9. data/ext/polyphony/polyphony.c +8 -0
  10. data/ext/polyphony/polyphony.h +11 -0
  11. data/lib/polyphony/adapters/fs.rb +4 -0
  12. data/lib/polyphony/adapters/process.rb +14 -1
  13. data/lib/polyphony/adapters/redis.rb +28 -0
  14. data/lib/polyphony/adapters/sequel.rb +19 -1
  15. data/lib/polyphony/core/debug.rb +129 -72
  16. data/lib/polyphony/core/exceptions.rb +21 -6
  17. data/lib/polyphony/core/global_api.rb +228 -73
  18. data/lib/polyphony/core/resource_pool.rb +65 -20
  19. data/lib/polyphony/core/sync.rb +57 -12
  20. data/lib/polyphony/core/thread_pool.rb +42 -5
  21. data/lib/polyphony/core/throttler.rb +21 -5
  22. data/lib/polyphony/core/timer.rb +125 -1
  23. data/lib/polyphony/extensions/exception.rb +36 -6
  24. data/lib/polyphony/extensions/fiber.rb +238 -57
  25. data/lib/polyphony/extensions/io.rb +4 -2
  26. data/lib/polyphony/extensions/kernel.rb +9 -4
  27. data/lib/polyphony/extensions/object.rb +8 -0
  28. data/lib/polyphony/extensions/openssl.rb +3 -1
  29. data/lib/polyphony/extensions/socket.rb +458 -39
  30. data/lib/polyphony/extensions/thread.rb +108 -43
  31. data/lib/polyphony/extensions/timeout.rb +12 -1
  32. data/lib/polyphony/extensions.rb +1 -0
  33. data/lib/polyphony/net.rb +66 -7
  34. data/lib/polyphony/version.rb +1 -1
  35. data/lib/polyphony.rb +0 -2
  36. data/test/test_backend.rb +6 -2
  37. data/test/test_global_api.rb +0 -23
  38. data/test/test_resource_pool.rb +1 -1
  39. data/test/test_throttler.rb +0 -6
  40. data/test/test_trace.rb +87 -0
  41. metadata +10 -8
  42. data/lib/polyphony/core/channel.rb +0 -15
@@ -1,8 +1,15 @@
1
+ # Kernel extensions
1
2
  module ::Kernel
3
+ # Prints a trace message to `STDOUT`, bypassing the Polyphony backend.
4
+ #
5
+ # @return [void]
2
6
  def trace(*args)
3
7
  STDOUT.orig_write(format_trace(args))
4
8
  end
5
9
 
10
+ # Formats a trace message.
11
+ #
12
+ # @return [String] trace message
6
13
  def format_trace(args)
7
14
  if args.size > 1 && args.first.is_a?(String)
8
15
  format("%s: %p\n", args.shift, args.size == 1 ? args.first : args)
@@ -15,14 +22,29 @@ module ::Kernel
15
22
  end
16
23
 
17
24
  module Polyphony
25
+
26
+ # Trace provides tools for tracing the activity of the current thread's
27
+ # backend.
18
28
  module Trace
19
29
  class << self
30
+
31
+ # Starts tracing, emitting events converted to hashes to the given block.
32
+ # If an IO instance is given, events are dumped to it instead.
33
+ #
34
+ # @param io [IO, nil] IO instance
35
+ # @param &block [Proc] event handler block
36
+ # @return [void]
20
37
  def start_event_firehose(io = nil, &block)
21
38
  Thread.backend.trace_proc = firehose_proc(io, block)
22
39
  end
23
40
 
24
41
  private
25
42
 
43
+ # Returns a firehose proc for the given io and block.
44
+ #
45
+ # @param io [IO, nil] IO instance
46
+ # @param block [Proc] event handler block
47
+ # @return [Proc] firehose proc
26
48
  def firehose_proc(io, block)
27
49
  if io
28
50
  ->(*e) { io.orig_write("#{trace_event_info(e).inspect}\n") }
@@ -33,53 +55,50 @@ module Polyphony
33
55
  end
34
56
  end
35
57
 
58
+ # Converts an event (expressed as an array) to a hash.
59
+ #
60
+ # @param e [Array] event as emitted by the backend
61
+ # @return [Hash] event hash
36
62
  def trace_event_info(e)
37
63
  {
38
- stamp: format_current_time,
64
+ stamp: Time.now,
39
65
  event: e[0]
40
66
  }.merge(
41
67
  send(:"event_props_#{e[0]}", e)
42
68
  )
43
69
  end
44
-
45
- def format_trace_event_message(e)
46
- props = send(:"event_props_#{e[0]}", e).merge(
47
- timestamp: format_current_time,
48
- event: e[0]
49
- )
50
- # templ = send(:"event_format_#{e[0]}", e)
51
-
52
- # msg = format("%<timestamp>s #{templ}\n", **props)
53
- end
54
-
55
- def format_current_time
56
- Time.now.strftime('%Y-%m-%d %H:%M:%S')
57
- end
58
70
 
59
- def generic_event_format
60
- '%<event>-12.12s'
61
- end
62
-
63
- def fiber_event_format
64
- "#{generic_event_format} %<fiber>-44.44s"
71
+ # Returns an event hash for a `:block` event.
72
+ #
73
+ # @param e [Array] event array
74
+ # @return [Hash] event hash
75
+ def event_props_block(e)
76
+ {
77
+ fiber: e[1],
78
+ caller: e[2]
79
+ }
65
80
  end
66
81
 
82
+ # Returns an event hash for a `:enter_poll` event.
83
+ #
84
+ # @param e [Array] event array
85
+ # @return [Hash] event hash
67
86
  def event_props_enter_poll(e)
68
87
  {}
69
88
  end
70
89
 
71
- def event_format_enter_poll(e)
72
- generic_event_format
73
- end
74
-
90
+ # Returns an event hash for a `:leave_poll` event.
91
+ #
92
+ # @param e [Array] event array
93
+ # @return [Hash] event hash
75
94
  def event_props_leave_poll(e)
76
95
  {}
77
96
  end
78
97
 
79
- def event_format_leave_poll(e)
80
- generic_event_format
81
- end
82
-
98
+ # Returns an event hash for a `:schedule` event.
99
+ #
100
+ # @param e [Array] event array
101
+ # @return [Hash] event hash
83
102
  def event_props_schedule(e)
84
103
  {
85
104
  fiber: e[1],
@@ -89,22 +108,22 @@ module Polyphony
89
108
  }
90
109
  end
91
110
 
92
- def event_format_schedule(e)
93
- "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s <= %<origin_fiber>s"
94
- end
95
-
96
- def event_props_unblock(e)
111
+ # Returns an event hash for a `:spin` event.
112
+ #
113
+ # @param e [Array] event array
114
+ # @return [Hash] event hash
115
+ def event_props_spin(e)
97
116
  {
98
117
  fiber: e[1],
99
- value: e[2],
100
- caller: e[3],
118
+ caller: e[2],
119
+ source_fiber: Fiber.current
101
120
  }
102
121
  end
103
122
 
104
- def event_format_unblock(e)
105
- "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s"
106
- end
107
-
123
+ # Returns an event hash for a `:terminate` event.
124
+ #
125
+ # @param e [Array] event array
126
+ # @return [Hash] event hash
108
127
  def event_props_terminate(e)
109
128
  {
110
129
  fiber: e[1],
@@ -112,48 +131,86 @@ module Polyphony
112
131
  }
113
132
  end
114
133
 
115
- def event_format_terminate(e)
116
- "#{fiber_event_format} %<value>-24.24p"
117
- end
118
-
119
- def event_props_block(e)
134
+ # Returns an event hash for a `:unblock` event.
135
+ #
136
+ # @param e [Array] event array
137
+ # @return [Hash] event hash
138
+ def event_props_unblock(e)
120
139
  {
121
140
  fiber: e[1],
122
- caller: e[2]
141
+ value: e[2],
142
+ caller: e[3],
123
143
  }
124
144
  end
125
145
 
126
- def event_format_block(e)
127
- "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s"
128
- end
146
+ # TODO: work on text formatting of events
147
+ # def format_trace_event_message(e)
148
+ # props = send(:"event_props_#{e[0]}", e).merge(
149
+ # timestamp: format_current_time,
150
+ # event: e[0]
151
+ # )
152
+ # templ = send(:"event_format_#{e[0]}", e)
153
+ # msg = format("%<timestamp>s #{templ}\n", **props)
154
+ # end
129
155
 
130
- def event_props_spin(e)
131
- {
132
- fiber: e[1],
133
- caller: e[2],
134
- source_fiber: Fiber.current
135
- }
136
- end
156
+ # def format_current_time
157
+ # Time.now.strftime('%Y-%m-%d %H:%M:%S')
158
+ # end
137
159
 
138
- def event_format_spin(e)
139
- "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s <= %<origin_fiber>s"
140
- end
160
+ # def generic_event_format
161
+ # '%<event>-12.12s'
162
+ # end
141
163
 
142
- def fibe_repr(fiber)
143
- format("%-6x %-20.20s %-10.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
144
- end
164
+ # def fiber_event_format
165
+ # "#{generic_event_format} %<fiber>-44.44s"
166
+ # end
145
167
 
146
- def fiber_compact_repr(fiber)
147
- if fiber.tag
148
- format("%-6x %-.20s %-.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
149
- else
150
- format("%-6x %-.10s", fiber.object_id, "(#{fiber.state})")
151
- end
152
- end
168
+ # def event_format_enter_poll(e)
169
+ # generic_event_format
170
+ # end
153
171
 
154
- def caller_repr(c)
155
- c.map { |i| i.gsub('/home/sharon/repo/polyphony/lib/polyphony', '') }.join(' ')
156
- end
172
+ # def event_format_leave_poll(e)
173
+ # generic_event_format
174
+ # end
175
+
176
+
177
+ # def event_format_schedule(e)
178
+ # "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s <= %<origin_fiber>s"
179
+ # end
180
+
181
+
182
+ # def event_format_unblock(e)
183
+ # "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s"
184
+ # end
185
+
186
+ # def event_format_terminate(e)
187
+ # "#{fiber_event_format} %<value>-24.24p"
188
+ # end
189
+
190
+ # def event_format_block(e)
191
+ # "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s"
192
+ # end
193
+
194
+
195
+ # def event_format_spin(e)
196
+ # "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s <= %<origin_fiber>s"
197
+ # end
198
+
199
+ # def fibe_repr(fiber)
200
+ # format("%-6x %-20.20s %-10.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
201
+ # end
202
+
203
+ # def fiber_compact_repr(fiber)
204
+ # if fiber.tag
205
+ # format("%-6x %-.20s %-.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
206
+ # else
207
+ # format("%-6x %-.10s", fiber.object_id, "(#{fiber.state})")
208
+ # end
209
+ # end
210
+
211
+ # def caller_repr(c)
212
+ # c.map { |i| i.gsub('/home/sharon/repo/polyphony/lib/polyphony', '') }.join(' ')
213
+ # end
157
214
  end
158
215
  end
159
216
  end
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- # Common exception class for interrupting fibers. These exceptions allow
5
- # control of fibers. BaseException exceptions can encapsulate a value and thus
6
- # provide a way to interrupt long-running blocking operations while still
7
- # passing a value back to the call site. BaseException exceptions can also
8
- # references a cancel scope in order to allow correct bubbling of exceptions
9
- # through nested cancel scopes.
4
+
5
+ # Base exception class for interrupting fibers. These exceptions allow control
6
+ # of fibers. BaseException exceptions can encapsulate a value and thus provide
7
+ # a way to interrupt long-running blocking operations while still passing a
8
+ # value back to the call site. BaseException exceptions can also references a
9
+ # cancel scope in order to allow correct bubbling of exceptions through nested
10
+ # cancel scopes.
10
11
  class BaseException < ::Exception
12
+
13
+ # Exception value, used mainly for `MoveOn` exceptions.
11
14
  attr_reader :value
12
15
 
16
+ # Initializes the exception, setting the caller and the value.
17
+ #
18
+ # @param value [any] Exception value
19
+ # @return [void]
13
20
  def initialize(value = nil)
14
21
  @caller_backtrace = caller
15
22
  @value = value
@@ -33,10 +40,18 @@ module Polyphony
33
40
 
34
41
  # Interjection is used to run arbitrary code on arbitrary fibers at any point
35
42
  class Interjection < BaseException
43
+
44
+ # Initializes an Interjection with the given proc.
45
+ #
46
+ # @param proc [Proc] interjection proc
47
+ # @return [void]
36
48
  def initialize(proc)
37
49
  @proc = proc
38
50
  end
39
51
 
52
+ # Invokes the exception by calling the associated proc.
53
+ #
54
+ # @return [void]
40
55
  def invoke
41
56
  @proc.call
42
57
  end
@@ -3,8 +3,16 @@
3
3
  require_relative './throttler'
4
4
 
5
5
  module Polyphony
6
- # Global API methods to be included in ::Object
6
+
7
+ # Global API methods to be included in `::Object`
7
8
  module GlobalAPI
9
+
10
+ # Spins up a fiber that will run the given block after sleeping for the
11
+ # given delay.
12
+ #
13
+ # @param delay [Number] delay in seconds before running the given block
14
+ # @param &block [Proc] block to run
15
+ # @return [Fiber] spun fiber
8
16
  def after(interval, &block)
9
17
  spin do
10
18
  sleep interval
@@ -12,64 +20,88 @@ module Polyphony
12
20
  end
13
21
  end
14
22
 
23
+ # call-seq:
24
+ # cancel_after(interval) { ... }
25
+ # cancel_after(interval, with_exception: exception) { ... }
26
+ # cancel_after(interval, with_exception: [klass, message]) { ... }
27
+ # cancel_after(interval) { |timeout| ... }
28
+ # cancel_after(interval, with_exception: exception) { |timeout| ... }
29
+ # cancel_after(interval, with_exception: [klass, message]) { |timeout| ... }
30
+ #
31
+ # Runs the given block after setting up a cancellation timer for
32
+ # cancellation. If the cancellation timer elapses, the execution will be
33
+ # interrupted with an exception defaulting to `Polyphony::Cancel`.
34
+ #
35
+ # This method should be used when a timeout should cause an exception to be
36
+ # propagated down the call stack or up the fiber tree.
37
+ #
38
+ # Example of normal use:
39
+ #
40
+ # def read_from_io_with_timeout(io)
41
+ # cancel_after(10) { io.read }
42
+ # rescue Polyphony::Cancel
43
+ # nil
44
+ # end
45
+ #
46
+ # The timeout period can be reset by passing a block that takes a single
47
+ # argument. The block will be provided with the canceller fiber. To reset
48
+ # the timeout, use `Fiber#reset`, as shown in the following example:
49
+ #
50
+ # cancel_after(10) do |timeout|
51
+ # loop do
52
+ # msg = socket.gets
53
+ # timeout.reset
54
+ # handle_msg(msg)
55
+ # end
56
+ # end
57
+ #
58
+ # @param interval [Number] timout in seconds
59
+ # @param with_exception: [Class, Exception] exception or exception class
60
+ # @param &block [Proc] block to execute
61
+ # @return [any] block's return value
15
62
  def cancel_after(interval, with_exception: Polyphony::Cancel, &block)
16
- if !block
17
- cancel_after_blockless_canceller(Fiber.current, interval, with_exception)
18
- elsif block.arity > 0
19
- cancel_after_with_block(Fiber.current, interval, with_exception, &block)
63
+ if block.arity > 0
64
+ cancel_after_with_optional_reset(interval, with_exception, &block)
20
65
  else
21
66
  Polyphony.backend_timeout(interval, with_exception, &block)
22
67
  end
23
68
  end
24
69
 
25
- def cancel_after_blockless_canceller(fiber, interval, with_exception)
26
- spin do
27
- sleep interval
28
- exception = cancel_exception(with_exception)
29
- exception.raising_fiber = nil
30
- fiber.schedule exception
31
- end
32
- end
33
-
34
- def cancel_after_with_block(fiber, interval, with_exception, &block)
35
- canceller = cancel_after_blockless_canceller(fiber, interval, with_exception)
36
- block.call(canceller)
37
- ensure
38
- canceller.stop
39
- end
40
-
41
- def cancel_exception(exception)
42
- case exception
43
- when Class then exception.new
44
- when Array then exception[0].new(exception[1])
45
- else RuntimeError.new(exception)
46
- end
47
- end
48
-
70
+ # Spins up a new fiber.
71
+ #
72
+ # @param tag [any] optional tag for the new fiber
73
+ # @param &block [Proc] fiber block
74
+ # @return [Fiber] new fiber
49
75
  def spin(tag = nil, &block)
50
76
  Fiber.current.spin(tag, caller, &block)
51
77
  end
52
78
 
79
+ # Spins up a new fiber, running the given block inside an infinite loop. If
80
+ # `rate:` or `interval:` parameters are given, the loop is throttled
81
+ # accordingly.
82
+ #
83
+ # @param tag [any] optional tag for the new fiber
84
+ # @param rate: [Number, nil] loop rate (times per second)
85
+ # @param interval: [Number, nil] interval between consecutive iterations in seconds
86
+ # @param &block [Proc] code to run
87
+ # @return [Fiber] new fiber
53
88
  def spin_loop(tag = nil, rate: nil, interval: nil, &block)
54
89
  if rate || interval
55
90
  Fiber.current.spin(tag, caller) do
56
91
  throttled_loop(rate: rate, interval: interval, &block)
57
92
  end
58
93
  else
59
- spin_looped_block(tag, caller, block)
60
- end
61
- end
62
-
63
- def spin_looped_block(tag, caller, block)
64
- Fiber.current.spin(tag, caller) do
65
- block.call while true
66
- rescue LocalJumpError, StopIteration
67
- # break called or StopIteration raised
94
+ spin_loop_without_throttling(tag, caller, block)
68
95
  end
69
96
  end
70
97
 
71
- def spin_scope
72
- raise unless block_given?
98
+ # Runs the given code, then waits for any child fibers of the current fibers
99
+ # to terminate.
100
+ #
101
+ # @param &block [Proc] code to run
102
+ # @return [any] given block's return value
103
+ def spin_scope(&block)
104
+ raise unless block
73
105
 
74
106
  spin do
75
107
  result = yield
@@ -78,61 +110,121 @@ module Polyphony
78
110
  end.await
79
111
  end
80
112
 
113
+ # Runs the given block in an infinite loop with a regular interval between
114
+ # consecutive iterations.
115
+ #
116
+ # @param interval [Number] interval between consecutive iterations in seconds
117
+ # @param &block [Proc] block to run
118
+ # @return [void]
81
119
  def every(interval, &block)
82
120
  Polyphony.backend_timer_loop(interval, &block)
83
121
  end
84
122
 
123
+ # call-seq:
124
+ # move_on_after(interval) { ... }
125
+ # move_on_after(interval, with_value: value) { ... }
126
+ # move_on_after(interval) { |canceller| ... }
127
+ # move_on_after(interval, with_value: value) { |canceller| ... }
128
+ #
129
+ # Runs the given block after setting up a cancellation timer for
130
+ # cancellation. If the cancellation timer elapses, the execution will be
131
+ # interrupted with a `Polyphony::MoveOn` exception, which will be rescued,
132
+ # and with cause the operation to return the given value.
133
+ #
134
+ # This method should be used when a timeout is to be handled locally,
135
+ # without generating an exception that is to propagated down the call stack
136
+ # or up the fiber tree.
137
+ #
138
+ # Example of normal use:
139
+ #
140
+ # move_on_after(10) {
141
+ # sleep 60
142
+ # 42
143
+ # } #=> nil
144
+ #
145
+ # move_on_after(10, with_value: :oops) {
146
+ # sleep 60
147
+ # 42
148
+ # } #=> :oops
149
+ #
150
+ # The timeout period can be reset by passing a block that takes a single
151
+ # argument. The block will be provided with the canceller fiber. To reset
152
+ # the timeout, use `Fiber#reset`, as shown in the following example:
153
+ #
154
+ # move_on_after(10) do |timeout|
155
+ # loop do
156
+ # msg = socket.gets
157
+ # timeout.reset
158
+ # handle_msg(msg)
159
+ # end
160
+ # end
161
+ #
162
+ # @param interval [Number] timout in seconds
163
+ # @param with_value: [any] return value in case of timeout
164
+ # @param &block [Proc] block to execute
165
+ # @return [any] block's return value
85
166
  def move_on_after(interval, with_value: nil, &block)
86
- if !block
87
- move_on_blockless_canceller(Fiber.current, interval, with_value)
88
- elsif block.arity > 0
89
- move_on_after_with_block(Fiber.current, interval, with_value, &block)
167
+ if block.arity > 0
168
+ move_on_after_with_optional_reset(interval, with_value, &block)
90
169
  else
91
170
  Polyphony.backend_timeout(interval, nil, with_value, &block)
92
171
  end
93
172
  end
94
173
 
95
- def move_on_blockless_canceller(fiber, interval, with_value)
96
- spin do
97
- sleep interval
98
- fiber.schedule with_value
99
- end
100
- end
101
-
102
- def move_on_after_with_block(fiber, interval, with_value, &block)
103
- canceller = spin do
104
- sleep interval
105
- fiber.schedule Polyphony::MoveOn.new(with_value)
106
- end
107
- block.call(canceller)
108
- rescue Polyphony::MoveOn => e
109
- e.value
110
- ensure
111
- canceller.stop
112
- end
113
-
174
+ # Returns the first message from the current fiber's mailbox. If the mailbox
175
+ # is empty, blocks until a message is available.
176
+ #
177
+ # @return [any] received message
114
178
  def receive
115
179
  Fiber.current.receive
116
180
  end
117
181
 
182
+ # Returns all messages currently pending on the current fiber's mailbox.
183
+ #
184
+ # @return [Array] array of received messages
118
185
  def receive_all_pending
119
186
  Fiber.current.receive_all_pending
120
187
  end
121
188
 
189
+ # Supervises the current fiber's children. See `Fiber#supervise` for
190
+ # options.
191
+ #
192
+ # @param *args [Array] positional parameters
193
+ # @param **opts [Hash] named parameters
194
+ # @param &block [Proc] given block
195
+ # @return [void]
122
196
  def supervise(*args, **opts, &block)
123
197
  Fiber.current.supervise(*args, **opts, &block)
124
198
  end
125
199
 
200
+ # Sleeps for the given duration. If the duration is `nil`, sleeps
201
+ # indefinitely.
202
+ #
203
+ # @param duration [Number, nil] duration
204
+ # @return [void]
126
205
  def sleep(duration = nil)
127
- return sleep_forever unless duration
128
-
129
- Polyphony.backend_sleep duration
130
- end
131
-
132
- def sleep_forever
133
- Polyphony.backend_wait_event(true)
206
+ duration ?
207
+ Polyphony.backend_sleep(duration) : Polyphony.backend_wait_event(true)
134
208
  end
135
209
 
210
+ # call-seq:
211
+ # throttled_loop(rate) { ... }
212
+ # throttled_loop(interval: value) { ... }
213
+ # throttled_loop(rate: value) { ... }
214
+ # throttled_loop(rate, count: value) { ... }
215
+ #
216
+ # Starts a throttled loop with the given rate. If `count:` is given, the
217
+ # loop is run for the given number of times. Otherwise, the loop is
218
+ # infinite. The loop rate (times per second) can be given as the rate
219
+ # parameter. The throttling can also be controlled by providing an
220
+ # `interval:` or `rate:` named parameter.
221
+ #
222
+ # @param rate [Number, nil] loop rate (times per second)
223
+ # @param rate: [Number] loop rate (times per second)
224
+ # @param interval: [Number] loop interval in seconds
225
+ # @param count: [Number, nil] number of iterations (nil for infinite)
226
+ # @param &block [Proc] code to run
227
+ # @return [void]
136
228
  def throttled_loop(rate = nil, **opts, &block)
137
229
  throttler = Polyphony::Throttler.new(rate || opts)
138
230
  if opts[:count]
@@ -144,10 +236,73 @@ module Polyphony
144
236
  end
145
237
  rescue LocalJumpError, StopIteration
146
238
  # break called or StopIteration raised
239
+ end
240
+
241
+ private
242
+
243
+ # Helper method for performing a `cancel_after` with optional reset.
244
+ #
245
+ # @param interval [Number] timeout interval in seconds
246
+ # @param exception [Exception, Class, Array<class, message>] exception spec
247
+ # @param &block [Proc] block to run
248
+ # @return [any] block's return value
249
+ def cancel_after_with_optional_reset(interval, exception, &block)
250
+ canceller = spin do
251
+ sleep interval
252
+ exception = cancel_exception(exception)
253
+ exception.raising_fiber = Fiber.current
254
+ fiber.cancel(exception).await
255
+ end
256
+ block.call(canceller)
147
257
  ensure
148
- throttler&.stop
258
+ canceller.stop
259
+ end
260
+
261
+ # Converts the given exception spec to an exception instance.
262
+ #
263
+ # @param exception [Exception, Class, Array<class, message>] exception spec
264
+ # @return [Exception] exception instance
265
+ def cancel_exception(exception)
266
+ case exception
267
+ when Class then exception.new
268
+ when Array then exception[0].new(exception[1])
269
+ else RuntimeError.new(exception)
270
+ end
149
271
  end
272
+
273
+ # Helper method for performing `#spin_loop` without throttling. Spins up a
274
+ # new fiber in which to run the loop.
275
+ #
276
+ # @param tag [any] new fiber's tag
277
+ # @param caller [Array<String>] caller info
278
+ # @param block [Proc] code to run
279
+ # @return [void]
280
+ def spin_loop_without_throttling(tag, caller, block)
281
+ Fiber.current.spin(tag, caller) do
282
+ block.call while true
283
+ rescue LocalJumpError, StopIteration
284
+ # break called or StopIteration raised
285
+ end
286
+ end
287
+
288
+ # Helper method for performing `#move_on_after` with optional reset.
289
+ #
290
+ # @param interval [Number] timeout interval in seconds
291
+ # @param value [any] return value in case of timeout
292
+ # @param &block [Proc] code to run
293
+ # @return [any] return value of given block or timeout value
294
+ def move_on_after_with_optional_reset(interval, value, &block)
295
+ fiber = Fiber.current
296
+ canceller = spin do
297
+ sleep interval
298
+ fiber.move_on(value).await
299
+ end
300
+ block.call(canceller)
301
+ rescue Polyphony::MoveOn => e
302
+ e.value
303
+ ensure
304
+ canceller.stop
305
+ end
306
+
150
307
  end
151
308
  end
152
-
153
- Object.include Polyphony::GlobalAPI