polyphony 0.38 → 0.43
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/.github/workflows/test.yml +11 -2
- data/.gitignore +2 -2
- data/.rubocop.yml +30 -0
- data/CHANGELOG.md +25 -2
- data/Gemfile.lock +15 -12
- data/README.md +2 -1
- data/Rakefile +3 -3
- data/TODO.md +27 -97
- data/docs/_config.yml +56 -7
- data/docs/_sass/custom/custom.scss +0 -30
- data/docs/_sass/overrides.scss +0 -46
- data/docs/{user-guide → _user-guide}/all-about-timers.md +0 -0
- data/docs/_user-guide/index.md +9 -0
- data/docs/{user-guide → _user-guide}/web-server.md +0 -0
- data/docs/api-reference/fiber.md +2 -2
- data/docs/api-reference/index.md +9 -0
- data/docs/api-reference/polyphony-process.md +1 -1
- data/docs/api-reference/thread.md +1 -1
- data/docs/faq.md +21 -11
- data/docs/getting-started/index.md +10 -0
- data/docs/getting-started/installing.md +2 -6
- data/docs/getting-started/overview.md +486 -0
- data/docs/getting-started/tutorial.md +27 -19
- data/docs/index.md +1 -1
- data/docs/main-concepts/concurrency.md +0 -5
- data/docs/main-concepts/design-principles.md +69 -21
- data/docs/main-concepts/extending.md +1 -1
- data/docs/main-concepts/index.md +9 -0
- data/examples/core/01-spinning-up-fibers.rb +1 -0
- data/examples/core/03-interrupting.rb +4 -1
- data/examples/core/04-handling-signals.rb +19 -0
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-sleeping.rb +14 -6
- data/examples/io/tunnel.rb +48 -0
- data/examples/io/xx-irb.rb +1 -1
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +13 -36
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -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/ext/{gyro → polyphony}/extconf.rb +2 -2
- data/ext/{gyro → polyphony}/fiber.c +17 -23
- data/ext/{gyro → polyphony}/libev.c +0 -0
- data/ext/{gyro → polyphony}/libev.h +0 -0
- data/ext/polyphony/libev_agent.c +718 -0
- data/ext/polyphony/libev_queue.c +216 -0
- data/ext/{gyro/gyro.c → polyphony/polyphony.c} +16 -40
- data/ext/{gyro/gyro.h → polyphony/polyphony.h} +19 -39
- data/ext/polyphony/polyphony_ext.c +23 -0
- data/ext/{gyro → polyphony}/socket.c +21 -18
- data/ext/polyphony/thread.c +206 -0
- data/ext/{gyro → polyphony}/tracing.c +1 -1
- data/lib/polyphony.rb +19 -14
- data/lib/polyphony/adapters/irb.rb +1 -1
- data/lib/polyphony/adapters/postgres.rb +6 -5
- data/lib/polyphony/adapters/process.rb +5 -5
- data/lib/polyphony/adapters/trace.rb +28 -28
- data/lib/polyphony/core/channel.rb +3 -3
- data/lib/polyphony/core/exceptions.rb +1 -1
- data/lib/polyphony/core/global_api.rb +13 -11
- data/lib/polyphony/core/resource_pool.rb +3 -3
- data/lib/polyphony/core/sync.rb +2 -2
- data/lib/polyphony/core/thread_pool.rb +6 -6
- data/lib/polyphony/core/throttler.rb +13 -6
- data/lib/polyphony/event.rb +27 -0
- data/lib/polyphony/extensions/core.rb +22 -14
- data/lib/polyphony/extensions/fiber.rb +4 -4
- data/lib/polyphony/extensions/io.rb +59 -25
- data/lib/polyphony/extensions/openssl.rb +36 -16
- data/lib/polyphony/extensions/socket.rb +27 -9
- data/lib/polyphony/extensions/thread.rb +16 -9
- data/lib/polyphony/net.rb +9 -9
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +4 -4
- data/test/helper.rb +14 -1
- data/test/test_agent.rb +124 -0
- data/test/{test_async.rb → test_event.rb} +15 -7
- data/test/test_ext.rb +25 -4
- data/test/test_fiber.rb +19 -10
- data/test/test_global_api.rb +4 -4
- data/test/test_io.rb +46 -24
- data/test/test_queue.rb +74 -0
- data/test/test_signal.rb +3 -40
- data/test/test_socket.rb +34 -0
- data/test/test_thread.rb +37 -16
- data/test/test_trace.rb +6 -5
- metadata +40 -43
- data/docs/_includes/nav.html +0 -51
- data/docs/_includes/prevnext.html +0 -17
- data/docs/_layouts/default.html +0 -106
- data/docs/api-reference.md +0 -11
- data/docs/api-reference/gyro-async.md +0 -57
- data/docs/api-reference/gyro-child.md +0 -29
- data/docs/api-reference/gyro-queue.md +0 -44
- data/docs/api-reference/gyro-timer.md +0 -51
- data/docs/api-reference/gyro.md +0 -25
- data/docs/getting-started.md +0 -10
- data/docs/main-concepts.md +0 -10
- data/docs/user-guide.md +0 -10
- data/examples/core/forever_sleep.rb +0 -19
- data/ext/gyro/async.c +0 -162
- data/ext/gyro/child.c +0 -141
- data/ext/gyro/gyro_ext.c +0 -33
- data/ext/gyro/io.c +0 -489
- data/ext/gyro/queue.c +0 -142
- data/ext/gyro/selector.c +0 -228
- data/ext/gyro/signal.c +0 -133
- data/ext/gyro/thread.c +0 -308
- data/ext/gyro/timer.c +0 -149
- data/test/test_timer.rb +0 -56
@@ -1,30 +0,0 @@
|
|
1
|
-
$nav-child-link-color: #9d9b9e;
|
2
|
-
$link-color: $blue-000;
|
3
|
-
|
4
|
-
.img-figure {
|
5
|
-
text-align: center;
|
6
|
-
}
|
7
|
-
|
8
|
-
#prevnext {
|
9
|
-
padding-top: 1em;
|
10
|
-
}
|
11
|
-
|
12
|
-
#prevnext span {
|
13
|
-
}
|
14
|
-
|
15
|
-
#prevnext span.prev {
|
16
|
-
float: left;
|
17
|
-
padding-right: 4em;
|
18
|
-
}
|
19
|
-
|
20
|
-
#prevnext span.next {
|
21
|
-
float: right;
|
22
|
-
}
|
23
|
-
|
24
|
-
#prevnext span.clear {
|
25
|
-
clear: both;
|
26
|
-
}
|
27
|
-
|
28
|
-
.h-align-center {
|
29
|
-
text-align: center;
|
30
|
-
}
|
data/docs/_sass/overrides.scss
CHANGED
@@ -1,46 +0,0 @@
|
|
1
|
-
.navigation-list-item.section-title {
|
2
|
-
padding-top: 0.75em;
|
3
|
-
padding-bottom: 0.75em;
|
4
|
-
}
|
5
|
-
|
6
|
-
span.section-title {
|
7
|
-
text-transform: uppercase;
|
8
|
-
font-weight: bold;
|
9
|
-
color: #888;
|
10
|
-
}
|
11
|
-
|
12
|
-
.navigation-list-item .navigation-list-child-list {
|
13
|
-
display: block;
|
14
|
-
}
|
15
|
-
|
16
|
-
.navigation-list-child-list {
|
17
|
-
padding-left: 0;
|
18
|
-
|
19
|
-
.navigation-list-item {
|
20
|
-
position: relative;
|
21
|
-
|
22
|
-
&::before {
|
23
|
-
position: absolute;
|
24
|
-
margin-top: 0.3em;
|
25
|
-
margin-left: -0.8em;
|
26
|
-
color: rgba($body-text-color, 0.3);
|
27
|
-
content: "";
|
28
|
-
}
|
29
|
-
}
|
30
|
-
}
|
31
|
-
|
32
|
-
a.navigation-list-link {
|
33
|
-
margin-left: 4px;
|
34
|
-
padding-left: 4px;
|
35
|
-
color: $nav-child-link-color;
|
36
|
-
font-weight: 600;
|
37
|
-
}
|
38
|
-
|
39
|
-
a.navigation-list-link.active {
|
40
|
-
margin-left: 0;
|
41
|
-
border-left: 4px #6ae solid;
|
42
|
-
}
|
43
|
-
|
44
|
-
a.navigation-list-link:hover {
|
45
|
-
color: $link-color;
|
46
|
-
}
|
File without changes
|
File without changes
|
data/docs/api-reference/fiber.md
CHANGED
@@ -71,7 +71,7 @@ f << 2
|
|
71
71
|
result = receive #=> 20
|
72
72
|
```
|
73
73
|
|
74
|
-
### #
|
74
|
+
### #auto_watcher → async
|
75
75
|
|
76
76
|
Returns a reusable `Gyro::Async` watcher instance associated with the fiber.
|
77
77
|
This method provides a way to minimize watcher allocation. Instead of allocating
|
@@ -84,7 +84,7 @@ def work(async)
|
|
84
84
|
async.signal
|
85
85
|
end
|
86
86
|
|
87
|
-
async = Fiber.current.
|
87
|
+
async = Fiber.current.auto_watcher
|
88
88
|
spin { work(async) }
|
89
89
|
async.await
|
90
90
|
```
|
@@ -23,6 +23,6 @@ shell command. If a block is given, the child process is started using
|
|
23
23
|
[`Polyphony#fork`](../polyphony/#fork-block---pid).
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
Polyphony::Process.watch('echo "Hello World"; sleep 1')
|
26
|
+
spin { Polyphony::Process.watch('echo "Hello World"; sleep 1') }
|
27
27
|
supervise(restart: :always)
|
28
28
|
```
|
@@ -12,7 +12,7 @@ Polyphony enhances the core `Thread` class with APIs for switching and
|
|
12
12
|
scheduling fibers, and reimplements some of its APIs such as `Thread#raise`
|
13
13
|
using fibers which, incidentally, make it safe.
|
14
14
|
|
15
|
-
Each thread has its own run queue and its own
|
15
|
+
Each thread has its own run queue and its own system agent. While running
|
16
16
|
multiple threads does not result in true parallelism in MRI Ruby, sometimes
|
17
17
|
multithreading is inevitable, for instance when using third-party gems that
|
18
18
|
spawn threads, or when calling blocking APIs that are not fiber-aware.
|
data/docs/faq.md
CHANGED
@@ -3,9 +3,19 @@ layout: page
|
|
3
3
|
title: Frequently Asked Questions
|
4
4
|
nav_order: 100
|
5
5
|
---
|
6
|
+
|
6
7
|
# Frequently Asked Questions
|
8
|
+
{: .no_toc }
|
9
|
+
|
10
|
+
## Table of contents
|
11
|
+
{: .no_toc .text-delta }
|
12
|
+
|
13
|
+
- TOC
|
14
|
+
{:toc}
|
15
|
+
|
16
|
+
---
|
7
17
|
|
8
|
-
|
18
|
+
## Why not just use callbacks instead of fibers?
|
9
19
|
|
10
20
|
It is true that reactor engines such as libev use callbacks to handle events.
|
11
21
|
There's also programming platforms such as [node.js](https://nodejs.org/) that
|
@@ -87,7 +97,7 @@ In conclusion:
|
|
87
97
|
* Callbacks often lead to code bloat.
|
88
98
|
* Callbacks are harder to debug.
|
89
99
|
|
90
|
-
|
100
|
+
## If callbacks suck, why not use promises?
|
91
101
|
|
92
102
|
Promises have gained a lot of traction during the last few years as an
|
93
103
|
alternative to callbacks, above all in the Javascript community. While promises
|
@@ -96,7 +106,7 @@ found to offer enough of a benefit. Promises still cause split logic, are quite
|
|
96
106
|
verbose and provide a non-native exception handling mechanism. In addition, they
|
97
107
|
do not make it easier to debug your code.
|
98
108
|
|
99
|
-
|
109
|
+
## Why is awaiting implicit? Why not use explicit async/await?
|
100
110
|
|
101
111
|
Actually, async/await was contemplated while developing Polyphony, but at a
|
102
112
|
certain point it was decided to abandon these methods / decorators in favor of a
|
@@ -108,7 +118,7 @@ Instead, we have decided to make blocking operations implicit and thus allow the
|
|
108
118
|
use of common APIs such as `Kernel#sleep` or `IO.popen` in a transparent manner.
|
109
119
|
After all, these APIs in their stock form block execution just as well.
|
110
120
|
|
111
|
-
|
121
|
+
## Why use `Fiber#transfer` and not `Fiber#resume`?
|
112
122
|
|
113
123
|
The API for `Fiber.yield`/`Fiber#resume` is stateful and is intended for the
|
114
124
|
asymmetric execution of coroutines. This is useful when using generators, or
|
@@ -118,7 +128,7 @@ between them, which is much easier to achieve using `Fiber#transfer`. In
|
|
118
128
|
addition, using `Fiber#transfer` allows us to perform blocking operations from
|
119
129
|
the main fiber, which is not possible when using `Fiber#resume`.
|
120
130
|
|
121
|
-
|
131
|
+
## Why does Polyphony reimplements core APIs such as `IO#read` and `Kernel#sleep`?
|
122
132
|
|
123
133
|
Polyphony "patches" some Ruby core and stdlib APIs, providing behavioraly
|
124
134
|
compatible fiber-aware implementations. We believe Polyphony has the potential
|
@@ -126,7 +136,7 @@ to profoundly change the way concurrent Ruby apps are written. Polyphony is
|
|
126
136
|
therefore designed to feel as much as possible like an integral part of the Ruby
|
127
137
|
runtime.
|
128
138
|
|
129
|
-
|
139
|
+
## Why is Polyphony not split into multiple gems?
|
130
140
|
|
131
141
|
Polyphony is currently at an experimental stage, and its different APIs are
|
132
142
|
still in flux. For that reason, all the different parts of Polyphony are
|
@@ -134,7 +144,7 @@ currently kept in a single gem. Once things stabilize, and as Polyphony
|
|
134
144
|
approaches version 1.0, it will be split into separate gems, each with its own
|
135
145
|
functionality.
|
136
146
|
|
137
|
-
|
147
|
+
## Can I use Polyphony in a multithreaded program?
|
138
148
|
|
139
149
|
Yes, as of version 0.27 Polyphony implements per-thread fiber-scheduling. It is
|
140
150
|
however important to note that Polyphony places the emphasis on a multi-fiber
|
@@ -148,7 +158,7 @@ are such a better fit for I/O bound Ruby programs. Threads should really be used
|
|
148
158
|
when performing synchronous operations that are not fiber-aware, such as running
|
149
159
|
an expensive SQLite query, or some other expensive system call.
|
150
160
|
|
151
|
-
|
161
|
+
## How Does Polyphony Fit Into the Ruby's Future Concurrency Plans
|
152
162
|
|
153
163
|
To our understanding, two things are currently on the horizon when it comes to
|
154
164
|
concurrency in Ruby: [auto-fibers](https://bugs.ruby-lang.org/issues/13618), and
|
@@ -167,18 +177,18 @@ Polyphony's fiber-based concurrency model. Guilds will allow true parallelism
|
|
167
177
|
and together with Polyphony will allow taking full advantage of multiple CPU
|
168
178
|
cores in a single Ruby process.
|
169
179
|
|
170
|
-
|
180
|
+
## Can I run Rails using Polyphony?
|
171
181
|
|
172
182
|
We haven't yet tested Rails with Polyphony, but most probably not. We do plan to
|
173
183
|
support running Rails in an eventual release.
|
174
184
|
|
175
|
-
|
185
|
+
## How can I contribute to Polyphony?
|
176
186
|
|
177
187
|
The Polyphony repository is at
|
178
188
|
[https://github.com/digital-fabric/polyphony](https://github.com/digital-fabric/polyphony).
|
179
189
|
Feel free to create issues and contribute pull requests.
|
180
190
|
|
181
|
-
|
191
|
+
## Who is behind this project?
|
182
192
|
|
183
193
|
I'm Sharon Rosner, an independent software developer living in France. Here's my
|
184
194
|
[github profile](https://github.com/ciconia). You can contact me by writing to
|
@@ -1,11 +1,8 @@
|
|
1
1
|
---
|
2
2
|
layout: page
|
3
3
|
title: Installing Polyphony
|
4
|
-
nav_order: 1
|
5
4
|
parent: Getting Started
|
6
|
-
|
7
|
-
prev_title: Home
|
8
|
-
next_title: Tutorial
|
5
|
+
nav_order: 1
|
9
6
|
---
|
10
7
|
# Installing Polyphony
|
11
8
|
|
@@ -13,8 +10,7 @@ next_title: Tutorial
|
|
13
10
|
|
14
11
|
In order to use Polyphony you need to have:
|
15
12
|
|
16
|
-
- Linux or MacOS (
|
17
|
-
being)
|
13
|
+
- Linux or MacOS (support for Windows will come at a later stage)
|
18
14
|
- Ruby (MRI) 2.6 or newer
|
19
15
|
|
20
16
|
## Installing the Polyphony Gem
|
@@ -0,0 +1,486 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Overview
|
4
|
+
parent: Getting Started
|
5
|
+
nav_order: 2
|
6
|
+
---
|
7
|
+
|
8
|
+
# Polyphony - an Overview
|
9
|
+
{: .no_toc }
|
10
|
+
|
11
|
+
## Table of contents
|
12
|
+
{: .no_toc .text-delta }
|
13
|
+
|
14
|
+
- TOC
|
15
|
+
{:toc}
|
16
|
+
|
17
|
+
---
|
18
|
+
|
19
|
+
## Introduction
|
20
|
+
|
21
|
+
Polyphony is a new Ruby library for building concurrent applications in Ruby.
|
22
|
+
Polyphony provides a comprehensive, structured concurrency model based on Ruby
|
23
|
+
fibers and using libev as a high-performance event reactor.
|
24
|
+
|
25
|
+
Polyphony is designed to maximize developer happiness. It provides a natural and
|
26
|
+
fluent API for writing concurrent Ruby apps while using the stock Ruby APIs such
|
27
|
+
as `IO`, `Process`, `Socket`, `OpenSSL` and `Net::HTTP` in a concurrent
|
28
|
+
multi-fiber environment. In addition, Polyphony offers a solid
|
29
|
+
exception-handling experience that builds on and enhances Ruby's
|
30
|
+
exception-handling mechanisms.
|
31
|
+
|
32
|
+
Polyphony includes a full-blown HTTP server implementation with integrated
|
33
|
+
support for HTTP 1 & 2, WebSockets, TLS/SSL termination and more. Polyphony also
|
34
|
+
provides fiber-aware adapters for connecting to PostgreSQL and Redis servers.
|
35
|
+
More adapters are being actively developed.
|
36
|
+
|
37
|
+
### Features
|
38
|
+
{: .no_toc }
|
39
|
+
|
40
|
+
- Co-operative scheduling of concurrent tasks using Ruby fibers.
|
41
|
+
- High-performance event reactor for handling I/O, timer, and other events.
|
42
|
+
- Natural, sequential programming style that makes it easy to reason about
|
43
|
+
concurrent code.
|
44
|
+
- Abstractions and constructs for controlling the execution of concurrent code:
|
45
|
+
supervisors, cancel scopes, throttling, resource pools etc.
|
46
|
+
- Code can use native networking classes and libraries, growing support for
|
47
|
+
third-party gems such as pg and redis.
|
48
|
+
- Use stdlib classes such as TCPServer and TCPSocket and Net::HTTP.
|
49
|
+
- Impressive performance and scalability characteristics, in terms of both
|
50
|
+
throughput and memory consumption (see below)
|
51
|
+
|
52
|
+
## Taking Polyphony for a Spin
|
53
|
+
|
54
|
+
Polyphony is different from other reactor-based solutions for Ruby in that
|
55
|
+
there's no need to use special classes for building your app, and there's no
|
56
|
+
need to setup reactor loops. Everything works the same except you can perform
|
57
|
+
multiple operations at the same time by creating fibers. In order to start a new
|
58
|
+
concurrent operation, you simply use `Kernel#spin`, which spins up a new fiber
|
59
|
+
and schedules it for running:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
require 'polyphony'
|
63
|
+
|
64
|
+
# Kernel#spin returns a Fiber instance
|
65
|
+
counter = spin do
|
66
|
+
count = 1
|
67
|
+
loop do
|
68
|
+
sleep 1
|
69
|
+
puts "count: #{count}"
|
70
|
+
count += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
puts "Press return to stop this program"
|
75
|
+
gets
|
76
|
+
```
|
77
|
+
|
78
|
+
The above program spins up a fiber named `counter`, which counts to infinity.
|
79
|
+
Meanwhile, the *main* fiber waits for input from the user, and then exits.
|
80
|
+
Notice how we haven't introduced any custom classes, and how we used stock APIs
|
81
|
+
such as `Kernel#sleep` and `Kernel#gets`. The only hint that this program is
|
82
|
+
concurrent is the call to `Kernel#spin`.
|
83
|
+
|
84
|
+
Behind the scenes, Polyphony takes care of automatically switching between
|
85
|
+
fibers, letting each fiber advance at its own pace according to its duties. For
|
86
|
+
example, when the main fiber calls `gets`, Polyphony starts waiting for data to
|
87
|
+
come in on `STDIN` and then switches control to the `counter` fiber. When the
|
88
|
+
`counter` fiber calls `sleep 1`, Polyphony starts a timer, and goes looking for
|
89
|
+
other work. If no other fiber is ready to run, Polyphony simply waits for at
|
90
|
+
least one event to occur, and then resumes the corresponding fiber.
|
91
|
+
|
92
|
+
## Fibers vs Threads
|
93
|
+
|
94
|
+
Most Ruby developers are familiar with threads, but fibers remain a little
|
95
|
+
explored and little understood concept in the Ruby language. While A thread is
|
96
|
+
an OS abstraction that is controlled by the OS, a fiber represents an execution
|
97
|
+
context that can be paused and resumed by the application, and has no
|
98
|
+
counterpart at the OS level.
|
99
|
+
|
100
|
+
When used for writing concurrent programming, fibers offer multiple benefits
|
101
|
+
over threads. They consume much less RAM than threads, and switching between
|
102
|
+
them is faster than switching between threads. In addition, since fibers require
|
103
|
+
no cooperation from the OS, an application can create literally millions of them
|
104
|
+
given enough RAM. Those advantages make fibers a compelling solution for creating
|
105
|
+
pervasively concurrent applications, even when using a dynamic high-level "slow"
|
106
|
+
language such as Ruby.
|
107
|
+
|
108
|
+
Ruby programs will only partly benefit from using mutiple threads for processing
|
109
|
+
work loads (due to the GVL), but fibers are a great match mostly for programs
|
110
|
+
that are I/O bound (that means spending most of their time talking to the
|
111
|
+
outside world). A fiber-based web-server, for example, can juggle thousands of
|
112
|
+
active concurrent connections, each advancing at its own pace, consuming only a
|
113
|
+
single CPU core.
|
114
|
+
|
115
|
+
Nevertheless, Polyphony fully supports multithreading, with each thread having
|
116
|
+
its own fiber run queue and its own libev event loop. In addition, Polyphony
|
117
|
+
enables cross-thread communication using
|
118
|
+
|
119
|
+
## Fibers vs Callbacks
|
120
|
+
|
121
|
+
Programming environments such as Node.js and libraries such as EventMachine have
|
122
|
+
popularized the usage of event loops for achieving concurrency. The application
|
123
|
+
is wrapped in a loop that polls for events and fires application-provided
|
124
|
+
callbacks that act on those events - for example receiving data on a socket
|
125
|
+
connection, or waiting for a timer to elapse.
|
126
|
+
|
127
|
+
While these callback-based solutions are established technologies and are used
|
128
|
+
frequently to build concurrent apps, they do have some major drawbacks. Firstly,
|
129
|
+
they force the developer to split the business logic into small pieces, each
|
130
|
+
being ran inside of a callback. Secondly, they complicate state management,
|
131
|
+
because state associated with the business logic cannot be kept *with* the
|
132
|
+
business logic, it has to be stored elsewhere. Finally, callback-based
|
133
|
+
concurrency complicates debugging, since a stacktrace at any given point in time
|
134
|
+
will always originate in the event loop, and will not contain any information on
|
135
|
+
the chain of events leading to the present moment.
|
136
|
+
|
137
|
+
Fibers, in contrast, let the developer express the business logic in a
|
138
|
+
sequential, easy to read manner: do this, then that. State can be stored right
|
139
|
+
in the business logic, as local variables. And finally, the sequential
|
140
|
+
programming style makes it much easier to debug your code, since stack traces
|
141
|
+
contain the entire history of execution from the app's inception.
|
142
|
+
|
143
|
+
## Structured Concurrency
|
144
|
+
|
145
|
+
Polyphony's tagline is "fine-grained concurrency for Ruby", because it makes it
|
146
|
+
really easy to spin up literally thousands of fibers that perform concurrent
|
147
|
+
work. But running such a large number of concurrent operations also means you
|
148
|
+
need tools for managing all that concurrency.
|
149
|
+
|
150
|
+
For that purpose, Polyphony follows a paradigm called *structured concurrency*.
|
151
|
+
The basic idea behind structured concurrency is that fibers are organised in a
|
152
|
+
hierarchy starting from the main fiber. A fiber spun by any given fiber is
|
153
|
+
considered a child of that fiber, and its lifetime is guaranteed to be limited
|
154
|
+
to that of its parent fiber. That is why in the example above, the `counter`
|
155
|
+
fiber is automatically stopped when the main fiber stops running.
|
156
|
+
|
157
|
+
The same goes for exception handling. Whenever an error occurs, if no suitable
|
158
|
+
`rescue` block has been defined for the fiber in which the exception was raised,
|
159
|
+
the exception will bubble up through the fiber's parent, grandparent etc, until
|
160
|
+
the exception is handled, up to the main fiber. If the exception was not
|
161
|
+
handled, the program will exit and dump the exception information just like a
|
162
|
+
normal Ruby program.
|
163
|
+
|
164
|
+
## Controlling Fiber Execution
|
165
|
+
|
166
|
+
Polyphony offers a wide range of APIs for controlling fibers that make it easy
|
167
|
+
to prevent your program turning into an incontrollable concurrent mess. In order
|
168
|
+
to control fibers, Polyphony introduces various APIs for stopping fibers,
|
169
|
+
scheduling fibers, awaiting for fibers to terminate, and even restarting them:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
f = spin do
|
173
|
+
puts "going to sleep"
|
174
|
+
sleep 1
|
175
|
+
puts "done sleeping"
|
176
|
+
ensure
|
177
|
+
puts "stopped"
|
178
|
+
end
|
179
|
+
|
180
|
+
sleep 0.5
|
181
|
+
f.stop
|
182
|
+
f.restart
|
183
|
+
f.await
|
184
|
+
```
|
185
|
+
|
186
|
+
The output of the above program will be:
|
187
|
+
|
188
|
+
```
|
189
|
+
going to sleep
|
190
|
+
stopped
|
191
|
+
going to sleep
|
192
|
+
done sleeping
|
193
|
+
stopped
|
194
|
+
```
|
195
|
+
|
196
|
+
The `Fiber#await` method waits for a fiber to terminate, and returns the fiber's
|
197
|
+
return value:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
a = spin { sleep 1; :foo }
|
201
|
+
b = spin { a.await }
|
202
|
+
b.await #=> :foo
|
203
|
+
```
|
204
|
+
|
205
|
+
In the program above the main fiber waits for fiber `b` to terminate, and `b`
|
206
|
+
waits for fiber `a` to terminate. The return value of `a.await` is `:foo`, and
|
207
|
+
hence the return value of `b.await` is also `foo`.
|
208
|
+
|
209
|
+
If we need to wait for multiple fibers, we can use `Fiber::await` or
|
210
|
+
`Fiber::select`:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
# get result of a bunch of fibers
|
214
|
+
fibers = 3.times.map { |i| spin { i * 10 } }
|
215
|
+
Fiber.await(*fibers) #=> [0, 10, 20]
|
216
|
+
|
217
|
+
# get the fastest reply of a bunch of URLs
|
218
|
+
fibers = urls.map { |u| spin { [u, HTTParty.get(u)] } }
|
219
|
+
# Fiber.select returns an array containing the fiber and its result
|
220
|
+
Fiber.select(*fibers) #=> [fiber, [url, result]]
|
221
|
+
```
|
222
|
+
|
223
|
+
Finally, fibers can be supervised, in a similar manner to Erlang supervision
|
224
|
+
trees. The `Kernel#supervise` method will wait for all child fibers to terminate
|
225
|
+
before returning, and can optionally restart any child fiber that has terminated
|
226
|
+
normally or with an exception:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
fiber1 = spin { sleep 1; raise 'foo' }
|
230
|
+
fiber2 = spin { sleep 1 }
|
231
|
+
|
232
|
+
supervise # blocks and then propagates the error raised in fiber1
|
233
|
+
```
|
234
|
+
|
235
|
+
## Message Passing
|
236
|
+
|
237
|
+
Polyphony also provides a comprehensive solution for using fibers as actors, in
|
238
|
+
a similar fashion to Erlang processes. Fibers can exchange messages between each
|
239
|
+
other, allowing each part of a concurrent system to function in a completely
|
240
|
+
autonomous manner. For example, a chat application can encapsulate each chat
|
241
|
+
room in a completely self-contained fiber:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
def chat_room
|
245
|
+
subscribers = []
|
246
|
+
|
247
|
+
loop do
|
248
|
+
# receive waits for a message to come in
|
249
|
+
case receive
|
250
|
+
# Using Ruby 2.7's pattern matching
|
251
|
+
in [:subscribe, subscriber]
|
252
|
+
subscribers << subscriber
|
253
|
+
in [:unsubscribe, subscriber]
|
254
|
+
subscribers.delete subscriber
|
255
|
+
in [:add_message, name, message]
|
256
|
+
subscribers.each { |s| s.call(name, message) }
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
CHAT_ROOMS = Hash.new do |h, n|
|
262
|
+
h[n] = spin { chat_room }
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
Notice how the state (the `subscribers` variable) stays local, and how the logic
|
267
|
+
of the chat room is expressed in a way that is both compact and easy to extend.
|
268
|
+
Also notice how the chat room is written as an infinite loop. This is a common
|
269
|
+
pattern in Polyphony, since fibers can always be stopped at any moment.
|
270
|
+
|
271
|
+
The code for handling a chat room user might be expressed as follows:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
def chat_user_handler(user_name, connection)
|
275
|
+
room = nil
|
276
|
+
message_subscriber = proc do |name, message|
|
277
|
+
connection.puts "#{name}: #{message}"
|
278
|
+
end
|
279
|
+
while command = connection.gets
|
280
|
+
case command
|
281
|
+
when /^connect (.+)/
|
282
|
+
room&.send [:unsubscribe, message_subscriber]
|
283
|
+
room = CHAT_ROOMS[$1]
|
284
|
+
when "disconnect"
|
285
|
+
room&.send [:unsubscribe, message_subscriber]
|
286
|
+
room = nil
|
287
|
+
when /^send (.+)/
|
288
|
+
room&.send [:add_message, user_name, $1]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## Other Concurrency Constructs
|
295
|
+
|
296
|
+
Polyphony includes various constructs that complement fibers. Resource pools
|
297
|
+
provide a generic solution for controlling concurrent access to limited
|
298
|
+
resources, such as database connections. A resource pool assures only one fiber
|
299
|
+
has access to a given resource at any time:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
DB_CONNECTIONS = Polyphony::ResourcePool.new(limit: 5) do
|
303
|
+
PG.connect(DB_OPTS)
|
304
|
+
end
|
305
|
+
|
306
|
+
def query_records(sql)
|
307
|
+
DB_CONNECTIONS.acquire do |db|
|
308
|
+
db.query(sql).to_a
|
309
|
+
end
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
Throttlers can be useful for rate limiting, for example preventing blacklisting
|
314
|
+
your system in case it sends too many emails, even across fibers:
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
MAX_EMAIL_RATE = 10 # max. 10 emails per second
|
318
|
+
EMAIL_THROTTLER = Polyphony::Throttler.new(MAX_EMAIL_RATE)
|
319
|
+
|
320
|
+
def send_email(addr, content)
|
321
|
+
EMAIL_THROTTLER.process do
|
322
|
+
...
|
323
|
+
end
|
324
|
+
end
|
325
|
+
```
|
326
|
+
|
327
|
+
In addition, various global methods (defined on the `Kernel` module) provide
|
328
|
+
common functionality, such as using timeouts:
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
# perform an delayed action (in a separate fiber)
|
332
|
+
after(10) { notify_user }
|
333
|
+
|
334
|
+
# perform a recurring action with time drift correction
|
335
|
+
every(1) { p Time.now }
|
336
|
+
|
337
|
+
# perform an operation with timeout without raising an exception
|
338
|
+
move_on_after(10) { perform_query }
|
339
|
+
|
340
|
+
# perform an operation with timeout, raising a Polyphony::Cancel exception
|
341
|
+
cancel_after(10) { perform_query }
|
342
|
+
```
|
343
|
+
|
344
|
+
## The System Agent
|
345
|
+
|
346
|
+
In order to implement automatic fiber switching when performing blocking
|
347
|
+
operations, Polyphony introduces a concept called the *system agent*. The system
|
348
|
+
agent is an object having a uniform interface, that performs all blocking
|
349
|
+
operations.
|
350
|
+
|
351
|
+
While a standard event loop-based solution would implement a blocking call
|
352
|
+
separately from the fiber scheduling, the system agent integrates the two to
|
353
|
+
create a blocking call that is already knows how to switch and schedule fibers.
|
354
|
+
For example, in Polyphony all APIs having to do with reading from files or
|
355
|
+
sockets end up calling `Thread.current.agent.read`, which does all the work.
|
356
|
+
|
357
|
+
This design offers some major advantages over other designs. It minimizes memory
|
358
|
+
allocations, of both Ruby objects and C structures. For example, instead of
|
359
|
+
having to allocate libev watchers on the heap and then pass them around, they
|
360
|
+
are allocated on the stack instead, which saves up on both memory and CPU cycles.
|
361
|
+
|
362
|
+
In addition, the agent interface includes two methods that allow maximizing
|
363
|
+
server performance by accepting connections and reading from sockets in a tight
|
364
|
+
loop. Here's a naive implementation of an HTTP/1 server:
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
require 'http/parser'
|
368
|
+
require 'polyphony'
|
369
|
+
|
370
|
+
def handle_client(socket)
|
371
|
+
parser = Http::Parser.new
|
372
|
+
reqs = []
|
373
|
+
parser.on_message_complete = proc { |env| reqs << { foo: :bar } }
|
374
|
+
|
375
|
+
Thread.current.agent.read_loop(socket) do |data|
|
376
|
+
parser << data
|
377
|
+
reqs.each { |r| reply(socket, r) }
|
378
|
+
reqs.clear
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def reply(socket)
|
383
|
+
data = "Hello world!\n"
|
384
|
+
headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
|
385
|
+
socket.write "HTTP/1.1 200 OK\r\n#{headers}\r\n#{data}"
|
386
|
+
end
|
387
|
+
|
388
|
+
server = TCPServer.open('0.0.0.0', 1234)
|
389
|
+
puts "listening on port 1234"
|
390
|
+
|
391
|
+
Thread.current.agent.accept_loop(server) do |client|
|
392
|
+
spin { handle_client(client) }
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
The `#read_loop` and `#accept_loop` agent methods implement tight loops that
|
397
|
+
provide a significant boost to performance (up to +30% better throughput.)
|
398
|
+
|
399
|
+
Currently, Polyphony includes a single system agent based on
|
400
|
+
[libev](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod). In the future,
|
401
|
+
Polyphony will include other platform-specific system agents, such as a Windows
|
402
|
+
agent using
|
403
|
+
[IOCP](https://docs.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports),
|
404
|
+
or an [io_uring](https://unixism.net/loti/what_is_io_uring.html) agent,
|
405
|
+
which might be a game-changer for writing highly-concurrent Ruby-based web apps.
|
406
|
+
|
407
|
+
## Writing Web Apps with Polyphony
|
408
|
+
|
409
|
+
Polyphony includes a full-featured web server implementation that supports
|
410
|
+
HTTP/1, HTTP/2, and WebSockets, can perform SSL termination (with automatic ALPN
|
411
|
+
protocol selection), and has preliminary support for Rack (the de-facto standard
|
412
|
+
Ruby web app interface).
|
413
|
+
|
414
|
+
The Polyphony HTTP server has a unique design that calls the application's
|
415
|
+
request handler after all request headers have been received. This allows the
|
416
|
+
application to better deal with slow client attacks, big file uploads, and also
|
417
|
+
to minimize costly memory allocation and GC'ing.
|
418
|
+
|
419
|
+
Benchmarks will be included here at a later time.
|
420
|
+
|
421
|
+
## Integrating Polyphony with other Gems
|
422
|
+
|
423
|
+
Polyphony aims to be a comprehensive concurrency solution for Ruby, and to
|
424
|
+
enable developers to use a maximum of core and stdlib APIs transparently in a
|
425
|
+
multi-fiber envrionment. Polyphony also provides adapters for common gems such
|
426
|
+
as postgres and redis, allowing using those gems in a fiber-aware manner.
|
427
|
+
|
428
|
+
For gems that do not yet have a fiber-aware adapter, Polyphony offers a general
|
429
|
+
solution in the form of a thread pool. A thread pool lets you offload blocking
|
430
|
+
method calls (that block the entire thread) onto worker threads, letting you
|
431
|
+
continue with other work while waiting for the call to return. For example,
|
432
|
+
here's how an `sqlite` adapter might work:
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
class SQLite3::Database
|
436
|
+
THREAD_POOL = Polyphony::ThreadPool.new
|
437
|
+
|
438
|
+
alias_method :orig_execute, :execute
|
439
|
+
def execute(sql, *args)
|
440
|
+
THREAD_POOL.process { orig_execute(sql, *args) }
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
Other cases might require converting a callback-based interface into a blocking
|
446
|
+
fiber-aware one. Here's (a simplified version of) how Polyphony uses the
|
447
|
+
callback-based `http_parser.rb` gem to parse incoming HTTP/1 requests:
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
class HTTP1Adapter
|
451
|
+
...
|
452
|
+
|
453
|
+
def on_headers_complete(headers)
|
454
|
+
@pending_requests << Request.new(headers, self)
|
455
|
+
end
|
456
|
+
|
457
|
+
def each(&block)
|
458
|
+
while (data = @connection.readpartial(8192))
|
459
|
+
# feed parser
|
460
|
+
@parser << data
|
461
|
+
while (request = @pending_requests.shift)
|
462
|
+
block.call(request)
|
463
|
+
return unless request.keep_alive?
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
...
|
469
|
+
end
|
470
|
+
```
|
471
|
+
|
472
|
+
In the code snippet above, the solution is quite simple. The fiber handling the
|
473
|
+
connection loops waiting for data to be read from the socket. Once the data
|
474
|
+
arrives, it is fed to the HTTP parser. The HTTP parser will call the
|
475
|
+
`on_headers_complete` callback, which simply adds a request to the requests
|
476
|
+
queue. The code then continues to handle any requests still in the queue.
|
477
|
+
|
478
|
+
## Future Directions
|
479
|
+
|
480
|
+
Polyphony is a young project, and will still need a lot of development effort to
|
481
|
+
reach version 1.0. Here are some of the exciting directions we're working on.
|
482
|
+
|
483
|
+
- Support for more core and stdlib APIs
|
484
|
+
- More adapters for gems with C-extensions, such as `mysql`, `sqlite3` etc
|
485
|
+
- Use `io_uring` agent as alternative to the libev agent
|
486
|
+
- More concurrency constructs for building highly concurrent applications
|