polyphony 0.31 → 0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
|