polyphony 0.31 → 0.32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +63 -64
- data/docs/main-concepts/exception-handling.md +50 -5
- data/ext/gyro/queue.c +9 -9
- data/ext/gyro/thread.c +5 -3
- data/lib/polyphony.rb +2 -4
- data/lib/polyphony/adapters/irb.rb +3 -2
- data/lib/polyphony/core/exceptions.rb +9 -10
- data/lib/polyphony/core/global_api.rb +13 -3
- data/lib/polyphony/extensions/fiber.rb +9 -5
- data/lib/polyphony/version.rb +1 -1
- data/test/test_fiber.rb +48 -0
- data/test/test_global_api.rb +17 -86
- data/test/test_signal.rb +33 -6
- metadata +2 -5
- data/examples/core/xx-resource_cancel.rb +0 -29
- data/lib/polyphony/core/cancel_scope.rb +0 -74
- data/test/test_cancel_scope.rb +0 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75e18e7719d577fc1f07679874d2c3afe23b5a6b6184c1c6723006d0f9fcdf80
|
4
|
+
data.tar.gz: 347608163b6d0ec934b2a70d1b28ffd1c0056b3da51f242f6619ac4a8e94a3ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41cfdd7550a444c590b08eff7c2bf716ce42a19250074bca0425334be4b32b96efba4d60bbe9b1257cf3b8616be31da940695709a25af8c5d03c9803dff8fb80
|
7
|
+
data.tar.gz: 3a12af73aa84d4c3ae9e5c94c0dd55f99bd7eaf3ca49e1f59ab4b80d224dfe1dc068c68f4f8e09ab3d0bcc4996f61ae8d03c42158912f24458ad0ed1e09e6b2a
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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
|
data/Gemfile.lock
CHANGED
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
|
-
-
|
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
|
16
|
+
## 0.33 Sinatra / Sidekiq
|
70
17
|
|
71
|
-
|
18
|
+
- sintra app with database access (postgresql)
|
72
19
|
|
73
|
-
-
|
74
|
-
-
|
75
|
-
-
|
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
|
-
|
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
|
213
|
-
|
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
|
-
|
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
|
```
|
data/ext/gyro/queue.c
CHANGED
@@ -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
|
-
|
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
|
-
|
73
|
-
|
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) {
|
data/ext/gyro/thread.c
CHANGED
@@ -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
|
-
|
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,
|
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",
|
data/lib/polyphony.rb
CHANGED
@@ -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 :
|
3
|
+
export :MoveOn, :Cancel, :Terminate
|
4
4
|
|
5
5
|
# Common exception class for interrupting fibers. These exceptions allow
|
6
|
-
# control of fibers.
|
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.
|
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
|
12
|
-
attr_reader :
|
11
|
+
class BaseException < ::Exception
|
12
|
+
attr_reader :value
|
13
13
|
|
14
|
-
def initialize(
|
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 <
|
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 <
|
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 <
|
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
|
-
|
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(
|
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(
|
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 =
|
193
|
+
@parent = parent
|
190
194
|
@caller = caller
|
191
195
|
@block = block
|
192
196
|
@mailbox = Gyro::Queue.new
|
data/lib/polyphony/version.rb
CHANGED
data/test/test_fiber.rb
CHANGED
@@ -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
|
data/test/test_global_api.rb
CHANGED
@@ -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
|
data/test/test_signal.rb
CHANGED
@@ -38,7 +38,7 @@ class SignalTest < MiniTest::Test
|
|
38
38
|
end
|
39
39
|
|
40
40
|
class SignalTrapTest < Minitest::Test
|
41
|
-
def
|
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 "
|
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 "
|
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.
|
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-
|
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
|
data/test/test_cancel_scope.rb
DELETED
@@ -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
|