polyphony 0.31 → 0.32

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24126ff67fd9fd240299e1af8567da58f072c6c60df3f77b40f87ac108010f80
4
- data.tar.gz: 95bcac53f361d65718aea1b55b557a7f68624d9815c38ee7c0cb79e11cb7bc96
3
+ metadata.gz: 75e18e7719d577fc1f07679874d2c3afe23b5a6b6184c1c6723006d0f9fcdf80
4
+ data.tar.gz: 347608163b6d0ec934b2a70d1b28ffd1c0056b3da51f242f6619ac4a8e94a3ad
5
5
  SHA512:
6
- metadata.gz: 8849c26d4c2eccba56e786ac7ad76c2e2129244b619b988d649dca636491dd6158c0bed8feac191df9a7958f946592a8987a1e6709f27ef1383403b279f5595d
7
- data.tar.gz: e8797e0b6e3852a208488213634b1360fc297e3ee9efa5c5e05d538a2684a02dfdf5efb00aca82b72064fc996a50c91de5c477a55ef5fd9dbf85275fd10e1f42
6
+ metadata.gz: 41cfdd7550a444c590b08eff7c2bf716ce42a19250074bca0425334be4b32b96efba4d60bbe9b1257cf3b8616be31da940695709a25af8c5d03c9803dff8fb80
7
+ data.tar.gz: 3a12af73aa84d4c3ae9e5c94c0dd55f99bd7eaf3ca49e1f59ab4b80d224dfe1dc068c68f4f8e09ab3d0bcc4996f61ae8d03c42158912f24458ad0ed1e09e6b2a
@@ -7,7 +7,7 @@ AllCops:
7
7
  - 'test/**/*.rb'
8
8
  - 'examples/**/*.rb'
9
9
  - 'Gemfile*'
10
- - lib/polyphony/http/agent.rb
10
+ - lib/polyphony/adapters/irb.rb
11
11
 
12
12
  Style/LambdaCall:
13
13
  Enabled: false
@@ -1,3 +1,12 @@
1
+ ## 0.32 2020-02-29
2
+
3
+ * Accept optional throttling rate in `#spin_loop`
4
+ * Remove CancelScope
5
+ * Allow spinning fibers from a parent fiber other than the current
6
+ * Add `#receive_pending` global API.
7
+ * Prevent race condition in `Gyro::Queue`.
8
+ * Improve signal handling - `INT`, `TERM` signals are now always handled in the
9
+ main fiber.
1
10
  * Fix adapter requires (redis and postgres).
2
11
 
3
12
  ## 0.31 2020-02-20
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.31)
4
+ polyphony (0.32)
5
5
  modulation (~> 1.0)
6
6
 
7
7
  GEM
data/TODO.md CHANGED
@@ -1,56 +1,5 @@
1
1
  ## 0.32 Working Sinatra application
2
2
 
3
- - Introduce mailbox limiting:
4
- - add API for limiting mailbox size:
5
-
6
- ```ruby
7
- Fiber.current.mailbox_limit = 1000
8
- ```
9
-
10
- - Add the limit for `Gyro::Queue`
11
-
12
- ```ruby
13
- Gyro::Queue.new(1000)
14
- ```
15
-
16
- - Pushing to a limited queue will block if limit is reached
17
-
18
- - Introduce selective receive:
19
-
20
- ```ruby
21
- # returns (or waits for) the first message for which the block returns true
22
- (_, item) = receive { |msg| msg.first == ref }
23
- ```
24
-
25
- Possible implementation:
26
-
27
- ```ruby
28
- def receive
29
- return @mailbox.shift unless block_given?
30
-
31
- loop
32
- msg = @mailbox.shift
33
- return msg if yield(msg)
34
-
35
- # message didn't match condition, put it back in queue
36
- @mailbox.push msg
37
- end
38
- end
39
- ```
40
-
41
- - Test hypothetical situation 1:
42
- - fiber A sends a request to fiber B
43
- - fiber B terminates on a raised exception
44
- - What happens to fiber A? (it should get the exception)
45
-
46
- - Accept rate/interval in `spin_loop` and `spin_worker_loop`:
47
-
48
- ```ruby
49
- spin_loop(10) { ... } # 10 times per second
50
- spin_loop(rate: 10) { ... } # 10 times per second
51
- spin_loop(interval: 10) { ... } # once every ten seconds
52
- ```
53
-
54
3
  - Docs
55
4
  - landing page:
56
5
  - links to the interesting stuff
@@ -58,21 +7,21 @@
58
7
  - faq
59
8
  - benchmarks
60
9
  - explain difference between `sleep` and `suspend`
61
- - concurrency overview: add explanation about async vs sync
62
-
63
- - move all adapters into polyphony/adapters
10
+ - add explanation about async vs sync
11
+ - discuss using `snooze` for ensuring responsiveness when executing CPU-bound work
64
12
 
65
13
  - Check why first call to `#sleep` returns too early in tests. Check the
66
14
  sleep behaviour in a spawned thread.
67
- - sintra app with database access (postgresql)
68
15
 
69
- ## 0.33 Sidekick
16
+ ## 0.33 Sinatra / Sidekiq
70
17
 
71
- Plan of action:
18
+ - sintra app with database access (postgresql)
72
19
 
73
- - fork sidekiq, make adjustments to Polyphony code
74
- - test performance
75
- - proceed from there
20
+ - sidekiq: Plan of action
21
+ - see if we can get by just writing an adapter
22
+ - if not, fork sidekiq, make adjustments to Polyphony code
23
+ - test performance
24
+ - proceed from there
76
25
 
77
26
  ## 0.34 Testing && Docs
78
27
 
@@ -112,10 +61,8 @@ Prior art:
112
61
  ### DNS server
113
62
 
114
63
  ```ruby
115
- Server = import('../../lib/polyphony/dns/server')
116
-
117
- server = Server.new do |transaction|
118
- puts "got query from #{transaction.info[:client_ip_address]}"
64
+ require 'polyphony/dns'
65
+ server = Polyphony::DNS::Server.new do |transaction|
119
66
  transaction.questions.each do |q|
120
67
  respond(transaction, q[:domain], q[:resource_class])
121
68
  end
@@ -129,3 +76,55 @@ Prior art:
129
76
 
130
77
  - https://github.com/socketry/async-dns
131
78
 
79
+ ### Work on API
80
+
81
+ - Introduce mailbox limiting:
82
+ - add API for limiting mailbox size:
83
+
84
+ ```ruby
85
+ Fiber.current.mailbox_limit = 1000
86
+ ```
87
+
88
+ - Add the limit for `Gyro::Queue`
89
+
90
+ ```ruby
91
+ Gyro::Queue.new(1000)
92
+ ```
93
+
94
+ - Pushing to a limited queue will block if limit is reached
95
+
96
+ - Introduce selective receive:
97
+
98
+ ```ruby
99
+ # returns (or waits for) the first message for which the block returns true
100
+ (_, item) = receive { |msg| msg.first == ref }
101
+ ```
102
+
103
+ Possible implementation:
104
+
105
+ ```ruby
106
+ def receive
107
+ return @mailbox.shift unless block_given?
108
+
109
+ loop
110
+ msg = @mailbox.shift
111
+ return msg if yield(msg)
112
+
113
+ # message didn't match condition, put it back in queue
114
+ @mailbox.push msg
115
+ end
116
+ end
117
+ ```
118
+
119
+ - Add option for setting the exception raised on cancelling using `#cancel_after`:
120
+
121
+ ```ruby
122
+ cancel_after(3, with_error: MyErrorClass) do
123
+ do_my_thing
124
+ end
125
+
126
+ # or a RuntimeError with message
127
+ cancel_after(3, with_error: 'Cancelling due to timeout') do
128
+ do_my_thing
129
+ end
130
+ ```
@@ -209,8 +209,39 @@ trace.
209
209
  To ensure correct behaviour for these two signals, polyphony installs signal
210
210
  handlers that ensure that the main thread's event loop stops if it's currently
211
211
  running, and that the corresponding exceptions (namely `SystemExit` and
212
- `Interrupt`) are handled correctly by propagating them using Polyphony's normal
213
- exception handling mechanisms.
212
+ `Interrupt`) are handled correctly by passing them to the main fiber.
213
+
214
+ ### Graceful process termination
215
+
216
+ In order to ensure your application terminates gracefully upon receiving an
217
+ `INT` or `TERM` signal, you'll need to:
218
+
219
+ 1. Rescue the corresponding exceptions in the main fiber.
220
+ 2. Rescue `Polyphony::Terminate` exceptions in each fiber that needs to perform
221
+ operations such as handling any pending requests, etc.
222
+
223
+ ```ruby
224
+ # In a worker fiber
225
+ def do_work
226
+ loop do
227
+ req = receive
228
+ handle_req(req)
229
+ end
230
+ rescue Polyphony::Terminate
231
+ # We still need to handle any pending request
232
+ receive_pending.each { handle_req(req) }
233
+ end
234
+
235
+ # on the main fiber
236
+ begin
237
+ spin_up_lots_fibers
238
+ rescue Interrupt, SystemExit
239
+ Fiber.current.terminate_all_children
240
+ Fiber.current.await_all_children
241
+ end
242
+ ```
243
+
244
+ ### Handling other signals
214
245
 
215
246
  Care should be taken when handling other signals. There are two options for
216
247
  correctly handling the signals: using Ruby's stock `trap` method, and using
@@ -219,16 +250,30 @@ usual, but making sure we're not inside the event loop:
219
250
 
220
251
  ```ruby
221
252
  trap('SIGHUP') do
222
- Thread.current.break_out_of_ev_loop(nil)
253
+ Thread.current.break_out_of_ev_loop(Thread.current.main_fiber, nil)
223
254
  handle_hup_signal
224
255
  end
225
256
  ```
226
257
 
227
- The alternative is to use `Polyphony.wait_for_signal`:
258
+ A second technique that might be useful is to use a `Gyro::Async` watcher and
259
+ signal it when the process signal is trapped:
260
+
261
+ ```ruby
262
+ sighup_async = Gyro::Async.new
263
+ sighup_handler = spin_loop do
264
+ sighup_async.await
265
+ handle_sighup
266
+ end
267
+
268
+ trap('SIGHUP') { sighup_async.signal }
269
+ ```
270
+
271
+ Another alternative is to use `Polyphony.wait_for_signal`, which uses a
272
+ `Gyro::Signal` watcher under the hood:
228
273
 
229
274
  ```ruby
230
275
  hup_handler = spin_loop do
231
- Polyphony.wait_for_signal
276
+ Polyphony.wait_for_signal('SIGHUP')
232
277
  handle_hup_signal
233
278
  end
234
279
  ```
@@ -58,7 +58,7 @@ VALUE Gyro_Queue_push(VALUE self, VALUE value) {
58
58
 
59
59
  if (RARRAY_LEN(queue->wait_queue) > 0) {
60
60
  VALUE async = rb_ary_shift(queue->wait_queue);
61
- return rb_funcall(async, ID_signal_bang, 1, value);
61
+ rb_funcall(async, ID_signal_bang, 1, Qnil);
62
62
  }
63
63
 
64
64
  rb_ary_push(queue->queue, value);
@@ -69,15 +69,15 @@ VALUE Gyro_Queue_shift(VALUE self) {
69
69
  struct Gyro_Queue *queue;
70
70
  GetGyro_Queue(self, queue);
71
71
 
72
- if (RARRAY_LEN(queue->queue) > 0) {
73
- return rb_ary_shift(queue->queue);
72
+ while (1) {
73
+ if (RARRAY_LEN(queue->queue) > 0) {
74
+ return rb_ary_shift(queue->queue);
75
+ }
76
+
77
+ VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
78
+ rb_ary_push(queue->wait_queue, async);
79
+ Gyro_Async_await(async);
74
80
  }
75
-
76
- VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
77
- rb_ary_push(queue->wait_queue, async);
78
- VALUE ret = Gyro_Async_await(async);
79
- RB_GC_GUARD(async);
80
- return ret;
81
81
  }
82
82
 
83
83
  VALUE Gyro_Queue_shift_no_wait(VALUE self) {
@@ -233,9 +233,11 @@ VALUE Thread_current_event_selector() {
233
233
  return rb_ivar_get(rb_thread_current(), ID_ivar_event_selector);
234
234
  }
235
235
 
236
- VALUE Thread_fiber_break_out_of_ev_loop(VALUE self, VALUE resume_obj) {
236
+ VALUE Thread_fiber_break_out_of_ev_loop(VALUE self, VALUE fiber, VALUE resume_obj) {
237
237
  VALUE selector = rb_ivar_get(self, ID_ivar_event_selector);
238
- Thread_schedule_fiber_with_priority(self, rb_fiber_current(), resume_obj);
238
+ if (fiber != Qnil) {
239
+ Thread_schedule_fiber_with_priority(self, fiber, resume_obj);
240
+ }
239
241
 
240
242
  if (Gyro_Selector_break_out_of_ev_loop(selector) == Qnil) {
241
243
  // we're not inside the ev_loop, so we just do a switchpoint
@@ -273,7 +275,7 @@ void Init_Thread() {
273
275
  rb_define_method(rb_cThread, "stop_event_selector", Thread_stop_event_selector, 0);
274
276
  rb_define_method(rb_cThread, "reset_fiber_scheduling", Thread_reset_fiber_scheduling, 0);
275
277
  rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
276
- rb_define_method(rb_cThread, "break_out_of_ev_loop", Thread_fiber_break_out_of_ev_loop, 1);
278
+ rb_define_method(rb_cThread, "break_out_of_ev_loop", Thread_fiber_break_out_of_ev_loop, 2);
277
279
 
278
280
  rb_define_method(rb_cThread, "schedule_fiber", Thread_schedule_fiber, 2);
279
281
  rb_define_method(rb_cThread, "schedule_fiber_with_priority",
@@ -28,15 +28,13 @@ module Polyphony
28
28
  Net = import './polyphony/net'
29
29
 
30
30
  auto_import(
31
- CancelScope: './polyphony/core/cancel_scope',
32
31
  Channel: './polyphony/core/channel',
33
32
  FS: './polyphony/adapters/fs',
34
33
  ResourcePool: './polyphony/core/resource_pool',
35
34
  Sync: './polyphony/core/sync',
36
35
  ThreadPool: './polyphony/core/thread_pool',
37
36
  Throttler: './polyphony/core/throttler',
38
- Trace: './polyphony/adapters/trace',
39
- Websocket: './polyphony/websocket'
37
+ Trace: './polyphony/adapters/trace'
40
38
  )
41
39
 
42
40
  class << self
@@ -70,7 +68,7 @@ end
70
68
  def install_terminating_signal_handler(signal, exception_class)
71
69
  trap(signal) do
72
70
  exception = exception_class.new
73
- Thread.current.break_out_of_ev_loop(exception)
71
+ Thread.current.break_out_of_ev_loop(Thread.current.main_fiber, exception)
74
72
  end
75
73
  end
76
74
 
@@ -9,7 +9,7 @@ if Object.constants.include?(:Reline)
9
9
  raise if read_ios.size > 1
10
10
  raise if write_ios.size > 0
11
11
  raise if error_ios.size > 0
12
-
12
+
13
13
  fiber = Fiber.current
14
14
  timer = spin do
15
15
  sleep timeout
@@ -43,9 +43,10 @@ else
43
43
  end
44
44
  end
45
45
 
46
+ # RubyLex patches
46
47
  class ::RubyLex
47
48
  class TerminateLineInput2 < RuntimeError
48
49
  end
49
50
  const_set(:TerminateLineInput, TerminateLineInput2)
50
51
  end
51
- end
52
+ end
@@ -1,29 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- export :Interrupt, :MoveOn, :Cancel, :Terminate
3
+ export :MoveOn, :Cancel, :Terminate
4
4
 
5
5
  # Common exception class for interrupting fibers. These exceptions allow
6
- # control of fibers. Interrupt exceptions can encapsulate a value and thus
6
+ # control of fibers. BaseException exceptions can encapsulate a value and thus
7
7
  # provide a way to interrupt long-running blocking operations while still
8
- # passing a value back to the call site. Interrupt exceptions can also
8
+ # passing a value back to the call site. BaseException exceptions can also
9
9
  # references a cancel scope in order to allow correct bubbling of exceptions
10
10
  # through nested cancel scopes.
11
- class Interrupt < ::Exception
12
- attr_reader :scope, :value
11
+ class BaseException < ::Exception
12
+ attr_reader :value
13
13
 
14
- def initialize(scope = nil, value = nil)
15
- @scope = scope
14
+ def initialize(value = nil)
16
15
  @value = value
17
16
  end
18
17
  end
19
18
 
20
19
  # MoveOn is used to interrupt a long-running blocking operation, while
21
20
  # continuing the rest of the computation.
22
- class MoveOn < Interrupt; end
21
+ class MoveOn < BaseException; end
23
22
 
24
23
  # Cancel is used to interrupt a long-running blocking operation, bubbling the
25
24
  # exception up through cancel scopes and supervisors.
26
- class Cancel < Interrupt; end
25
+ class Cancel < BaseException; end
27
26
 
28
27
  # Terminate is used to interrupt a fiber once its parent fiber has terminated.
29
- class Terminate < Interrupt; end
28
+ class Terminate < BaseException; end
@@ -32,8 +32,14 @@ module API
32
32
  Fiber.current.spin(tag, caller, &block)
33
33
  end
34
34
 
35
- def spin_loop(tag = nil, &block)
36
- Fiber.current.spin(tag, caller) { loop(&block) }
35
+ def spin_loop(tag = nil, rate: nil, &block)
36
+ if rate
37
+ Fiber.current.spin(tag, caller) do
38
+ throttled_loop(rate, &block)
39
+ end
40
+ else
41
+ Fiber.current.spin(tag, caller) { loop(&block) }
42
+ end
37
43
  end
38
44
 
39
45
  def every(interval)
@@ -50,7 +56,7 @@ module API
50
56
  fiber = ::Fiber.current
51
57
  canceller = spin do
52
58
  sleep interval
53
- fiber.schedule Exceptions::MoveOn.new(nil, with_value)
59
+ fiber.schedule Exceptions::MoveOn.new(with_value)
54
60
  end
55
61
  block.call
56
62
  rescue Exceptions::MoveOn => e
@@ -63,6 +69,10 @@ module API
63
69
  Fiber.current.receive
64
70
  end
65
71
 
72
+ def receive_pending
73
+ Fiber.current.receive_pending
74
+ end
75
+
66
76
  def sleep(duration = nil)
67
77
  return sleep_forever unless duration
68
78
 
@@ -23,7 +23,7 @@ module FiberControl
23
23
  def interrupt(value = nil)
24
24
  return if @running == false
25
25
 
26
- schedule Exceptions::MoveOn.new(nil, value)
26
+ schedule Exceptions::MoveOn.new(value)
27
27
  end
28
28
  alias_method :stop, :interrupt
29
29
 
@@ -139,6 +139,10 @@ module FiberMessaging
139
139
  def receive
140
140
  @mailbox.shift
141
141
  end
142
+
143
+ def receive_pending
144
+ @mailbox.shift_each
145
+ end
142
146
  end
143
147
 
144
148
  # Methods for controlling child fibers
@@ -149,7 +153,7 @@ module ChildFiberControl
149
153
 
150
154
  def spin(tag = nil, orig_caller = caller, &block)
151
155
  f = Fiber.new { |v| f.run(v) }
152
- f.prepare(tag, block, orig_caller)
156
+ f.prepare(tag, block, orig_caller, self)
153
157
  (@children ||= {})[f] = true
154
158
  f
155
159
  end
@@ -180,13 +184,13 @@ class ::Fiber
180
184
 
181
185
  extend FiberControlClassMethods
182
186
 
183
- attr_accessor :tag, :thread
187
+ attr_accessor :tag, :thread, :parent
184
188
 
185
- def prepare(tag, block, caller)
189
+ def prepare(tag, block, caller, parent)
186
190
  __fiber_trace__(:fiber_create, self)
187
191
  @thread = Thread.current
188
192
  @tag = tag
189
- @parent = Fiber.current
193
+ @parent = parent
190
194
  @caller = caller
191
195
  @block = block
192
196
  @mailbox = Gyro::Queue.new
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.31'
4
+ VERSION = '0.32'
5
5
  end
@@ -13,6 +13,24 @@ class FiberTest < MiniTest::Test
13
13
  f&.stop
14
14
  end
15
15
 
16
+ def test_children_parent
17
+ assert_nil Fiber.current.parent
18
+
19
+ f1 = spin {}
20
+ f2 = spin {}
21
+
22
+ assert_equal [f1, f2], Fiber.current.children
23
+ assert_equal Fiber.current, f1.parent
24
+ assert_equal Fiber.current, f2.parent
25
+ end
26
+
27
+ def test_spin_from_different_fiber
28
+ f1 = spin { sleep }
29
+ f2 = f1.spin { sleep }
30
+ assert_equal f1, f2.parent
31
+ assert_equal [f2], f1.children
32
+ end
33
+
16
34
  def test_await
17
35
  result = nil
18
36
  f = Fiber.current.spin do
@@ -744,6 +762,36 @@ class MailboxTest < MiniTest::Test
744
762
  f.await
745
763
  assert_equal ['foo'] * 100, messages
746
764
  end
765
+
766
+ def test_receive_pending
767
+ assert_equal [], receive_pending
768
+
769
+ (1..5).each { |i| Fiber.current << i }
770
+ assert_equal (1..5).to_a, receive_pending
771
+ assert_equal [], receive_pending
772
+ end
773
+
774
+ def test_receive_pending_on_termination
775
+ buffer = []
776
+ worker = spin do
777
+ loop { buffer << receive }
778
+ rescue Polyphony::Terminate
779
+ receive_pending.each { |r| buffer << r }
780
+ end
781
+
782
+ worker << 1
783
+ worker << 2
784
+ 10.times { snooze }
785
+ assert_equal [1, 2], buffer
786
+
787
+ worker << 3
788
+ worker << 4
789
+ worker << 5
790
+ worker.terminate
791
+ worker.await
792
+
793
+ assert_equal (1..5).to_a, buffer
794
+ end
747
795
  end
748
796
 
749
797
  class FiberControlTest < MiniTest::Test
@@ -42,92 +42,6 @@ class SpinTest < MiniTest::Test
42
42
  end
43
43
  end
44
44
 
45
- class CancelScopeTest < Minitest::Test
46
- def sleep_with_cancel(ctx, mode = nil)
47
- Polyphony::CancelScope.new(mode: mode).call do |c|
48
- ctx[:cancel_scope] = c
49
- ctx[:result] = sleep(0.01)
50
- end
51
- end
52
-
53
- def test_that_cancel_scope_cancels_fiber
54
- ctx = {}
55
- spin do
56
- after(0.005) { ctx[:cancel_scope].cancel! }
57
- sleep_with_cancel(ctx, :cancel)
58
- rescue Exception => e
59
- ctx[:result] = e
60
- nil
61
- end
62
- assert_nil ctx[:result]
63
- # async operation will only begin on next iteration of event loop
64
- assert_nil ctx[:cancel_scope]
65
-
66
- Thread.current.switch_fiber
67
- assert_kind_of Polyphony::CancelScope, ctx[:cancel_scope]
68
- assert_kind_of Polyphony::Cancel, ctx[:result]
69
- end
70
-
71
- def test_that_cancel_scope_cancels_async_op_with_stop
72
- ctx = {}
73
- spin do
74
- after(0) { ctx[:cancel_scope].cancel! }
75
- sleep_with_cancel(ctx, :stop)
76
- end
77
-
78
- Thread.current.switch_fiber
79
- assert ctx[:cancel_scope]
80
- assert_nil ctx[:result]
81
- end
82
-
83
- def test_that_cancel_after_raises_cancelled_exception
84
- result = nil
85
- spin do
86
- cancel_after(0.01) do
87
- sleep(1000)
88
- end
89
- result = 42
90
- rescue Polyphony::Cancel
91
- result = :cancelled
92
- end
93
- suspend
94
- assert_equal :cancelled, result
95
- end
96
-
97
- # def test_that_cancel_scopes_can_be_nested
98
- # inner_result = nil
99
- # outer_result = nil
100
- # spin do
101
- # Polyphony::CancelScope.new(timeout: 0.01) do
102
- # Polyphony::CancelScope.new(timeout: 0.02) do
103
- # sleep(1000)
104
- # end
105
- # inner_result = 42
106
- # end
107
- # outer_result = 42
108
- # end
109
- # suspend
110
- # assert_nil inner_result
111
- # assert_equal 42, outer_result
112
-
113
- # Polyphony.reset!
114
-
115
- # outer_result = nil
116
- # spin do
117
- # move_on_after(0.02) do
118
- # move_on_after(0.01) do
119
- # sleep(1000)
120
- # end
121
- # inner_result = 42
122
- # end
123
- # outer_result = 42
124
- # end
125
- # suspend
126
- # assert_equal 42, inner_result
127
- # assert_equal 42, outer_result
128
- # end
129
- end
130
-
131
45
  class ExceptionTest < MiniTest::Test
132
46
  def test_cross_fiber_backtrace
133
47
  error = nil
@@ -195,7 +109,9 @@ class MoveOnAfterTest < MiniTest::Test
195
109
  assert t1 - t0 < 0.02
196
110
  assert_equal :bar, v
197
111
  end
112
+ end
198
113
 
114
+ class SpinTest < MiniTest::Test
199
115
  def test_spin_without_tag
200
116
  f = spin { }
201
117
  assert_kind_of Fiber, f
@@ -207,7 +123,9 @@ class MoveOnAfterTest < MiniTest::Test
207
123
  assert_kind_of Fiber, f
208
124
  assert_equal :foo, f.tag
209
125
  end
126
+ end
210
127
 
128
+ class SpinLoopTest < MiniTest::Test
211
129
  def test_spin_loop
212
130
  buffer = []
213
131
  counter = 0
@@ -243,6 +161,17 @@ class MoveOnAfterTest < MiniTest::Test
243
161
  assert_equal :my_loop, f.tag
244
162
  end
245
163
 
164
+ def test_spin_loop_with_rate
165
+ buffer = []
166
+ counter = 0
167
+ f = spin_loop(rate: 50) { buffer << (counter += 1) }
168
+ sleep 0.1
169
+ f.stop
170
+ assert counter >= 5 && counter <= 6
171
+ end
172
+ end
173
+
174
+ class ThrottledLoopTest < MiniTest::Test
246
175
  def test_throttled_loop
247
176
  buffer = []
248
177
  counter = 0
@@ -263,7 +192,9 @@ class MoveOnAfterTest < MiniTest::Test
263
192
  f.await
264
193
  assert_equal [1, 2, 3, 4, 5], buffer
265
194
  end
195
+ end
266
196
 
197
+ class GlobalAPIEtcTest < MiniTest::Test
267
198
  def test_every
268
199
  buffer = []
269
200
  f = spin do
@@ -38,7 +38,7 @@ class SignalTest < MiniTest::Test
38
38
  end
39
39
 
40
40
  class SignalTrapTest < Minitest::Test
41
- def test_signal_exception_propagation
41
+ def test_signal_exception_handling
42
42
  i, o = IO.pipe
43
43
  pid = Polyphony.fork do
44
44
  i.close
@@ -46,12 +46,10 @@ class SignalTrapTest < Minitest::Test
46
46
  spin do
47
47
  sleep 1
48
48
  rescue ::Interrupt => e
49
+ # the signal will be trapped in the context of this fiber
49
50
  o.puts "1-interrupt"
50
51
  raise e
51
52
  end.await
52
- rescue ::Interrupt => e
53
- o.puts "2-interrupt"
54
- raise e
55
53
  end.await
56
54
  rescue ::Interrupt => e
57
55
  o.puts "3-interrupt"
@@ -64,7 +62,36 @@ class SignalTrapTest < Minitest::Test
64
62
  Process.kill('INT', pid)
65
63
  watcher.await
66
64
  buffer = i.read
67
- assert_equal "1-interrupt\n2-interrupt\n3-interrupt\n", buffer
65
+ assert_equal "3-interrupt\n", buffer
66
+ end
67
+
68
+ def test_signal_exception_with_cleanup
69
+ i, o = IO.pipe
70
+ pid = Polyphony.fork do
71
+ i.close
72
+ spin do
73
+ spin do
74
+ sleep
75
+ rescue Polyphony::Terminate
76
+ o.puts "1 - terminated"
77
+ end.await
78
+ rescue Polyphony::Terminate
79
+ o.puts "2 - terminated"
80
+ end.await
81
+ rescue Interrupt
82
+ o.puts "3 - interrupted"
83
+ Fiber.current.terminate_all_children
84
+ Fiber.current.await_all_children
85
+ ensure
86
+ o.close
87
+ end
88
+ sleep 0.01
89
+ o.close
90
+ watcher = Gyro::Child.new(pid)
91
+ Process.kill('INT', pid)
92
+ watcher.await
93
+ buffer = i.read
94
+ assert_equal "3 - interrupted\n2 - terminated\n1 - terminated\n", buffer
68
95
  end
69
96
 
70
97
  def test_signal_exception_possible_race_condition
@@ -106,6 +133,6 @@ class SignalTrapTest < Minitest::Test
106
133
  Process.kill('INT', pid)
107
134
  Gyro::Child.new(pid).await
108
135
  buffer = i.read
109
- assert_equal "1-interrupt\n3-interrupt\n", buffer
136
+ assert_equal "3-interrupt\n", buffer
110
137
  end
111
138
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyphony
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.31'
4
+ version: '0.32'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-28 00:00:00.000000000 Z
11
+ date: 2020-02-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: modulation
@@ -298,7 +298,6 @@ files:
298
298
  - examples/core/xx-queue-async.rb
299
299
  - examples/core/xx-readpartial.rb
300
300
  - examples/core/xx-recurrent-timer.rb
301
- - examples/core/xx-resource_cancel.rb
302
301
  - examples/core/xx-resource_delegate.rb
303
302
  - examples/core/xx-signals.rb
304
303
  - examples/core/xx-sleep-forever.rb
@@ -383,7 +382,6 @@ files:
383
382
  - lib/polyphony/adapters/postgres.rb
384
383
  - lib/polyphony/adapters/redis.rb
385
384
  - lib/polyphony/adapters/trace.rb
386
- - lib/polyphony/core/cancel_scope.rb
387
385
  - lib/polyphony/core/channel.rb
388
386
  - lib/polyphony/core/exceptions.rb
389
387
  - lib/polyphony/core/global_api.rb
@@ -405,7 +403,6 @@ files:
405
403
  - test/helper.rb
406
404
  - test/run.rb
407
405
  - test/test_async.rb
408
- - test/test_cancel_scope.rb
409
406
  - test/test_ext.rb
410
407
  - test/test_fiber.rb
411
408
  - test/test_global_api.rb
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
- require 'polyphony'
5
-
6
- resource_count = 0
7
- Pool = Polyphony::ResourcePool.new(limit: 3) do
8
- +"resource#{resource_count += 1}"
9
- end
10
-
11
- def user(number)
12
- loop do
13
- Polyphony::CancelScope.new(timeout: 0.2) do |scope|
14
- scope.on_cancel { puts "#{number} (cancelled)" }
15
- Pool.acquire do |r|
16
- puts "#{number} #{r.inspect} >"
17
- sleep(0.1 + rand * 0.2)
18
- puts "#{number} #{r.inspect} <"
19
- end
20
- end
21
- end
22
- end
23
-
24
- 10.times do |x|
25
- spin { user(x + 1) }
26
- end
27
-
28
- t0 = Time.now
29
- throttled_loop(0.1) { puts "uptime: #{Time.now - t0}" }
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- export_default :CancelScope
4
-
5
- require 'fiber'
6
-
7
- Exceptions = import('./exceptions')
8
-
9
- # A cancellation scope that can be used to cancel an asynchronous task
10
- class CancelScope
11
- def initialize(opts = {}, &block)
12
- @opts = opts
13
- @fibers = []
14
- start_timeout_waiter if @opts[:timeout]
15
- call(&block) if block
16
- end
17
-
18
- def error_class
19
- @opts[:mode] == :cancel ? Exceptions::Cancel : Exceptions::MoveOn
20
- end
21
-
22
- def cancel!
23
- @cancelled = true
24
- @fibers.each do |f|
25
- f.schedule error_class.new(self, @opts[:value])
26
- end
27
- @on_cancel&.()
28
- end
29
-
30
- def start_timeout_waiter
31
- @timeout_waiter = spin do
32
- sleep @opts[:timeout]
33
- @timeout_waiter = nil
34
- cancel!
35
- end
36
- end
37
-
38
- def stop_timeout_waiter
39
- return unless @timeout_waiter
40
-
41
- @timeout_waiter.stop
42
- @timeout_waiter = nil
43
- end
44
-
45
- def reset_timeout
46
- return unless @timeout_waiter
47
-
48
- @timeout_waiter.stop
49
- start_timeout_waiter
50
- end
51
-
52
- # def disable
53
- # @timeout&.stop
54
- # end
55
-
56
- def call
57
- fiber = Fiber.current
58
- @fibers << fiber
59
- yield self
60
- rescue Exceptions::MoveOn => e
61
- e.scope == self ? e.value : raise(e)
62
- ensure
63
- @fibers.delete fiber
64
- stop_timeout_waiter if @fibers.empty? && @timeout_waiter
65
- end
66
-
67
- def on_cancel(&block)
68
- @on_cancel = block
69
- end
70
-
71
- def cancelled?
72
- @cancelled
73
- end
74
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'helper'
4
-
5
- class CancelScopeTest < MiniTest::Test
6
- def test_that_cancel_scope_can_cancel_provided_block
7
- buffer = []
8
- Polyphony::CancelScope.new { |scope|
9
- spin { scope.cancel! }
10
- buffer << 1
11
- snooze
12
- buffer << 2
13
- }
14
- assert_equal [1], buffer
15
- end
16
-
17
- def test_that_cancel_scope_can_cancel_multiple_fibers
18
- buffer = []
19
- scope = Polyphony::CancelScope.new
20
- fibers = (1..3).map { |i|
21
- spin {
22
- scope.call do
23
- buffer << i
24
- snooze
25
- buffer << i * 10
26
- end
27
- }
28
- }
29
- snooze
30
- scope.cancel!
31
- assert_equal [1, 2, 3], buffer
32
- end
33
-
34
- def test_that_cancel_scope_takes_timeout_option
35
- buffer = []
36
- Polyphony::CancelScope.new(timeout: 0.01) { |scope|
37
- buffer << 1
38
- sleep 0.02
39
- buffer << 2
40
- }
41
- assert_equal [1], buffer
42
- end
43
-
44
- def test_that_cancel_scope_cancels_timeout_waiter_if_block_provided
45
- buffer = []
46
- t0 = Time.now
47
- scope = Polyphony::CancelScope.new(timeout: 1) { |scope|
48
- buffer << 1
49
- }
50
- assert_equal [1], buffer
51
- assert Time.now - t0 < 1
52
- assert_nil scope.instance_variable_get(:@timeout_waiter)
53
- end
54
-
55
- def test_that_cancel_scope_can_cancel_multiple_fibers_with_timeout
56
- buffer = []
57
- t0 = Time.now
58
- scope = Polyphony::CancelScope.new(timeout: 0.02)
59
- fibers = (1..3).map { |i|
60
- spin {
61
- scope.call do
62
- buffer << i
63
- sleep i
64
- buffer << i * 10
65
- end
66
- }
67
- }
68
- Fiber.await(*fibers)
69
- assert Time.now - t0 < 0.05
70
- assert_equal [1, 2, 3], buffer
71
- end
72
-
73
- def test_reset_timeout
74
- buffer = []
75
- scope = Polyphony::CancelScope.new(timeout: 0.01)
76
- t0 = Time.now
77
- scope.call {
78
- sleep 0.005
79
- scope.reset_timeout
80
- sleep 0.005
81
- }
82
-
83
- assert !scope.cancelled?
84
- end
85
-
86
- def test_on_cancel
87
- buffer = []
88
- Polyphony::CancelScope.new { |scope|
89
- spin { scope.cancel! }
90
- scope.on_cancel { buffer << :cancelled }
91
- buffer << 1
92
- snooze
93
- buffer << 2
94
- }
95
- assert_equal [1, :cancelled], buffer
96
- end
97
-
98
- def test_cancelled?
99
- scope = Polyphony::CancelScope.new
100
- spin {
101
- scope.call { sleep 1 }
102
- }
103
-
104
- snooze
105
- assert !scope.cancelled?
106
- scope.cancel!
107
- assert scope.cancelled?
108
- end
109
- end