polyphony 0.19 → 0.20
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/.gitignore +1 -1
- data/.rubocop.yml +87 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile.lock +17 -6
- data/README.md +200 -139
- data/Rakefile +4 -4
- data/TODO.md +35 -7
- data/bin/poly +11 -0
- data/docs/getting-started/getting-started.md +1 -1
- data/docs/summary.md +3 -0
- data/docs/technical-overview/exception-handling.md +94 -0
- data/docs/technical-overview/fiber-scheduling.md +99 -0
- data/examples/core/cancel.rb +8 -4
- data/examples/core/channel_echo.rb +18 -17
- data/examples/core/defer.rb +12 -0
- data/examples/core/enumerator.rb +4 -4
- data/examples/core/fiber_error.rb +9 -0
- data/examples/core/fiber_error_with_backtrace.rb +73 -0
- data/examples/core/fork.rb +6 -6
- data/examples/core/genserver.rb +16 -8
- data/examples/core/lock.rb +3 -3
- data/examples/core/move_on.rb +4 -3
- data/examples/core/move_on_twice.rb +5 -5
- data/examples/core/move_on_with_ensure.rb +8 -11
- data/examples/core/move_on_with_value.rb +14 -0
- data/examples/core/{multiple_spawn.rb → multiple_spin.rb} +5 -5
- data/examples/core/nested_cancel.rb +5 -5
- data/examples/core/{nested_multiple_spawn.rb → nested_multiple_spin.rb} +6 -6
- data/examples/core/nested_spin.rb +17 -0
- data/examples/core/pingpong.rb +21 -0
- data/examples/core/pulse.rb +4 -5
- data/examples/core/resource.rb +6 -4
- data/examples/core/resource_cancel.rb +6 -9
- data/examples/core/resource_delegate.rb +3 -3
- data/examples/core/sleep.rb +3 -3
- data/examples/core/sleep_spin.rb +19 -0
- data/examples/core/snooze.rb +32 -0
- data/examples/core/spin.rb +14 -0
- data/examples/core/{spawn_cancel.rb → spin_cancel.rb} +6 -7
- data/examples/core/spin_error.rb +17 -0
- data/examples/core/spin_error_backtrace.rb +30 -0
- data/examples/core/spin_uncaught_error.rb +15 -0
- data/examples/core/supervisor.rb +8 -8
- data/examples/core/supervisor_with_cancel_scope.rb +7 -7
- data/examples/core/supervisor_with_error.rb +8 -8
- data/examples/core/supervisor_with_manual_move_on.rb +6 -7
- data/examples/core/suspend.rb +13 -0
- data/examples/core/thread.rb +1 -1
- data/examples/core/thread_cancel.rb +9 -11
- data/examples/core/thread_pool.rb +18 -14
- data/examples/core/throttle.rb +7 -7
- data/examples/core/timeout.rb +3 -3
- data/examples/fs/read.rb +7 -9
- data/examples/http/config.ru +7 -3
- data/examples/http/cuba.ru +22 -0
- data/examples/http/happy_eyeballs.rb +6 -4
- data/examples/http/http_client.rb +1 -1
- data/examples/http/http_get.rb +1 -1
- data/examples/http/http_parse_experiment.rb +21 -16
- data/examples/http/http_proxy.rb +28 -26
- data/examples/http/http_server.rb +10 -10
- data/examples/http/http_server_forked.rb +6 -5
- data/examples/http/http_server_throttled.rb +3 -3
- data/examples/http/http_ws_server.rb +11 -11
- data/examples/http/https_raw_client.rb +1 -1
- data/examples/http/https_server.rb +8 -8
- data/examples/http/https_wss_server.rb +13 -11
- data/examples/http/rack_server.rb +2 -2
- data/examples/http/rack_server_https.rb +4 -4
- data/examples/http/rack_server_https_forked.rb +5 -5
- data/examples/http/websocket_secure_server.rb +6 -6
- data/examples/http/websocket_server.rb +5 -5
- data/examples/interfaces/pg_client.rb +4 -4
- data/examples/interfaces/pg_pool.rb +13 -6
- data/examples/interfaces/pg_transaction.rb +5 -4
- data/examples/interfaces/redis_channels.rb +15 -11
- data/examples/interfaces/redis_client.rb +2 -2
- data/examples/interfaces/redis_pubsub.rb +2 -1
- data/examples/interfaces/redis_pubsub_perf.rb +13 -9
- data/examples/io/backticks.rb +11 -0
- data/examples/io/cat.rb +4 -5
- data/examples/io/echo_client.rb +9 -4
- data/examples/io/echo_client_from_stdin.rb +20 -0
- data/examples/io/echo_pipe.rb +7 -8
- data/examples/io/echo_server.rb +8 -6
- data/examples/io/echo_server_with_timeout.rb +13 -10
- data/examples/io/echo_stdin.rb +3 -3
- data/examples/io/httparty.rb +2 -2
- data/examples/io/httparty_multi.rb +8 -4
- data/examples/io/httparty_threaded.rb +6 -2
- data/examples/io/io_read.rb +2 -2
- data/examples/io/irb.rb +16 -4
- data/examples/io/net-http.rb +3 -3
- data/examples/io/open.rb +17 -0
- data/examples/io/system.rb +3 -3
- data/examples/io/tcpserver.rb +15 -0
- data/examples/io/tcpsocket.rb +6 -5
- data/examples/performance/multi_snooze.rb +29 -0
- data/examples/performance/{perf_snooze.rb → snooze.rb} +7 -5
- data/examples/performance/snooze_raw.rb +39 -0
- data/ext/gyro/async.c +165 -0
- data/ext/gyro/child.c +167 -0
- data/ext/{ev → gyro}/extconf.rb +4 -3
- data/ext/gyro/gyro.c +316 -0
- data/ext/{ev/ev.h → gyro/gyro.h} +12 -7
- data/ext/gyro/gyro_ext.c +23 -0
- data/ext/{ev → gyro}/io.c +65 -57
- data/ext/{ev → gyro}/libev.h +0 -0
- data/ext/gyro/signal.c +117 -0
- data/ext/{ev → gyro}/socket.c +61 -6
- data/ext/gyro/timer.c +199 -0
- data/ext/libev/Changes +35 -0
- data/ext/libev/README +2 -1
- data/ext/libev/ev.c +213 -151
- data/ext/libev/ev.h +95 -88
- data/ext/libev/ev_epoll.c +26 -15
- data/ext/libev/ev_kqueue.c +11 -5
- data/ext/libev/ev_linuxaio.c +642 -0
- data/ext/libev/ev_poll.c +13 -8
- data/ext/libev/ev_port.c +5 -2
- data/ext/libev/ev_vars.h +14 -3
- data/ext/libev/ev_wrap.h +16 -0
- data/lib/ev_ext.bundle +0 -0
- data/lib/polyphony.rb +46 -50
- data/lib/polyphony/auto_run.rb +12 -0
- data/lib/polyphony/core/cancel_scope.rb +11 -7
- data/lib/polyphony/core/channel.rb +16 -9
- data/lib/polyphony/core/coprocess.rb +101 -51
- data/lib/polyphony/core/exceptions.rb +14 -12
- data/lib/polyphony/core/resource_pool.rb +21 -8
- data/lib/polyphony/core/supervisor.rb +10 -5
- data/lib/polyphony/core/sync.rb +7 -6
- data/lib/polyphony/core/thread.rb +4 -4
- data/lib/polyphony/core/thread_pool.rb +4 -4
- data/lib/polyphony/core/throttler.rb +6 -4
- data/lib/polyphony/extensions/core.rb +253 -0
- data/lib/polyphony/extensions/io.rb +28 -16
- data/lib/polyphony/extensions/openssl.rb +2 -1
- data/lib/polyphony/extensions/socket.rb +47 -52
- data/lib/polyphony/http.rb +4 -3
- data/lib/polyphony/http/agent.rb +68 -57
- data/lib/polyphony/http/server.rb +5 -5
- data/lib/polyphony/http/server/http1.rb +268 -0
- data/lib/polyphony/http/server/http2.rb +62 -0
- data/lib/polyphony/http/server/http2_stream.rb +104 -0
- data/lib/polyphony/http/server/rack.rb +64 -0
- data/lib/polyphony/http/server/request.rb +119 -0
- data/lib/polyphony/net.rb +26 -15
- data/lib/polyphony/postgres.rb +17 -13
- data/lib/polyphony/redis.rb +16 -15
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony/websocket.rb +11 -4
- data/polyphony.gemspec +13 -9
- data/test/eg.rb +27 -0
- data/test/helper.rb +25 -0
- data/test/run.rb +5 -0
- data/test/test_async.rb +33 -0
- data/test/test_coprocess.rb +239 -77
- data/test/test_core.rb +95 -61
- data/test/test_gyro.rb +148 -0
- data/test/test_http_server.rb +313 -0
- data/test/test_io.rb +79 -27
- data/test/test_kernel.rb +22 -12
- data/test/test_signal.rb +36 -0
- data/test/test_timer.rb +24 -0
- metadata +89 -33
- data/examples/core/nested_async.rb +0 -17
- data/examples/core/next_tick.rb +0 -12
- data/examples/core/sleep_spawn.rb +0 -19
- data/examples/core/spawn.rb +0 -14
- data/examples/core/spawn_error.rb +0 -28
- data/examples/performance/perf_multi_snooze.rb +0 -21
- data/ext/ev/async.c +0 -168
- data/ext/ev/child.c +0 -169
- data/ext/ev/ev_ext.c +0 -23
- data/ext/ev/ev_module.c +0 -242
- data/ext/ev/signal.c +0 -119
- data/ext/ev/timer.c +0 -197
- data/lib/polyphony/core/fiber_pool.rb +0 -98
- data/lib/polyphony/extensions/kernel.rb +0 -169
- data/lib/polyphony/http/http1_adapter.rb +0 -254
- data/lib/polyphony/http/http2_adapter.rb +0 -157
- data/lib/polyphony/http/rack.rb +0 -25
- data/lib/polyphony/http/request.rb +0 -66
- data/test/test_ev.rb +0 -110
data/Rakefile
CHANGED
@@ -6,15 +6,15 @@ require "rake/clean"
|
|
6
6
|
# frozen_string_literal: true
|
7
7
|
|
8
8
|
require "rake/extensiontask"
|
9
|
-
Rake::ExtensionTask.new("
|
10
|
-
ext.ext_dir = "ext/
|
9
|
+
Rake::ExtensionTask.new("gyro_ext") do |ext|
|
10
|
+
ext.ext_dir = "ext/gyro"
|
11
11
|
end
|
12
12
|
|
13
13
|
task :default => [:compile, :test]
|
14
14
|
task :test do
|
15
|
-
|
15
|
+
exec 'ruby test/run.rb'
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
task default: %w[compile]
|
19
19
|
|
20
20
|
CLEAN.include "**/*.o", "**/*.so", "**/*.bundle", "**/*.jar", "pkg", "tmp"
|
data/TODO.md
CHANGED
@@ -1,26 +1,53 @@
|
|
1
1
|
# Roadmap:
|
2
2
|
|
3
|
-
## 0.20
|
3
|
+
## 0.20.1
|
4
|
+
|
5
|
+
- Cull, edit and annotate examples
|
6
|
+
- Work better mechanism supervising multiple coprocesses (`when_done` feels a
|
7
|
+
bit hacky)
|
8
|
+
|
9
|
+
## 0.21 REPL usage, coprocess introspection, monitoring
|
10
|
+
|
11
|
+
- Implement `move_on_after(1, with: nil) { ... }`
|
12
|
+
- Implement `Coprocess.await` for waiting on multiple coprocesses without
|
13
|
+
starting them in a supervisor, will also necessitate adding `Supervisor#add`
|
14
|
+
- Implement `Coprocess#location`
|
15
|
+
- Implement `Coprocess#alive?`
|
16
|
+
- Implement `Coprocess#caller` - points to coprocess that called the coprocess
|
17
|
+
- Implement `Coprocess.list` - a list of running coprocesses
|
18
|
+
|
19
|
+
## 0.22 Full Rack adapter implementation
|
20
|
+
|
21
|
+
- Homogenize HTTP 1 and HTTP 2 headers - upcase ? downcase ?
|
22
|
+
- Rewrite agent code to use sequential API (like I did for server)
|
23
|
+
- Streaming bodies for HTTP client
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
def download_doc
|
27
|
+
response = Polyphony::HTTP::Agent.get('https://acme.com/doc.pdf')
|
28
|
+
File.open('doc.pdf', 'wb+') do |f|
|
29
|
+
response.each { |chunk| f << chunk } # streaming body
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
4
33
|
|
5
|
-
- follow Rack specification (doesn't have to include stuff like streaming or
|
6
|
-
websockets)
|
7
34
|
- find some demo Rack apps and test with Polyphony
|
8
35
|
|
9
|
-
## 0.
|
36
|
+
## 0.23 Working Sinatra application
|
10
37
|
|
11
38
|
- app with database access (postgresql)
|
12
39
|
- benchmarks!
|
13
40
|
|
14
|
-
## 0.
|
41
|
+
## 0.24 Support for multi-threading
|
15
42
|
|
16
43
|
- Separate event loop for each thread
|
17
44
|
|
18
|
-
## 0.
|
45
|
+
## 0.25 Testing
|
19
46
|
|
20
47
|
- test thread / thread_pool modules
|
21
48
|
- report test coverage
|
22
49
|
|
23
|
-
## 0.
|
50
|
+
## 0.26 Documentation
|
24
51
|
|
25
52
|
# DNS
|
26
53
|
|
@@ -56,3 +83,4 @@ puts "listening on port 5300"
|
|
56
83
|
Prior art:
|
57
84
|
|
58
85
|
- https://github.com/socketry/async-dns
|
86
|
+
|
data/bin/poly
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative('../lib/polyphony/http')
|
4
|
+
|
5
|
+
app_path = ARGV.first || './config.ru'
|
6
|
+
app = Polyphony::HTTP::Rack.load(app_path)
|
7
|
+
opts = { reuse_addr: true, dont_linger: true }
|
8
|
+
|
9
|
+
puts "listening on port 1234"
|
10
|
+
puts "pid: #{Process.pid}"
|
11
|
+
Polyphony::HTTP::Server.serve('0.0.0.0', 1234, opts, &app)
|
data/docs/summary.md
CHANGED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Exception Handling in a Multi-Fiber Environment
|
2
|
+
|
3
|
+
Ruby employs a pretty robust exception handling mechanism. An raised exception
|
4
|
+
will bubble up the call stack until a suitable exception handler is found, based
|
5
|
+
on the exception's class. In addition, the exception will include a stack trace
|
6
|
+
showing the execution path from the exception's locus back to the program's
|
7
|
+
entry point. Unfortunately, when exceptions are raised while switching between
|
8
|
+
fibers, stack traces will only include partial information. Here's a simple
|
9
|
+
demonstration:
|
10
|
+
|
11
|
+
*fiber_exception.rb*
|
12
|
+
```ruby
|
13
|
+
require 'fiber'
|
14
|
+
|
15
|
+
def fail!
|
16
|
+
raise 'foobar'
|
17
|
+
end
|
18
|
+
|
19
|
+
f = Fiber.new do
|
20
|
+
Fiber.new do
|
21
|
+
fail!
|
22
|
+
end.transfer
|
23
|
+
end
|
24
|
+
|
25
|
+
f.transfer
|
26
|
+
```
|
27
|
+
|
28
|
+
Running the above program will give us:
|
29
|
+
|
30
|
+
```
|
31
|
+
Traceback (most recent call last):
|
32
|
+
1: from fiber_exception.rb:9:in `block (2 levels) in <main>'
|
33
|
+
fiber_exception.rb:4:in `fail!': foobar (RuntimeError)
|
34
|
+
```
|
35
|
+
|
36
|
+
So, the stack trace includes two frames: the exception's locus on line 4 and the
|
37
|
+
call site at line 9. But we have no information on how we got to line 9. Let's
|
38
|
+
imagine if we had more complete information about the sequence of execution. In
|
39
|
+
fact, what is missing is information about how the different fibers were
|
40
|
+
created. If we had that, our stack trace would have looked something like this:
|
41
|
+
|
42
|
+
```
|
43
|
+
Traceback (most recent call last):
|
44
|
+
4: from fiber_exception.rb:13:in `<main>'
|
45
|
+
3: from fiber_exception.rb:7:in `Fiber.new'
|
46
|
+
2: from fiber_exception.rb:8:in `Fiber.new'
|
47
|
+
1: from fiber_exception.rb:9:in `block (2 levels) in <main>'
|
48
|
+
fiber_exception.rb:4:in `fail!': foobar (RuntimeError)
|
49
|
+
```
|
50
|
+
|
51
|
+
In order to achieve this, Polyphony patches `Fiber.new` to keep track of the
|
52
|
+
call stack at the moment the fiber was created, as well as the fiber from which
|
53
|
+
the call happened. In addition, Polyphony patches `Exception#backtrace` in order
|
54
|
+
to synthesize a complete stack trace based on the call stack information stored
|
55
|
+
for the current fiber. This is done recursively through the chain of fibers
|
56
|
+
leading up to the current location. What we end up with is a record of the
|
57
|
+
entire sequence of (possibly intermittent) execution leading up to the point
|
58
|
+
where the exception was raised.
|
59
|
+
|
60
|
+
In addition, the backtrace is sanitized to remove stack frames originating from
|
61
|
+
the Polyphony code itself, which hides away the Polyphony plumbing and lets
|
62
|
+
developers concentrate on their own code. The sanitizing of exception backtraces
|
63
|
+
can be disabled by setting the `Exception.__disable_sanitized_backtrace__` flag:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Exception.__disable_sanitized_backtrace__ = true
|
67
|
+
...
|
68
|
+
```
|
69
|
+
|
70
|
+
## Cleaning up after exceptions
|
71
|
+
|
72
|
+
A major issue when handling exceptions is cleaning up - freeing up resources
|
73
|
+
that have been allocated, cancelling ongoing operations, etc. Polyphony allows
|
74
|
+
using the normal `ensure` statement for cleaning up. Have a look at Polyphony's
|
75
|
+
implementation of `Kernel#sleep`:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
def sleep(duration)
|
79
|
+
timer = Gyro::Timer.new(duration, 0)
|
80
|
+
timer.await
|
81
|
+
ensure
|
82
|
+
timer.stop
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
This method creates a one-shot timer with the given duration and then suspends
|
87
|
+
the current fiber, waiting for the timer to fire and then resume the fiber.
|
88
|
+
While the awaiting fiber is suspended, other operations might be going on, which
|
89
|
+
might interrupt the `sleep` operation by scheduling the awaiting fiber with an
|
90
|
+
exception, for example a `MoveOn` or a `Cancel` exception. For this reason, we
|
91
|
+
need to *ensure* that the timer will be stopped, regardless of whether it has
|
92
|
+
fired or not. We call `timer.stop` inside an ensure block, thus ensuring that
|
93
|
+
the timer will have stopped once the awaiting fiber has resumed, even if it has
|
94
|
+
not fired.
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# Fiber Scheduling
|
2
|
+
|
3
|
+
Ruby provides two mechanisms for transferring control between fibers:
|
4
|
+
`Fiber#resume` / `Fiber.yield` and `Fiber#transfer`. The first is inherently asymmetric and is famously used
|
5
|
+
for implementing generators and [resumable enumerators](https://blog.appsignal.com/2018/11/27/ruby-magic-fibers-and-enumerators-in-ruby.html).
|
6
|
+
Another limiting factor of using resume / yield is that the root fiber can't
|
7
|
+
yield away, limiting its usability as a resumable fiber.
|
8
|
+
|
9
|
+
The second mechanism, using `Fiber#transfer`, is completely symmetric and allows
|
10
|
+
use of the root fiber as a general purpose resumable execution context.
|
11
|
+
Polyphony uses `Fiber#transfer` exclusively for scheduling fibers.
|
12
|
+
|
13
|
+
## The Reactor Fiber
|
14
|
+
|
15
|
+
Polyphony relies on [libev](http://software.schmorp.de/pkg/libev.html) for
|
16
|
+
handling events such as I/O readiness, timers and signals. The libev event loop
|
17
|
+
runs in a separate fiber (called the reactor fiber) and handles each event by
|
18
|
+
resuming the appropriate fiber using `Fiber#transfer`. The event loop will
|
19
|
+
continue running and scheduling fibers as long as there's active event watchers.
|
20
|
+
When all event watchers have been deactivated, the event loop terminates and
|
21
|
+
control is transferred to the root fiber, which will either terminate the
|
22
|
+
program, or go on with other work, and possibly another run of the event loop.
|
23
|
+
|
24
|
+
## Fiber scheduling
|
25
|
+
|
26
|
+
When a new fiber is created, it is in a suspended state. To start it, it needs
|
27
|
+
to be resumed using `Fiber#transfer`. Upon performing a blocking operation, such
|
28
|
+
as `sleep` or `gets`, an event watcher will be created, and control will be
|
29
|
+
transferred to the reactor fiber, which will resume running the event loop. A
|
30
|
+
fiber waiting for an event will be resumed using `Fiber#transfer` once the event
|
31
|
+
has been received, and will continue execution until encountering another
|
32
|
+
blocking operation, at which point it will again create an event watcher and
|
33
|
+
transfer control back to the reactor fiber.
|
34
|
+
|
35
|
+
## Interrupting blocking operations
|
36
|
+
|
37
|
+
Sometimes it is desirable to be able to interrupt a blocking operation, such as
|
38
|
+
waiting for a socket to be readable, or sleeping for an extended period of time.
|
39
|
+
This is especially useful when higher-level constructs are needed for
|
40
|
+
controlling multiple concurrent operations.
|
41
|
+
|
42
|
+
Polyphony provides the ability to interrupt a blocking operation by harnessing
|
43
|
+
the ability to transfer values back and forth when using `Fiber#transfer`.
|
44
|
+
Whenever a waiting fiber transfers control to the reactor fiber, the value
|
45
|
+
received upon being resumed is checked. If the value is an exception, it will
|
46
|
+
be raised in the context of the waiting fiber, effectively signalling that the
|
47
|
+
blocking operation has been unsuccessful and allowing exception handling using
|
48
|
+
the usual mechanisms offered by Ruby, namely `rescue` and `ensure` (see also
|
49
|
+
[exception handling](exception-handling.md)).
|
50
|
+
|
51
|
+
Here's an siplified example of how this mechanism works when reading from an I/O
|
52
|
+
object (the actual code for I/O reading in Polyphony is written in C and a bit
|
53
|
+
more involved):
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
def read_from(io)
|
57
|
+
loop do
|
58
|
+
result = IO.readnonblock(8192, exception: false)
|
59
|
+
if result == :wait_readable
|
60
|
+
wait_readable(io)
|
61
|
+
else
|
62
|
+
return result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def wait_readable(io)
|
68
|
+
watcher = Gyro::IO.new(io, :read)
|
69
|
+
# transfer control to event loop
|
70
|
+
result = $__reactor_fiber__.transfer
|
71
|
+
# got control back, check if result is an exception
|
72
|
+
raise result if result.is_a?(Exception)
|
73
|
+
result
|
74
|
+
ensure
|
75
|
+
watcher.active = false
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
In the above example, the `wait_readable` method will normally wait indefinitely
|
80
|
+
until the IO object has become readable. But we could interrupt it at any time
|
81
|
+
by scheduling the corresponding fiber with an exception.
|
82
|
+
|
83
|
+
## Deferred Operations
|
84
|
+
|
85
|
+
In addition to waiting for blocking operations, Polyphony provides numerous APIs
|
86
|
+
for suspending and scheduling fibers:
|
87
|
+
|
88
|
+
- `Fiber#safe_transfer(value = nil)` - transfers control to another fiber with
|
89
|
+
exception handling.
|
90
|
+
- `Fiber#schedule(value = nil)` - schedules a fiber to be resumed once the
|
91
|
+
event loop becomes idle.
|
92
|
+
- `Kernel#snooze` - transfers control to the reactor fiber while scheduling the
|
93
|
+
current fiber to be resumed immediately once the event loop is idle.
|
94
|
+
- `Kernel#suspend` - suspends the current fiber indefinitely by transferring
|
95
|
+
control to the reactor fiber.
|
96
|
+
|
97
|
+
In addition, a lower level API allows running arbitrary code in the context of
|
98
|
+
the reactor loop using `Kernel#defer`. Using this API will run the given block
|
99
|
+
the next time the event loop is idle.
|
data/examples/core/cancel.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require 'polyphony'
|
4
|
+
require 'polyphony/auto_run'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
begin
|
7
|
+
puts 'going to sleep...'
|
8
|
+
cancel_after(1) do
|
9
|
+
sleep(60)
|
10
|
+
end
|
11
|
+
rescue Polyphony::Cancel
|
12
|
+
puts 'cancelled'
|
9
13
|
end
|
@@ -1,41 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require 'polyphony'
|
4
|
+
require 'polyphony/auto_run'
|
5
5
|
|
6
|
-
def echo(
|
7
|
-
puts
|
8
|
-
while msg =
|
9
|
-
|
6
|
+
def echo(cin, cout)
|
7
|
+
puts 'start echoer'
|
8
|
+
while (msg = cin.receive)
|
9
|
+
cout << "you said: #{msg}"
|
10
10
|
end
|
11
11
|
ensure
|
12
|
-
puts
|
12
|
+
puts 'echoer stopped'
|
13
13
|
end
|
14
14
|
|
15
|
-
chan1, chan2 =
|
15
|
+
chan1, chan2 = 2.times.map { Polyphony::Channel.new }
|
16
16
|
|
17
|
-
|
17
|
+
spin { echo(chan1, chan2) }
|
18
18
|
|
19
19
|
spin do
|
20
|
-
puts
|
21
|
-
while msg = chan2.receive
|
20
|
+
puts 'start receiver'
|
21
|
+
while (msg = chan2.receive)
|
22
22
|
puts msg
|
23
23
|
$main.resume if msg =~ /world/
|
24
24
|
end
|
25
25
|
ensure
|
26
|
-
puts
|
26
|
+
puts 'receiver stopped'
|
27
27
|
end
|
28
28
|
|
29
29
|
$main = spin do
|
30
|
+
puts 'start main'
|
30
31
|
t0 = Time.now
|
31
|
-
puts
|
32
|
-
chan1 <<
|
33
|
-
puts
|
34
|
-
chan1 <<
|
32
|
+
puts 'send hello'
|
33
|
+
chan1 << 'hello'
|
34
|
+
puts 'send world'
|
35
|
+
chan1 << 'world'
|
35
36
|
|
36
37
|
suspend
|
37
|
-
|
38
|
-
puts
|
38
|
+
|
39
|
+
puts 'closing channels'
|
39
40
|
chan1.close
|
40
41
|
chan2.close
|
41
42
|
puts "done #{Time.now - t0}"
|
data/examples/core/enumerator.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require 'polyphony'
|
4
|
+
require 'polyphony/auto_run'
|
5
5
|
|
6
|
-
enum = [1,2,3].each
|
6
|
+
enum = [1, 2, 3].each
|
7
7
|
|
8
8
|
spin do
|
9
|
-
while e = enum.next rescue nil
|
9
|
+
while (e = enum.next rescue nil)
|
10
10
|
puts e
|
11
|
-
sleep 1
|
11
|
+
sleep 0.1
|
12
12
|
end
|
13
13
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
# This is an experiment to see if we could provide better backtraces for
|
6
|
+
# exceptions raised in fibers. Our approach is to monkey-patch Fiber.new so as
|
7
|
+
# to keep track of the caller stack trace and calling fiber. We also
|
8
|
+
# monkey-patch Exception#backtrace to calculate the full backtrace based on the
|
9
|
+
# fiber in which the exception was raised. The benefit of this approach is that
|
10
|
+
# there's no need to sanitize the backtrace (remove stack frames related to the
|
11
|
+
# backtrace calculation).
|
12
|
+
class Fiber
|
13
|
+
attr_writer :__calling_fiber__, :__caller__
|
14
|
+
|
15
|
+
class << self
|
16
|
+
alias_method :orig_new, :new
|
17
|
+
def new(&block)
|
18
|
+
calling_fiber = Fiber.current
|
19
|
+
fiber_caller = caller
|
20
|
+
orig_new(&block).tap do |f|
|
21
|
+
f.__calling_fiber__ = calling_fiber
|
22
|
+
f.__caller__ = fiber_caller
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def caller
|
28
|
+
@__caller__ ||= []
|
29
|
+
if @__calling_fiber__
|
30
|
+
@__caller__ + @__calling_fiber__.caller
|
31
|
+
else
|
32
|
+
@__caller__
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Exception
|
38
|
+
alias_method :orig_initialize, :initialize
|
39
|
+
|
40
|
+
def initialize(*args)
|
41
|
+
@__raising_fiber__ = Fiber.current
|
42
|
+
orig_initialize(*args)
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :orig_backtrace, :backtrace
|
46
|
+
def backtrace
|
47
|
+
unless @backtrace_called
|
48
|
+
@backtrace_called = true
|
49
|
+
return orig_backtrace
|
50
|
+
end
|
51
|
+
|
52
|
+
if @__raising_fiber__
|
53
|
+
backtrace = orig_backtrace || []
|
54
|
+
backtrace + @__raising_fiber__.caller
|
55
|
+
else
|
56
|
+
orig_backtrace
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def foo
|
62
|
+
Fiber.new do
|
63
|
+
bar
|
64
|
+
end.resume
|
65
|
+
end
|
66
|
+
|
67
|
+
def bar
|
68
|
+
Fiber.new do
|
69
|
+
raise 'baz'
|
70
|
+
end.resume
|
71
|
+
end
|
72
|
+
|
73
|
+
foo
|