polyphony 0.43.8
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 +7 -0
- data/.gitbook.yaml +4 -0
- data/.github/workflows/test.yml +29 -0
- data/.gitignore +59 -0
- data/.rubocop.yml +175 -0
- data/CHANGELOG.md +393 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +141 -0
- data/LICENSE +21 -0
- data/README.md +51 -0
- data/Rakefile +26 -0
- data/TODO.md +201 -0
- data/bin/polyphony-debug +87 -0
- data/docs/_config.yml +64 -0
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/title.html +1 -0
- data/docs/_sass/custom/custom.scss +10 -0
- data/docs/_sass/overrides.scss +0 -0
- data/docs/_user-guide/all-about-timers.md +126 -0
- data/docs/_user-guide/index.md +9 -0
- data/docs/_user-guide/web-server.md +136 -0
- data/docs/api-reference/exception.md +27 -0
- data/docs/api-reference/fiber.md +425 -0
- data/docs/api-reference/index.md +9 -0
- data/docs/api-reference/io.md +36 -0
- data/docs/api-reference/object.md +99 -0
- data/docs/api-reference/polyphony-baseexception.md +33 -0
- data/docs/api-reference/polyphony-cancel.md +26 -0
- data/docs/api-reference/polyphony-moveon.md +24 -0
- data/docs/api-reference/polyphony-net.md +20 -0
- data/docs/api-reference/polyphony-process.md +28 -0
- data/docs/api-reference/polyphony-resourcepool.md +59 -0
- data/docs/api-reference/polyphony-restart.md +18 -0
- data/docs/api-reference/polyphony-terminate.md +18 -0
- data/docs/api-reference/polyphony-threadpool.md +67 -0
- data/docs/api-reference/polyphony-throttler.md +77 -0
- data/docs/api-reference/polyphony.md +36 -0
- data/docs/api-reference/thread.md +88 -0
- data/docs/assets/img/echo-fibers.svg +1 -0
- data/docs/assets/img/sleeping-fiber.svg +1 -0
- data/docs/faq.md +195 -0
- data/docs/favicon.ico +0 -0
- data/docs/getting-started/index.md +10 -0
- data/docs/getting-started/installing.md +34 -0
- data/docs/getting-started/overview.md +486 -0
- data/docs/getting-started/tutorial.md +359 -0
- data/docs/index.md +94 -0
- data/docs/main-concepts/concurrency.md +151 -0
- data/docs/main-concepts/design-principles.md +161 -0
- data/docs/main-concepts/exception-handling.md +291 -0
- data/docs/main-concepts/extending.md +89 -0
- data/docs/main-concepts/fiber-scheduling.md +197 -0
- data/docs/main-concepts/index.md +9 -0
- data/docs/polyphony-logo.png +0 -0
- data/examples/adapters/concurrent-ruby.rb +9 -0
- data/examples/adapters/pg_client.rb +36 -0
- data/examples/adapters/pg_notify.rb +35 -0
- data/examples/adapters/pg_pool.rb +43 -0
- data/examples/adapters/pg_transaction.rb +31 -0
- data/examples/adapters/redis_blpop.rb +12 -0
- data/examples/adapters/redis_channels.rb +122 -0
- data/examples/adapters/redis_client.rb +19 -0
- data/examples/adapters/redis_pubsub.rb +26 -0
- data/examples/adapters/redis_pubsub_perf.rb +68 -0
- data/examples/core/01-spinning-up-fibers.rb +18 -0
- data/examples/core/02-awaiting-fibers.rb +20 -0
- data/examples/core/03-interrupting.rb +39 -0
- data/examples/core/04-handling-signals.rb +19 -0
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-at_exit.rb +29 -0
- data/examples/core/xx-caller.rb +12 -0
- data/examples/core/xx-channels.rb +45 -0
- data/examples/core/xx-daemon.rb +14 -0
- data/examples/core/xx-deadlock.rb +8 -0
- data/examples/core/xx-deferring-an-operation.rb +14 -0
- data/examples/core/xx-erlang-style-genserver.rb +81 -0
- data/examples/core/xx-exception-backtrace.rb +40 -0
- data/examples/core/xx-fork-cleanup.rb +22 -0
- data/examples/core/xx-fork-spin.rb +42 -0
- data/examples/core/xx-fork-terminate.rb +27 -0
- data/examples/core/xx-forking.rb +24 -0
- data/examples/core/xx-move_on.rb +23 -0
- data/examples/core/xx-pingpong.rb +18 -0
- data/examples/core/xx-queue-async.rb +120 -0
- data/examples/core/xx-readpartial.rb +18 -0
- data/examples/core/xx-recurrent-timer.rb +12 -0
- data/examples/core/xx-resource_delegate.rb +31 -0
- data/examples/core/xx-signals.rb +16 -0
- data/examples/core/xx-sleep-forever.rb +9 -0
- data/examples/core/xx-sleeping.rb +25 -0
- data/examples/core/xx-snooze-starve.rb +16 -0
- data/examples/core/xx-spin-fork.rb +49 -0
- data/examples/core/xx-spin_error_backtrace.rb +33 -0
- data/examples/core/xx-state-machine.rb +51 -0
- data/examples/core/xx-stop.rb +20 -0
- data/examples/core/xx-supervise-process.rb +30 -0
- data/examples/core/xx-supervisors.rb +21 -0
- data/examples/core/xx-thread-selector-sleep.rb +51 -0
- data/examples/core/xx-thread-selector-snooze.rb +46 -0
- data/examples/core/xx-thread-sleep.rb +17 -0
- data/examples/core/xx-thread-snooze.rb +34 -0
- data/examples/core/xx-thread_pool.rb +17 -0
- data/examples/core/xx-throttling.rb +18 -0
- data/examples/core/xx-timeout.rb +10 -0
- data/examples/core/xx-timer-gc.rb +17 -0
- data/examples/core/xx-trace.rb +79 -0
- data/examples/core/xx-using-a-mutex.rb +21 -0
- data/examples/core/xx-worker-thread.rb +30 -0
- data/examples/io/tunnel.rb +48 -0
- data/examples/io/xx-backticks.rb +11 -0
- data/examples/io/xx-echo_client.rb +25 -0
- data/examples/io/xx-echo_client_from_stdin.rb +21 -0
- data/examples/io/xx-echo_pipe.rb +16 -0
- data/examples/io/xx-echo_server.rb +17 -0
- data/examples/io/xx-echo_server_with_timeout.rb +34 -0
- data/examples/io/xx-echo_stdin.rb +14 -0
- data/examples/io/xx-happy-eyeballs.rb +36 -0
- data/examples/io/xx-httparty.rb +38 -0
- data/examples/io/xx-irb.rb +17 -0
- data/examples/io/xx-net-http.rb +15 -0
- data/examples/io/xx-open.rb +16 -0
- data/examples/io/xx-switch.rb +15 -0
- data/examples/io/xx-system.rb +11 -0
- data/examples/io/xx-tcpserver.rb +15 -0
- data/examples/io/xx-tcpsocket.rb +18 -0
- data/examples/io/xx-zip.rb +19 -0
- data/examples/performance/fiber_transfer.rb +47 -0
- data/examples/performance/fs_read.rb +38 -0
- data/examples/performance/mem-usage.rb +56 -0
- data/examples/performance/messaging.rb +29 -0
- data/examples/performance/multi_snooze.rb +33 -0
- data/examples/performance/snooze.rb +39 -0
- data/examples/performance/snooze_raw.rb +39 -0
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +74 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +45 -0
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
- data/examples/performance/thread-vs-fiber/threaded_server.rb +27 -0
- data/examples/performance/thread-vs-fiber/xx-httparty_multi.rb +36 -0
- data/examples/performance/thread-vs-fiber/xx-httparty_threaded.rb +29 -0
- data/examples/performance/thread_pool_perf.rb +63 -0
- data/examples/performance/xx-array.rb +11 -0
- data/examples/performance/xx-fiber-switch.rb +9 -0
- data/examples/performance/xx-snooze.rb +15 -0
- data/examples/xx-spin.rb +32 -0
- data/ext/libev/Changes +548 -0
- data/ext/libev/LICENSE +37 -0
- data/ext/libev/README +59 -0
- data/ext/libev/README.embed +3 -0
- data/ext/libev/ev.c +5279 -0
- data/ext/libev/ev.h +856 -0
- data/ext/libev/ev_epoll.c +296 -0
- data/ext/libev/ev_kqueue.c +224 -0
- data/ext/libev/ev_linuxaio.c +642 -0
- data/ext/libev/ev_poll.c +156 -0
- data/ext/libev/ev_port.c +192 -0
- data/ext/libev/ev_select.c +316 -0
- data/ext/libev/ev_vars.h +215 -0
- data/ext/libev/ev_win32.c +162 -0
- data/ext/libev/ev_wrap.h +216 -0
- data/ext/libev/test_libev_win32.c +123 -0
- data/ext/polyphony/extconf.rb +20 -0
- data/ext/polyphony/fiber.c +109 -0
- data/ext/polyphony/libev.c +2 -0
- data/ext/polyphony/libev.h +9 -0
- data/ext/polyphony/libev_agent.c +882 -0
- data/ext/polyphony/polyphony.c +71 -0
- data/ext/polyphony/polyphony.h +97 -0
- data/ext/polyphony/polyphony_ext.c +21 -0
- data/ext/polyphony/queue.c +168 -0
- data/ext/polyphony/ring_buffer.c +96 -0
- data/ext/polyphony/ring_buffer.h +28 -0
- data/ext/polyphony/thread.c +208 -0
- data/ext/polyphony/tracing.c +11 -0
- data/lib/polyphony.rb +136 -0
- data/lib/polyphony/adapters/fs.rb +19 -0
- data/lib/polyphony/adapters/irb.rb +52 -0
- data/lib/polyphony/adapters/postgres.rb +110 -0
- data/lib/polyphony/adapters/process.rb +33 -0
- data/lib/polyphony/adapters/redis.rb +67 -0
- data/lib/polyphony/adapters/trace.rb +138 -0
- data/lib/polyphony/core/channel.rb +46 -0
- data/lib/polyphony/core/exceptions.rb +36 -0
- data/lib/polyphony/core/global_api.rb +124 -0
- data/lib/polyphony/core/resource_pool.rb +117 -0
- data/lib/polyphony/core/sync.rb +21 -0
- data/lib/polyphony/core/thread_pool.rb +64 -0
- data/lib/polyphony/core/throttler.rb +41 -0
- data/lib/polyphony/event.rb +17 -0
- data/lib/polyphony/extensions/core.rb +174 -0
- data/lib/polyphony/extensions/fiber.rb +379 -0
- data/lib/polyphony/extensions/io.rb +221 -0
- data/lib/polyphony/extensions/openssl.rb +81 -0
- data/lib/polyphony/extensions/socket.rb +150 -0
- data/lib/polyphony/extensions/thread.rb +108 -0
- data/lib/polyphony/net.rb +77 -0
- data/lib/polyphony/version.rb +5 -0
- data/polyphony.gemspec +40 -0
- data/test/coverage.rb +54 -0
- data/test/eg.rb +27 -0
- data/test/helper.rb +56 -0
- data/test/q.rb +24 -0
- data/test/run.rb +5 -0
- data/test/stress.rb +25 -0
- data/test/test_agent.rb +130 -0
- data/test/test_event.rb +59 -0
- data/test/test_ext.rb +196 -0
- data/test/test_fiber.rb +988 -0
- data/test/test_global_api.rb +352 -0
- data/test/test_io.rb +249 -0
- data/test/test_kernel.rb +57 -0
- data/test/test_process_supervision.rb +46 -0
- data/test/test_queue.rb +112 -0
- data/test/test_resource_pool.rb +138 -0
- data/test/test_signal.rb +100 -0
- data/test/test_socket.rb +34 -0
- data/test/test_supervise.rb +103 -0
- data/test/test_thread.rb +170 -0
- data/test/test_thread_pool.rb +101 -0
- data/test/test_throttler.rb +50 -0
- data/test/test_trace.rb +68 -0
- metadata +482 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Concurrency the Easy Way
|
4
|
+
nav_order: 1
|
5
|
+
parent: Main Concepts
|
6
|
+
permalink: /main-concepts/concurrency/
|
7
|
+
prev_title: Tutorial
|
8
|
+
next_title: How Fibers are Scheduled
|
9
|
+
---
|
10
|
+
# Concurrency the Easy Way
|
11
|
+
|
12
|
+
Concurrency is a major consideration for modern programmers. Applications and
|
13
|
+
digital platforms are nowadays expected to do multiple things at once: serve
|
14
|
+
multiple clients, process multiple background jobs, talk to multiple external
|
15
|
+
services. Concurrency is the property of our programming environment allowing us
|
16
|
+
to schedule and control multiple ongoing operations.
|
17
|
+
|
18
|
+
Traditionally, concurrency has been achieved by using multiple processes or
|
19
|
+
threads. Both approaches have proven problematic. Processes consume relatively a
|
20
|
+
lot of memory, and are relatively difficult to coordinate. Threads consume less
|
21
|
+
memory than processes and make it difficult to synchronize access to shared
|
22
|
+
resources, often leading to race conditions and memory corruption. Using threads
|
23
|
+
often necessitates either using special-purpose thread-safe data structures, or
|
24
|
+
otherwise protecting shared resource access using mutexes and critical sections.
|
25
|
+
In addition, dynamic languages such as Ruby and Python will synchronize multiple
|
26
|
+
threads using a global interpreter lock, which means thread execution cannot be
|
27
|
+
parallelized. Furthermore, the amount of threads and processes on a single
|
28
|
+
system is relatively limited, to the order of several hundreds or a few thousand
|
29
|
+
at most.
|
30
|
+
|
31
|
+
Polyphony offers a third way to write concurrent programs, by using a Ruby
|
32
|
+
construct called [fibers](https://ruby-doc.org/core-2.6.5/Fiber.html). Fibers,
|
33
|
+
based on the idea of [coroutines](https://en.wikipedia.org/wiki/Coroutine),
|
34
|
+
provide a way to run a computation that can be suspended and resumed at any
|
35
|
+
moment. For example, a computation waiting for a reply from a database can
|
36
|
+
suspend itself, transferring control to another ongoing computation, and be
|
37
|
+
resumed once the database has sent back its reply. Meanwhile, another
|
38
|
+
computation is started that opens a socket to a remote service, and then
|
39
|
+
suspends itself, waiting for the connection to be established.
|
40
|
+
|
41
|
+
This form of concurrency, called cooperative concurrency (in contrast to
|
42
|
+
pre-emptive concurrency, like in threads and processes), offers many advantages,
|
43
|
+
especially for applications that are [I/O
|
44
|
+
bound](https://en.wikipedia.org/wiki/I/O_bound). Fibers are very lightweight
|
45
|
+
(starting at about 10KB), can be context-switched faster than threads or
|
46
|
+
processes, and literally millions of them can be created on a single system -
|
47
|
+
the only limiting factor is available memory.
|
48
|
+
|
49
|
+
Polyphony takes Ruby's fibers and adds a way to schedule and switch between them
|
50
|
+
automatically whenever a blocking operation is started, such as waiting for a
|
51
|
+
TCP connection to be established, for incoming data on an HTTP conection, or for
|
52
|
+
a timer to elapse. In addition, Polyphony patches the stock Ruby classes to
|
53
|
+
support its concurrency model, letting developers use all of Ruby's stdlib, for
|
54
|
+
example `Net::HTTP` and `Mail` while reaping the benefits of lightweight,
|
55
|
+
fine-grained, performant, fiber-based concurrency.
|
56
|
+
|
57
|
+
Writing concurrent applications using Polyphony's fiber-based concurrency model
|
58
|
+
offers a significant performance advantage. Complex concurrent tasks can be
|
59
|
+
broken down into many fine-grained concurrent operations with very low overhead.
|
60
|
+
More importantly, this concurrency model lets developers express their ideas in
|
61
|
+
a sequential fashion, leading to source code that is much easier to read and
|
62
|
+
understand, compared to callback-style programming.
|
63
|
+
|
64
|
+
## Fibers - Polyphony's basic unit of concurrency
|
65
|
+
|
66
|
+
Polyphony extends the core `Fiber` class with additional functionality that
|
67
|
+
allows scheduling, synchronizing, interrupting and otherwise controlling running
|
68
|
+
fibers. Starting a concurrent operation inside a fiber is as simple as a `spin`
|
69
|
+
method call:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
while (connection = server.accept)
|
73
|
+
spin { handle_connection(connection) }
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
In order to facilitate developing applications that employ complex concurrent
|
78
|
+
patterns and can scale easily, Polyphony employs a [structured
|
79
|
+
approach](https://en.wikipedia.org/wiki/Structured_concurrency) to controlling
|
80
|
+
fiber lifetime. A spun fiber is considered the *child* of the fiber from which
|
81
|
+
it was spun, and is always limited to the life time of its parent:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
parent = spin do
|
85
|
+
do_something
|
86
|
+
child = spin do
|
87
|
+
do_some_other_stuff
|
88
|
+
end
|
89
|
+
# the child fiber is guaranteed to stop executing before the parent fiber
|
90
|
+
# terminates
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
Any uncaught exception raised in a fiber will be
|
95
|
+
[propagated]((exception-handling.md)) to its parent, and potentially further up
|
96
|
+
the fiber hierarchy, all the way to the main fiber:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
parent = spin do
|
100
|
+
child = spin do
|
101
|
+
raise 'foo'
|
102
|
+
end
|
103
|
+
sleep
|
104
|
+
end
|
105
|
+
|
106
|
+
sleep
|
107
|
+
# the exception will be propagated from the child fiber to the parent fiber,
|
108
|
+
# and from the parent fiber to the main fiber, which will cause the program to
|
109
|
+
# abort.
|
110
|
+
```
|
111
|
+
|
112
|
+
In addition, fibers can communicate with each other using message passing,
|
113
|
+
turning them into autonomous actors in a highly concurrent environment. Message
|
114
|
+
passing is in many ways a superior way to pass data between concurrent entities,
|
115
|
+
obviating the need to synchronize access to shared resources:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
writer = spin do
|
119
|
+
while (write_request = receive)
|
120
|
+
do_write(write_request)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
...
|
124
|
+
writer << { stamp: Time.now, value: rand }
|
125
|
+
```
|
126
|
+
|
127
|
+
## Higher-Order Concurrency Constructs
|
128
|
+
|
129
|
+
Polyphony also provides several methods and constructs for controlling multiple
|
130
|
+
fibers. Methods like `cancel_after` and `move_on_after` allow interrupting a
|
131
|
+
fiber that's blocking on any arbitrary operation.
|
132
|
+
|
133
|
+
Some other constructs offered by Polyphony:
|
134
|
+
|
135
|
+
* `Mutex` - a mutex used to synchronize access to a single shared resource.
|
136
|
+
* `ResourcePool` - used for synchronizing access to a limited amount of shared
|
137
|
+
resources, for example a pool of database connections.
|
138
|
+
* `Throttler` - used for throttling repeating operations, for example throttling
|
139
|
+
access to a shared resource, or throttling incoming requests.
|
140
|
+
|
141
|
+
## A Compelling Concurrency Solution for Ruby
|
142
|
+
|
143
|
+
> The goal of Ruby is to make programmers happy.
|
144
|
+
|
145
|
+
— Yukihiro “Matz” Matsumoto
|
146
|
+
|
147
|
+
Polyphony's goal is to make programmers even happier by offering them an easy
|
148
|
+
way to write concurrent applications in Ruby. Polyphony aims to show that Ruby
|
149
|
+
can be used for developing sufficiently high-performance applications, while
|
150
|
+
offering all the advantages of Ruby, with source code that is easy to read and
|
151
|
+
understand.
|
@@ -0,0 +1,161 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: The Design of Polyphony
|
4
|
+
nav_order: 5
|
5
|
+
parent: Main Concepts
|
6
|
+
permalink: /main-concepts/design-principles/
|
7
|
+
prev_title: Extending Polyphony
|
8
|
+
---
|
9
|
+
# The Design of Polyphony
|
10
|
+
|
11
|
+
Polyphony is a new gem that aims to enable developing high-performance
|
12
|
+
concurrent applications in Ruby using a fluent, compact syntax and API.
|
13
|
+
Polyphony enables fine-grained concurrency - the splitting up of operations into
|
14
|
+
a large number of concurrent tasks, each concerned with small part of the whole
|
15
|
+
and advancing at its own pace. Polyphony aims to solve some of the problems
|
16
|
+
associated with concurrent Ruby programs using a novel design that sets it apart
|
17
|
+
from other approaches currently being used in Ruby.
|
18
|
+
|
19
|
+
## Origins
|
20
|
+
|
21
|
+
The Ruby core language (at least in its MRI implementation) currently provides
|
22
|
+
two main constructs for performing concurrent work: threads and fibers. While
|
23
|
+
Ruby threads are basically wrappers for OS threads, fibers are essentially
|
24
|
+
continuations, allowing pausing and resuming distinct computations. Fibers have
|
25
|
+
been traditionally used mostly for implementing enumerators and generators.
|
26
|
+
|
27
|
+
In addition to the core Ruby concurrency primitives, some Ruby gems have been
|
28
|
+
offering an alternative solution to writing concurrent Ruby apps, most notably
|
29
|
+
[EventMachine](https://github.com/eventmachine/eventmachine/), which implements
|
30
|
+
an event reactor and offers an asynchronous callback-based API for writing
|
31
|
+
concurrent code.
|
32
|
+
|
33
|
+
In the last couple of years, however, fibers have been receiving more attention
|
34
|
+
as a possible constructs for writing concurrent programs. In particular, the
|
35
|
+
[Async](https://github.com/socketry/async) framework, created by [Samuel
|
36
|
+
Williams](https://github.com/ioquatix), offering a comprehensive set of
|
37
|
+
libraries, employs fibers in conjunction with an event reactor provided by the
|
38
|
+
[nio4r](https://github.com/socketry/nio4r) gem, which wraps the C
|
39
|
+
library [libev](http://software.schmorp.de/pkg/libev.html).
|
40
|
+
|
41
|
+
In addition, recently some effort was undertaken to provide a way to
|
42
|
+
[automatically switch between fibers](https://bugs.ruby-lang.org/issues/13618)
|
43
|
+
whenever a blocking operation is performed, or to [integrate a fiber
|
44
|
+
scheduler](https://bugs.ruby-lang.org/issues/16786) into the core Ruby code.
|
45
|
+
|
46
|
+
Nevertheless, while work is being done to harness fibers for providing a better
|
47
|
+
way to do concurrency in Ruby, fibers remain a mistery for most Ruby
|
48
|
+
programmers, a perplexing unfamiliar corner right at the heart of Ruby.
|
49
|
+
|
50
|
+
## The History of Polyphony
|
51
|
+
|
52
|
+
Polyphony started as an experiment, but over about two years of slow, jerky
|
53
|
+
evolution turned into something I'm really excited to share with the Ruby
|
54
|
+
community. Polyphony's design is both similar and different than the projects
|
55
|
+
mentioned above.
|
56
|
+
|
57
|
+
Polyphony today as nothing like the way it began. A careful examination of the
|
58
|
+
[CHANGELOG](https://github.com/digital-fabric/polyphony/blob/master/CHANGELOG.md)
|
59
|
+
would show how Polyphony explored not only different event reactor designs, but
|
60
|
+
also different API designs incorporating various concurrent paradigms such as
|
61
|
+
promises, async/await, fibers, and finally structured concurrency.
|
62
|
+
|
63
|
+
Throughout the development process, it was my intention to create a programming
|
64
|
+
interface that would make it easy to author highly-concurrent Ruby programs.
|
65
|
+
|
66
|
+
## Design Principles
|
67
|
+
|
68
|
+
While Polyphony, like nio4r or EventMachine, uses an event reactor to turn
|
69
|
+
blocking operations into non-blocking ones, it completely embraces fibers and in
|
70
|
+
fact does not provide any callback-based APIs.
|
71
|
+
|
72
|
+
Furthermore, Polyphony provides fullblown fiber-aware implementations of
|
73
|
+
blocking operations, such as `read/write`, `sleep` or `waitpid`, instead of just
|
74
|
+
event watching primitives.
|
75
|
+
|
76
|
+
Polyphony's design is based on the following principles:
|
77
|
+
|
78
|
+
- The concurrency model should feel "baked-in". The API should allow
|
79
|
+
concurrency with minimal effort. Polyphony should facilitate writing both
|
80
|
+
large apps and small scripts with as little boilerplate code as possible.
|
81
|
+
There should be no calls to initialize the event reactor, or other ceremonial
|
82
|
+
code:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
require 'polyphony'
|
86
|
+
|
87
|
+
# start 10 fibers, each sleeping for 3 seconds
|
88
|
+
10.times { spin { sleep 3 } }
|
89
|
+
|
90
|
+
puts 'going to sleep now'
|
91
|
+
# wait for other fibers to terminate
|
92
|
+
suspend
|
93
|
+
```
|
94
|
+
|
95
|
+
- Blocking operations should yield to other concurrent tasks without any
|
96
|
+
decoration or wrapper APIs. This means no `async/await` notation, and no
|
97
|
+
async callback-style APIs.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# in Polyphony, I/O ops might block the current fiber, but implicitly yield to
|
101
|
+
# other concurrent fibers:
|
102
|
+
clients.each do |client|
|
103
|
+
spin { client.puts 'Elvis has left the chatroom' }
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
- Concurrency primitives should be accessible using idiomatic Ruby techniques
|
108
|
+
(blocks, method chaining...) and should feel as much as possible "part of the
|
109
|
+
language". The resulting API is fundamentally based on methods rather than classes,
|
110
|
+
for example `spin` or `move_on_after`, leading to a coding style that is both
|
111
|
+
more compact and more legible:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
fiber = spin {
|
115
|
+
move_on_after(3) {
|
116
|
+
do_something_slow
|
117
|
+
}
|
118
|
+
}
|
119
|
+
```
|
120
|
+
|
121
|
+
- Polyphony should embrace Ruby's standard `raise/rescue/ensure` exception
|
122
|
+
handling mechanism. Exception handling in a highly concurrent environment
|
123
|
+
should be robust and foolproof:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
cancel_after(0.5) do
|
127
|
+
puts 'going to sleep'
|
128
|
+
sleep 1
|
129
|
+
# this will not be printed
|
130
|
+
puts 'wokeup'
|
131
|
+
ensure
|
132
|
+
# this will be printed
|
133
|
+
puts 'done sleeping'
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
- Concurrency primitives should allow creating higher-order concurrent
|
138
|
+
constructs through composition.
|
139
|
+
|
140
|
+
- The entire design should embrace fibers. There should be no callback-based
|
141
|
+
asynchronous APIs. The library and its ecosystem will foster the development
|
142
|
+
of techniques and tools for converting callback-based APIs to fiber-based ones.
|
143
|
+
|
144
|
+
- Use of extensive monkey patching of Ruby core modules and classes such as
|
145
|
+
`Kernel`, `Fiber`, `IO` and `Timeout`. This allows porting over non-Polyphony
|
146
|
+
code, as well as using a larger part of stdlib in a concurrent manner, without
|
147
|
+
having to use custom non-standard network classes or other glue code.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
require 'polyphony'
|
151
|
+
|
152
|
+
# use TCPServer from Ruby's stdlib
|
153
|
+
server = TCPServer.open('127.0.0.1', 1234)
|
154
|
+
while (client = server.accept)
|
155
|
+
spin {
|
156
|
+
while (data = client.gets)
|
157
|
+
client.write("you said: #{ data.chomp }\n")
|
158
|
+
end
|
159
|
+
}
|
160
|
+
end
|
161
|
+
```
|
@@ -0,0 +1,291 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Exception Handling
|
4
|
+
nav_order: 3
|
5
|
+
parent: Main Concepts
|
6
|
+
permalink: /main-concepts/exception-handling/
|
7
|
+
prev_title: How Fibers are Scheduled
|
8
|
+
next_title: Extending Polyphony
|
9
|
+
---
|
10
|
+
# Exception Handling
|
11
|
+
|
12
|
+
Ruby employs a pretty robust exception handling mechanism. An raised exception
|
13
|
+
will propagate up the fiber tree until a suitable exception handler is found,
|
14
|
+
based on the exception's class. In addition, the exception will include a stack
|
15
|
+
trace showing the execution path from the exception's locus back to the
|
16
|
+
program's entry point. Unfortunately, when exceptions are raised while switching
|
17
|
+
between fibers, stack traces will only include partial information. Here's a
|
18
|
+
simple demonstration:
|
19
|
+
|
20
|
+
_fiber\_exception.rb_
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
require 'fiber'
|
24
|
+
|
25
|
+
def fail!
|
26
|
+
raise 'foobar'
|
27
|
+
end
|
28
|
+
|
29
|
+
f = Fiber.new do
|
30
|
+
Fiber.new do
|
31
|
+
fail!
|
32
|
+
end.transfer
|
33
|
+
end
|
34
|
+
|
35
|
+
f.transfer
|
36
|
+
```
|
37
|
+
|
38
|
+
Running the above program will give us:
|
39
|
+
|
40
|
+
```text
|
41
|
+
Traceback (most recent call last):
|
42
|
+
1: from fiber_exception.rb:9:in `block (2 levels) in <main>'
|
43
|
+
fiber_exception.rb:4:in `fail!': foobar (RuntimeError)
|
44
|
+
```
|
45
|
+
|
46
|
+
So, the stack trace includes two frames: the exception's locus on line 4 and the
|
47
|
+
call site at line 9. But we have no information on how we got to line 9. Let's
|
48
|
+
imagine if we had more complete information about the sequence of execution. In
|
49
|
+
fact, what is missing is information about how the different fibers were
|
50
|
+
created. If we had that, our stack trace would have looked something like this:
|
51
|
+
|
52
|
+
```text
|
53
|
+
Traceback (most recent call last):
|
54
|
+
4: from fiber_exception.rb:13:in `<main>'
|
55
|
+
3: from fiber_exception.rb:7:in `Fiber.new'
|
56
|
+
2: from fiber_exception.rb:8:in `Fiber.new'
|
57
|
+
1: from fiber_exception.rb:9:in `block (2 levels) in <main>'
|
58
|
+
fiber_exception.rb:4:in `fail!': foobar (RuntimeError)
|
59
|
+
```
|
60
|
+
|
61
|
+
In order to achieve this, Polyphony patches `Fiber.new` to keep track of the
|
62
|
+
call stack at the moment the fiber was created, as well as the fiber from which
|
63
|
+
the call happened. In addition, Polyphony patches `Exception#backtrace` in order
|
64
|
+
to synthesize a complete stack trace based on the call stack information stored
|
65
|
+
for the current fiber. This is done recursively through the chain of fibers
|
66
|
+
leading up to the current location. What we end up with is a record of the
|
67
|
+
entire sequence of \(possibly intermittent\) execution leading up to the point
|
68
|
+
where the exception was raised.
|
69
|
+
|
70
|
+
In addition, the backtrace is sanitized to remove stack frames originating from
|
71
|
+
the Polyphony code itself, which hides away the Polyphony plumbing and lets
|
72
|
+
developers concentrate on their own code. The sanitizing of exception backtraces
|
73
|
+
can be disabled by setting the `Exception.__disable_sanitized_backtrace__` flag:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
Exception.__disable_sanitized_backtrace__ = true
|
77
|
+
...
|
78
|
+
```
|
79
|
+
|
80
|
+
## Exceptions and Fiber Scheduling
|
81
|
+
|
82
|
+
Polyphony takes advantages of Ruby's `Fiber#transfer` API to allow interrupting
|
83
|
+
fiber execution and raise cross-fiber exceptions. This is done by inspecting the
|
84
|
+
return value of `Fiber#transfer`, which returns when the fiber resumes, at every
|
85
|
+
[switchpoint](../fiber-scheduling/#switchpoints). If the return value is an
|
86
|
+
exception, it is raised in the context of the resumed fiber, and is then subject
|
87
|
+
to any `rescue` statements in the context of that fiber.
|
88
|
+
|
89
|
+
Exceptions can be passed to arbitrary fibers by using `Fiber#raise`. They can also be manually raised in fibers by using `Fiber#schedule`:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
f = spin do
|
93
|
+
suspend
|
94
|
+
rescue => e
|
95
|
+
puts e.message
|
96
|
+
end
|
97
|
+
|
98
|
+
f.schedule(RuntimeError.new('foo')) #=> will print 'foo'
|
99
|
+
```
|
100
|
+
|
101
|
+
## Cleaning Up After Exceptions - Using Ensure
|
102
|
+
|
103
|
+
A major issue when handling exceptions is cleaning up - freeing up resources
|
104
|
+
that have been allocated, cancelling ongoing operations, etc. Polyphony allows
|
105
|
+
using the normal `ensure` statement for cleaning up. Have a look at Polyphony's
|
106
|
+
implementation of `Kernel#sleep`:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
def sleep(duration)
|
110
|
+
timer = Gyro::Timer.new(duration, 0)
|
111
|
+
timer.await
|
112
|
+
ensure
|
113
|
+
timer.stop
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
This method creates a one-shot timer with the given duration and then suspends
|
118
|
+
the current fiber, waiting for the timer to fire and then resume the fiber.
|
119
|
+
While the awaiting fiber is suspended, other operations might be going on, which
|
120
|
+
might interrupt the `sleep` operation by scheduling the awaiting fiber with an
|
121
|
+
exception, for example a `MoveOn` or a `Cancel` exception. For this reason, we
|
122
|
+
need to _ensure_ that the timer will be stopped, regardless of whether it has
|
123
|
+
fired or not. We call `timer.stop` inside an ensure block, thus ensuring that
|
124
|
+
the timer will have stopped once the awaiting fiber has resumed, even if it has
|
125
|
+
not fired.
|
126
|
+
|
127
|
+
## Exception Propagation
|
128
|
+
|
129
|
+
One of the "annoying" things about exceptions is that for them to be useful, you
|
130
|
+
have to intercept them \(using `rescue`\). If you forget to do that, you'll end
|
131
|
+
up with uncaught exceptions that can wreak havoc. For example, by default a Ruby
|
132
|
+
`Thread` in which an exception was raised without being caught, will simply
|
133
|
+
terminate with the exception silently swallowed.
|
134
|
+
|
135
|
+
To prevent the same from happening with fibers, Polyphony provides a robust
|
136
|
+
mechanism that propagates uncaught exceptions up through the chain of parent
|
137
|
+
fibers. Let's discuss the following example:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
require 'polyphony'
|
141
|
+
|
142
|
+
spin do
|
143
|
+
spin do
|
144
|
+
spin do
|
145
|
+
spin do
|
146
|
+
raise 'foo'
|
147
|
+
end
|
148
|
+
sleep
|
149
|
+
end
|
150
|
+
sleep
|
151
|
+
end
|
152
|
+
sleep
|
153
|
+
end
|
154
|
+
|
155
|
+
sleep
|
156
|
+
```
|
157
|
+
|
158
|
+
In the above example, four nested fibers are created, and each of them, except
|
159
|
+
for the innermost fiber, goes to sleep for an unlimited duration. An exception
|
160
|
+
is raised in the innermost fiber, and having no corresponding exception handler,
|
161
|
+
will propagate up through the enclosing fibers, until reaching the
|
162
|
+
top-most level, that of the root fiber, at which point the exception will cause
|
163
|
+
the program to abort and print an error message.
|
164
|
+
|
165
|
+
## MoveOn and Cancel - Interrupting Fiber Execution
|
166
|
+
|
167
|
+
In addition to enhancing Ruby's normal exception-handling mechanism, Polyphony
|
168
|
+
provides two exception classes that used exclusively to interrupt fiber
|
169
|
+
execution: `MoveOn` and `Cancel`. Both of these classes are used in various
|
170
|
+
fiber-control APIs, and `MoveOn` exceptions in particular are handled in a
|
171
|
+
particular manner by Polyphony. The difference between `MoveOn` and `Cancel` is
|
172
|
+
that `MoveOn` stops fiber execution without the exception propagating. It can
|
173
|
+
optionally provide an arbitrary return value for the fiber. `Cancel` will propagate
|
174
|
+
up like all exceptions.
|
175
|
+
|
176
|
+
The `MoveOn` and `Cancel` classes are normally used indirectly, through the
|
177
|
+
`Fiber#interrupt` and `Fiber#cancel` APIs, and also through the use of [cancel
|
178
|
+
scopes](#):
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
f1 = spin { sleep 100; return 'foo' }
|
182
|
+
f2 = spin { f1.await }
|
183
|
+
...
|
184
|
+
f1.interrupt('bar')
|
185
|
+
f2.result #=> 'bar'
|
186
|
+
|
187
|
+
f3 = spin { sleep 100 }
|
188
|
+
...
|
189
|
+
f3.cancel #=> will raise a Cancel exception
|
190
|
+
```
|
191
|
+
|
192
|
+
In addition to `MoveOn` and `Cancel`, Polyphony employs internally another
|
193
|
+
exception class, `Terminate` for terminating a fiber once its parent has
|
194
|
+
finished executing.
|
195
|
+
|
196
|
+
## The Special Problem of Signal Handling
|
197
|
+
|
198
|
+
Ruby by default handles process signals by generating exceptions, allowing the
|
199
|
+
handling of signals in a structured manner. However, process signals may arrive
|
200
|
+
at any moment, and may be trapped while any arbitrary fiber is running, and even
|
201
|
+
while an event loop is running.
|
202
|
+
|
203
|
+
Two signals in particular require special care as they involve the stopping of
|
204
|
+
the entire process: `TERM` and `INT`. The `TERM` signal should be handled
|
205
|
+
gracefully, i.e. with proper cleanup, which also means terminating all fibers.
|
206
|
+
The `INT` signal requires halting the process and printing a correct stack
|
207
|
+
trace.
|
208
|
+
|
209
|
+
To ensure correct behaviour for these two signals, polyphony installs signal
|
210
|
+
handlers that ensure that the main thread's event loop stops if it's currently
|
211
|
+
running, and that the corresponding exceptions (namely `SystemExit` and
|
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
|
245
|
+
|
246
|
+
Care should be taken when handling other signals. There are two options for
|
247
|
+
correctly handling the signals: using Ruby's stock `trap` method, and using
|
248
|
+
Polyphony's signal watchers. The stock method involves trapping signals as
|
249
|
+
usual, but making sure we're not inside the event loop:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
trap('SIGHUP') do
|
253
|
+
Thread.current.break_out_of_ev_loop(Thread.current.main_fiber, nil)
|
254
|
+
handle_hup_signal
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
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:
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
hup_handler = spin_loop do
|
276
|
+
Polyphony.wait_for_signal('SIGHUP')
|
277
|
+
handle_hup_signal
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
## The Special Problem of Thread Termination
|
282
|
+
|
283
|
+
Thread termination using `Thread#kill` or `Thread#raise` also presents the same
|
284
|
+
problems as signal handling in a multi-fiber environment. The termination can
|
285
|
+
occur while any fiber is running, and even while running the thread's event
|
286
|
+
loop.
|
287
|
+
|
288
|
+
To ensure proper thread termination, including the termination of all the
|
289
|
+
thread's fibers, Polyphony patches the `Thread#kill` and `Thread#raise` methods
|
290
|
+
to schedule the thread's main fiber with the corresponding exceptions, thus
|
291
|
+
ensuring an orderly termination or exception handling.
|