polyphony 0.27 → 0.28
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 +4 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile +12 -1
- data/Gemfile.lock +83 -5
- data/Rakefile +4 -0
- data/TODO.md +11 -20
- data/docs/_config.yml +15 -0
- data/docs/_includes/nav.html +47 -0
- data/docs/_sass/custom/custom.scss +5 -0
- data/docs/_sass/overrides.scss +45 -0
- data/docs/assets/img/echo-fibers.svg +1 -0
- data/docs/assets/img/sleeping-fiber.svg +1 -0
- data/docs/faq.md +182 -0
- data/docs/getting-started/installing.md +10 -2
- data/docs/getting-started/tutorial.md +333 -26
- data/docs/getting-started.md +10 -0
- data/docs/index.md +91 -0
- data/docs/technical-overview/concurrency.md +78 -16
- data/docs/technical-overview/design-principles.md +7 -0
- data/docs/technical-overview/exception-handling.md +57 -9
- data/docs/technical-overview/extending.md +7 -0
- data/docs/technical-overview/fiber-scheduling.md +128 -18
- data/docs/technical-overview.md +10 -0
- data/docs/user-guide/web-server.md +7 -0
- data/docs/user-guide.md +10 -0
- data/examples/core/xx-deadlock.rb +8 -0
- data/examples/core/xx-state-machine.rb +51 -0
- data/examples/core/xx-trace.rb +80 -0
- data/examples/interfaces/pg_notify.rb +35 -0
- data/examples/io/xx-httparty.rb +31 -6
- data/examples/io/xx-irb.rb +1 -11
- data/examples/io/xx-switch.rb +15 -0
- data/ext/gyro/gyro.c +77 -38
- data/ext/gyro/gyro.h +15 -5
- data/ext/gyro/gyro_ext.c +3 -0
- data/ext/gyro/thread.c +47 -32
- data/ext/gyro/tracing.c +11 -0
- data/lib/polyphony/core/global_api.rb +11 -4
- data/lib/polyphony/core/supervisor.rb +1 -0
- data/lib/polyphony/core/thread_pool.rb +44 -35
- data/lib/polyphony/extensions/fiber.rb +19 -9
- data/lib/polyphony/extensions/io.rb +14 -14
- data/lib/polyphony/extensions/socket.rb +3 -3
- data/lib/polyphony/irb.rb +13 -0
- data/lib/polyphony/postgres.rb +15 -0
- data/lib/polyphony/trace.rb +98 -0
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +1 -0
- data/polyphony.gemspec +21 -12
- data/test/helper.rb +3 -2
- data/test/test_fiber.rb +53 -3
- data/test/test_global_api.rb +12 -0
- data/test/test_gyro.rb +2 -2
- data/test/test_supervisor.rb +12 -0
- data/test/test_thread.rb +12 -0
- data/test/test_thread_pool.rb +75 -0
- data/test/test_throttler.rb +6 -0
- data/test/test_trace.rb +66 -0
- metadata +99 -9
- data/docs/README.md +0 -36
- data/docs/summary.md +0 -60
- data/docs/technical-overview/faq.md +0 -97
@@ -1,9 +1,143 @@
|
|
1
|
-
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: A Gentle Introduction to Polyphony
|
4
|
+
nav_order: 2
|
5
|
+
parent: Getting Started
|
6
|
+
permalink: /getting-started/tutorial/
|
7
|
+
---
|
8
|
+
# A Gentle Introduction to Polyphony
|
9
|
+
|
10
|
+
Polyphony is a new Ruby library aimed at making writing concurrent Ruby apps
|
11
|
+
easy and fun. In this article, we'll introduce Polyphony's fiber-based
|
12
|
+
concurrency model, some of Polyphony's API, and demonstrate how to solve some
|
13
|
+
simple situations related to concurrent computing.
|
14
|
+
|
15
|
+
## What are Fibers and What are They Good For?
|
16
|
+
|
17
|
+
Fibers are some of Ruby's most underappreciated hidden gems. Up until now,
|
18
|
+
fibers have been used mostly as the underlying mechanism for implementing
|
19
|
+
lazy enumerators and asynchronous generators. Fibers encapsulate, in short,
|
20
|
+
an execution context that can be paused and resumed at will.
|
21
|
+
|
22
|
+
Fibers are also at the heart of Polyphony's concurrency model. Polyphony employs
|
23
|
+
fibers as a way to run multiple tasks at once, each task advancing at its own
|
24
|
+
pace, pausing when waiting for an event to occur, and automatically resuming
|
25
|
+
when that event has occurred.
|
26
|
+
|
27
|
+
Take for example a web app: in order to fulfil an incoming request, multiple
|
28
|
+
steps are required: querying the database, fetching cached entries from Redis,
|
29
|
+
talking to third-party services such as Twilio or AWS S3. Each step can last
|
30
|
+
tens of milliseconds, and blocks the current thread. Such an app is said to be
|
31
|
+
I/O-bound, that is, it mostly spends its time waiting for some external
|
32
|
+
services.
|
33
|
+
|
34
|
+
The traditional approach to handling multiple requests concurrently is to employ
|
35
|
+
multiple threads or processes, but this approach has numerous disavantages:
|
36
|
+
|
37
|
+
- Both threads and processes are heavyweight, in both memory consmption and
|
38
|
+
the cost associated with context-switching.
|
39
|
+
- Threads introduce hard-to-debug race conditions, and do not offer true
|
40
|
+
parallelism, owing to Ruby's GVL.
|
41
|
+
- Processes are more difficult to coordinate, since they do not share memory.
|
42
|
+
- Both threads and processes are limited to a few thousand at best on a single
|
43
|
+
machine. Trying to spawn a thread per client essentially limits the scaling
|
44
|
+
capacity of your system.
|
45
|
+
|
46
|
+
Polyphony eschews both threads and processes in favor of fibers as the basic
|
47
|
+
unit of concurrency. The idea is that any time a blocking I/O operation occurs,
|
48
|
+
the current fiber is paused, and another fiber which has been marked as
|
49
|
+
*runnable* is resumed. This way, your Ruby code can keep on handling incoming
|
50
|
+
HTTP requests as they come with a scaling capacity that is virtually only
|
51
|
+
limited by available memory.
|
52
|
+
|
53
|
+
## Switchpoints and the Fiber-Switching Dance
|
54
|
+
|
55
|
+
In order to make pausing and resuming fibers completely automatic and painfree,
|
56
|
+
we need to know when an operation is going to block, and when it can be
|
57
|
+
completed without blocking. Operations that might block execution are considered
|
58
|
+
*switchpoints*. A switchpoint is a point in time at which control might switch
|
59
|
+
from the currently running fiber to another fiber that is in a runnable state.
|
60
|
+
Switchpoints may occur in any of the following cases:
|
61
|
+
|
62
|
+
- On a call to any blocking operation, such as `#sleep`, `Fiber#await`,
|
63
|
+
`Thread#join` etc.
|
64
|
+
- On fiber termination
|
65
|
+
- On a call to `#suspend`
|
66
|
+
- On a call to `#snooze`
|
67
|
+
- On a call to `Thread#switch_fiber`
|
68
|
+
|
69
|
+
At any switchpoint, the following takes place:
|
70
|
+
|
71
|
+
- Check if any fiber is runnable, that is, ready to continue processing.
|
72
|
+
- If no fiber is runnable, watch for events (see below) and wait for at least
|
73
|
+
one fiber to become runnable.
|
74
|
+
- Pause the current fiber and switch to the first runnable fiber, which resumes
|
75
|
+
at the point it was last paused.
|
76
|
+
|
77
|
+
The automatic switching between fibers is complemented by employing
|
78
|
+
[libev](http://software.schmorp.de/pkg/libev.html), a multi-platform high
|
79
|
+
performance event reactor that allows listening to I/O, timer and other events.
|
80
|
+
At every switchpoint where no fibers are runnable, the libev evet loop is run
|
81
|
+
until events occur, which in turn cause the relevant fibers to become runnable.
|
82
|
+
|
83
|
+
Let's examine a simple example:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
require 'polyphony'
|
87
|
+
|
88
|
+
spin do
|
89
|
+
puts "Going to sleep..."
|
90
|
+
sleep 1
|
91
|
+
puts "Woke up"
|
92
|
+
end
|
93
|
+
|
94
|
+
suspend
|
95
|
+
puts "We're done"
|
96
|
+
```
|
97
|
+
|
98
|
+
The above program does nothing exceptional, it just sleeps for 1 second and
|
99
|
+
prints a bunch of messages. But it is enough to demonstrate how concurrency
|
100
|
+
works in Polyphony. Here's a flow chart of the transfer of control:
|
101
|
+
|
102
|
+
<p class="img-figure"><img src="../../assets/img/sleeping-fiber.svg"></p>
|
103
|
+
|
104
|
+
Here's the actual sequence of execution (in pseudo-code)
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# (main fiber)
|
108
|
+
sleeper = spin { ... } # The main fiber spins up a new fiber marked as runnable
|
109
|
+
suspend # The main fiber suspends, waiting for all other work to finish
|
110
|
+
Thread.current.switch_fiber # Polyphony looks for other runnable fibers
|
111
|
+
|
112
|
+
# (sleeper fiber)
|
113
|
+
puts "Going to sleep..." # The sleeper fiber starts running
|
114
|
+
sleep 1 # The sleeper fiber goes to sleep
|
115
|
+
Gyro::Timer.new(1, 0).await # A timer event watcher is setup and yields
|
116
|
+
Thread.current.switch_fiber # Polyphony looks for other runnable fibers
|
117
|
+
Thread.current.selector.run # With no work left, the event loop is ran
|
118
|
+
fiber.schedule # The timer event fires, scheduling the sleeper fiber
|
119
|
+
# <= The sleep method returns
|
120
|
+
puts "Woke up"
|
121
|
+
Thread.current.switch_fiber # With the fiber done, Polyphony looks for work
|
122
|
+
|
123
|
+
# with no more work, control is returned to the main fiber
|
124
|
+
# (main fiber)
|
125
|
+
# <=
|
126
|
+
# With no more work left, the main fiber is resumed and the suspend call returns
|
127
|
+
puts "We're done"
|
128
|
+
```
|
129
|
+
|
130
|
+
What we have done in fact is we multiplexed two different contexts of execution
|
131
|
+
(fibers) onto a single thread, each fiber continuing at its own pace and
|
132
|
+
yielding control when waiting for something to happen. This context-switching
|
133
|
+
dance, performed automatically by Polyphony behind the scenes, enables building
|
134
|
+
highly-concurrent Ruby apps, with minimal impact on performance.
|
2
135
|
|
3
136
|
## Building a Simple Echo Server with Polyphony
|
4
137
|
|
5
|
-
|
6
|
-
|
138
|
+
Let's now turn our attention to something a bit more useful: a concurrent echo
|
139
|
+
server. Our server will accept TCP connections and send back whatever it receives
|
140
|
+
from the client.
|
7
141
|
|
8
142
|
We'll start by opening a server socket:
|
9
143
|
|
@@ -42,7 +176,7 @@ continue to run until the call to `#handle_client` returns, and that will not
|
|
42
176
|
happen as long as the read loop is not done.
|
43
177
|
|
44
178
|
Fortunately, Polyphony makes it super easy to do more than one thing at once.
|
45
|
-
Let's spin up a separate
|
179
|
+
Let's spin up a separate fiber for each client:
|
46
180
|
|
47
181
|
```ruby
|
48
182
|
while (client = server.accept)
|
@@ -50,31 +184,33 @@ while (client = server.accept)
|
|
50
184
|
end
|
51
185
|
```
|
52
186
|
|
53
|
-
Now, our little program can handle
|
54
|
-
little sprinkling of `spin`.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
187
|
+
Now, our little program can effectively handle thousands of clients, all with a
|
188
|
+
little sprinkling of `spin`. The call to `server.accept` suspends the main fiber
|
189
|
+
until a connection is made, allowing other fibers to run while it's waiting.
|
190
|
+
Likewise, the call to `client.gets` suspends the *client's fiber* until incoming
|
191
|
+
data becomes available. Again, all of that is handled automatically by
|
192
|
+
Polyphony, and the only hint that our program is concurrent is that little
|
193
|
+
innocent call to `#spin`.
|
59
194
|
|
60
|
-
|
61
|
-
made, allowing other coprocesses to continue running. Likewise, the call to
|
62
|
-
`client.gets` suspends the *client's coprocess* until incoming data becomes
|
63
|
-
available. All this is handled automatically by Polyphony, and the only hint
|
64
|
-
that our program is concurrent is that innocent call to `spin`.
|
195
|
+
Here's a flow chart showing the transfer of control between the different fibers:
|
65
196
|
|
66
|
-
|
197
|
+
<p class="img-figure"><img src="../../assets/img/echo-fibers.svg"></p>
|
198
|
+
|
199
|
+
Let's consider the advantage of the Polyphony concurrency model:
|
67
200
|
|
68
201
|
- We didn't need to create custom handler classes with callbacks.
|
69
202
|
- We didn't need to use custom classes or APIs for our networking code.
|
70
|
-
- Our code is terse, easy to read and
|
71
|
-
|
203
|
+
- Each task is expressed sequentially. Our code is terse, easy to read and, most
|
204
|
+
importantly, expresses the order of events clearly and without having our
|
205
|
+
logic split across different methods.
|
206
|
+
- We have a server that can scale to thousands of clients without breaking a
|
207
|
+
sweat.
|
72
208
|
|
73
209
|
## Handling Inactive Connections
|
74
210
|
|
75
211
|
Now that we have a working concurrent echo server, let's add some bells and
|
76
212
|
whistles. First of all, let's get rid of clients that are not active. We'll do
|
77
|
-
this by using a Polyphony construct called a cancel scope. Cancel
|
213
|
+
this by using a Polyphony construct called a cancel scope. Cancel scopes define
|
78
214
|
an execution context that can cancel any operation ocurring within its scope:
|
79
215
|
|
80
216
|
```ruby
|
@@ -100,9 +236,10 @@ thus reset the cancel scope timer.
|
|
100
236
|
|
101
237
|
In addition, we use an `ensure` block to make sure the client connection is
|
102
238
|
closed, whether or not it was interrupted by the cancel scope timer. The habit
|
103
|
-
of always cleaning up using `ensure` in the face of interruptions is a
|
104
|
-
fundamental element of using Polyphony. It makes your code robust,
|
105
|
-
highly concurrent execution environment
|
239
|
+
of always cleaning up using `ensure` in the face of potential interruptions is a
|
240
|
+
fundamental element of using Polyphony correctly. It makes your code robust,
|
241
|
+
even in a highly chaotic concurrent execution environment where tasks can be
|
242
|
+
interrupted at any time.
|
106
243
|
|
107
244
|
Here's the complete source code for our Polyphony-based echo server:
|
108
245
|
|
@@ -130,8 +267,178 @@ while (client = server.accept)
|
|
130
267
|
end
|
131
268
|
```
|
132
269
|
|
133
|
-
##
|
270
|
+
## Waiting and Interrupting
|
271
|
+
|
272
|
+
Polyphony makes it very easy to run multiple concurrent fibers. You can
|
273
|
+
basically start a fiber for any operation that involves talking to the outside
|
274
|
+
world - running a database query, making an HTTP request, sending off a webhook
|
275
|
+
invocation etc. While it's trivial to spin off thousands of fibers, we'd also
|
276
|
+
like a way to control all those fibers.
|
277
|
+
|
278
|
+
Polyphony provides a number of tools for controlling fiber execution. Let's
|
279
|
+
examine some of these tools and how they work. Suppose we have a fiber that was
|
280
|
+
previously spun:
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
fiber = spin { do_some_work }
|
284
|
+
```
|
285
|
+
|
286
|
+
We can wait for the fiber to terminate:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
fiber.await # alternatively fiber.join
|
290
|
+
```
|
291
|
+
|
292
|
+
Notice that the await call returns the return value of the fiber block:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
fiber = spin { 2 + 2}
|
296
|
+
fiber.await #=> 4
|
297
|
+
```
|
298
|
+
|
299
|
+
We can also stop the fiber at any point:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
fiber.stop # or fiber.interrupt
|
303
|
+
```
|
304
|
+
|
305
|
+
We can inject a return value for the fiber using `stop`:
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
fiber = spin do
|
309
|
+
sleep 1
|
310
|
+
1 + 1
|
311
|
+
end
|
312
|
+
|
313
|
+
spin { puts "1 + 1 = #{fiber.await} wha?" }
|
314
|
+
|
315
|
+
fiber.stop(3)
|
316
|
+
suspend
|
317
|
+
```
|
318
|
+
|
319
|
+
We can also *cancel* the fiber, which raises a `Polyphony::Cancel` exception:
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
fiber.cancel!
|
323
|
+
```
|
324
|
+
|
325
|
+
And finally, we can interrupt the fiber with an exception raised in its current
|
326
|
+
context:
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
fiber.raise 'foo'
|
330
|
+
```
|
331
|
+
|
332
|
+
For more information on how exceptions are handled in Polyphony, see [exception
|
333
|
+
handling](../../technical-overview/exception-handling/).
|
334
|
+
|
335
|
+
## Supervising - controlling multiple fibers at once
|
336
|
+
|
337
|
+
Here's a simple example that we'll use to demonstrate some of the tools provided
|
338
|
+
by Polyphony for controlling fibers. Let's build a script that fetches the local
|
339
|
+
time for multiple time zones:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
require 'polyphony'
|
343
|
+
require 'httparty'
|
344
|
+
require 'json'
|
345
|
+
|
346
|
+
def get_time(tzone)
|
347
|
+
res = HTTParty.get("http://worldtimeapi.org/api/timezone/#{tzone}")
|
348
|
+
json = JSON.parse(res.body)
|
349
|
+
Time.parse(json['datetime'])
|
350
|
+
end
|
351
|
+
|
352
|
+
zones = %w{
|
353
|
+
Europe/London Europe/Paris Europe/Bucharest America/New_York Asia/Bangkok
|
354
|
+
}
|
355
|
+
zones.each do |tzone|
|
356
|
+
spin do
|
357
|
+
time = get_time(tzone)
|
358
|
+
puts "Time in #{tzone}: #{time}"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
suspend
|
363
|
+
```
|
364
|
+
|
365
|
+
Now that we're familiar with the use of the `#spin` method, we know that all
|
366
|
+
those HTTP requests will be processed concurrently, and we can expect those 5
|
367
|
+
separate requests to occur within a fraction of a second (depending on our
|
368
|
+
machine's location). Also notice how we just used `httparty` with fiber-level
|
369
|
+
concurrency, without any boilerplate or employing special wrapper classes.
|
370
|
+
|
371
|
+
Just as before, we suspend the main fiber after spinning off the worker fibers,
|
372
|
+
in order to wait for everything else to be done. But if we needed to do other
|
373
|
+
work? For example, we might want to collect the different local times into a
|
374
|
+
hash to be processed later. In that case, we can use a `Supervisor`:
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
def get_times(zones)
|
378
|
+
Polyphony::Supervisor.new do |s|
|
379
|
+
zones.each do |tzone|
|
380
|
+
s.spin { [tzone, get_time(tzone)] }
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
get_times(zones).await.each do |tzone, time|
|
386
|
+
puts "Time in #{tzone}: #{time}"
|
387
|
+
end
|
388
|
+
```
|
389
|
+
|
390
|
+
There's quite a bit going on here, so let's break it down. We first construct a
|
391
|
+
supervisor and spin our fibers in its context using `Supervisor#spin`.
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
Polyphony::Supervisor.new do |s|
|
395
|
+
...
|
396
|
+
s.spin { ... }
|
397
|
+
...
|
398
|
+
end
|
399
|
+
```
|
400
|
+
|
401
|
+
Once our worker fibers are spun, the supervisor can be used to control them. We
|
402
|
+
can wait for all fibers to terminate using `Supervisor#await`, which returns an
|
403
|
+
array with the return values of all fibers (in the above example, each fiber
|
404
|
+
returns the time zone and the local time).
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
results = supervisor.await
|
408
|
+
```
|
409
|
+
|
410
|
+
We can also select the result of the first fiber that has finished executing.
|
411
|
+
All the other fibers will be interrupted:
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
result, fiber = supervisor.select
|
415
|
+
```
|
416
|
+
|
417
|
+
(Notice how `Supervisor#select` returns both the fiber's return value and the
|
418
|
+
fiber itself).
|
419
|
+
|
420
|
+
We can also interrupt all the supervised fibers by using `Supervisor#interrupt`
|
421
|
+
(or `#stop`) just like with single fibers:
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
supervisor.interrupt
|
425
|
+
```
|
426
|
+
|
427
|
+
## What Else Can I Do with Polyphony?
|
428
|
+
|
429
|
+
Polyphony currently provides support for any library that uses Ruby's stock
|
430
|
+
`socket` and `openssl` classes. Polyphony also includes adapters for the `pg`,
|
431
|
+
`redis` and `irb` gems. It also includes an implementation of an integrated HTTP
|
432
|
+
1 / HTTP 2 / websockets web server with support for TLS termination, ALPN
|
433
|
+
protocol selection and preliminary rack support.
|
434
|
+
|
435
|
+
## Fibers are the Future!
|
134
436
|
|
135
|
-
|
136
|
-
|
137
|
-
|
437
|
+
Implementing concurrency at the level of fibers opens up so many new
|
438
|
+
possibilities for Ruby. Polyphony has the performance characteristics and
|
439
|
+
provides the necessary tools for transforming how concurrent Ruby apps are
|
440
|
+
written. Polyphony is still new, and the present documentation is far from being
|
441
|
+
complete. To learn more about Polyphony, read the [technical
|
442
|
+
overview](../../technical-overview/design-principles/). For more examples please
|
443
|
+
consult the [Github
|
444
|
+
repository](https://github.com/digital-fabric/polyphony/tree/master/examples).
|
data/docs/index.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Home
|
4
|
+
nav_order: 1
|
5
|
+
description: Lorem ipsum
|
6
|
+
permalink: /
|
7
|
+
---
|
8
|
+
|
9
|
+
# Polyphony - fine-grained concurrency for Ruby
|
10
|
+
|
11
|
+
> Polyphony \| pəˈlɪf\(ə\)ni \|
|
12
|
+
> 1. _Music_ the style of simultaneously combining a number of parts, each
|
13
|
+
> forming an individual melody and harmonizing with each other.
|
14
|
+
> 2. _Programming_ a Ruby gem for concurrent programming focusing on performance
|
15
|
+
> and developer happiness.
|
16
|
+
|
17
|
+
Polyphony is a library for building concurrent applications in Ruby. Polyphony
|
18
|
+
harnesses the power of [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html)
|
19
|
+
to provide a cooperative, sequential coroutine-based concurrency model. Under
|
20
|
+
the hood, Polyphony uses [libev](https://github.com/enki/libev) as a
|
21
|
+
high-performance event reactor that provides timers, I/O watchers and other
|
22
|
+
asynchronous event primitives.
|
23
|
+
|
24
|
+
|
25
|
+
## Focused on Developer Happiness
|
26
|
+
|
27
|
+
Polyphony is designed to make concurrent Ruby programming feel natural and
|
28
|
+
fluent. Polyphony reduces the boilerplate usually associated with concurrent
|
29
|
+
programming, and introduces concurrency primitives that are easy to use, easy to
|
30
|
+
understand, and above all idiomatic.
|
31
|
+
|
32
|
+
## Optimized for High Performance
|
33
|
+
|
34
|
+
Polyphony offers high performance for I/O bound Ruby apps. Distributing
|
35
|
+
concurrent tasks over fibers, instead of threads or processes, minimizes memory
|
36
|
+
consumption and reduces the cost of context-switching.
|
37
|
+
|
38
|
+
## Designed for Interoperability
|
39
|
+
|
40
|
+
Polyphony makes it possible to use normal Ruby built-in classes like `IO`, and
|
41
|
+
`Socket` in a concurrent multi-fiber environment. Polyphony takes care of
|
42
|
+
context-switching automatically whenever a blocking call like `Socket#accept`,
|
43
|
+
`IO#read` or `Kernel#sleep` is issued.
|
44
|
+
|
45
|
+
## A Growing Ecosystem
|
46
|
+
|
47
|
+
Polyphony includes a full-blown HTTP server implementation with integrated
|
48
|
+
support for HTTP 1, HTTP 2 and WebSockets, TLS/SSL termination, automatic
|
49
|
+
ALPN protocol selection, and body streaming. Polyphony also includes fiber-aware
|
50
|
+
extensions for PostgreSQL and Redis. More databases and services are forthcoming.
|
51
|
+
|
52
|
+
## Features
|
53
|
+
|
54
|
+
* Co-operative scheduling of concurrent tasks using Ruby fibers.
|
55
|
+
* High-performance event reactor for handling I/O events and timers.
|
56
|
+
* Natural, sequential programming style that makes it easy to reason about
|
57
|
+
concurrent code.
|
58
|
+
* Abstractions and constructs for controlling the execution of concurrent code:
|
59
|
+
supervisors, cancel scopes, throttling, resource pools etc.
|
60
|
+
* Code can use native networking classes and libraries, growing support for
|
61
|
+
third-party gems such as `pg` and `redis`.
|
62
|
+
* Use stdlib classes such as `TCPServer` and `TCPSocket` and `Net::HTTP`.
|
63
|
+
* Competitive performance and scalability characteristics, in terms of both
|
64
|
+
throughput and memory consumption.
|
65
|
+
|
66
|
+
## Prior Art
|
67
|
+
|
68
|
+
Polyphony draws inspiration from the following, in no particular order:
|
69
|
+
|
70
|
+
* [nio4r](https://github.com/socketry/nio4r/) and
|
71
|
+
[async](https://github.com/socketry/async) (Polyphony's C-extension code is
|
72
|
+
largely a spinoff of
|
73
|
+
[nio4r's](https://github.com/socketry/nio4r/tree/master/ext))
|
74
|
+
* The [go scheduler](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html)
|
75
|
+
* [EventMachine](https://github.com/eventmachine/eventmachine)
|
76
|
+
* [Trio](https://trio.readthedocs.io/)
|
77
|
+
* [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually,
|
78
|
+
Erlang in general)
|
79
|
+
|
80
|
+
## Going further
|
81
|
+
|
82
|
+
To learn more about using Polyphony to build concurrent applications, read the
|
83
|
+
technical overview below, or look at the [included
|
84
|
+
examples](https://github.com/digital-fabric/polyphony/tree/9e0f3b09213156bdf376ef33684ef267517f06e8/examples/README.md).
|
85
|
+
A thorough reference is forthcoming.
|
86
|
+
|
87
|
+
## Contributing to Polyphony
|
88
|
+
|
89
|
+
Issues and pull requests will be gladly accepted. Please use the git repository
|
90
|
+
at https://github.com/digital-fabric/polyphony as your primary point of
|
91
|
+
departure for contributing.
|
@@ -1,28 +1,86 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Concurrency the Easy Way
|
4
|
+
nav_order: 2
|
5
|
+
parent: Technical Overview
|
6
|
+
permalink: /technical-overview/concurrency/
|
7
|
+
---
|
1
8
|
# Concurrency the Easy Way
|
2
9
|
|
3
|
-
Concurrency is a major consideration for modern programmers. Applications and
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
Concurrency is a major consideration for modern programmers. Applications and
|
11
|
+
digital platforms are nowadays expected to do multiple things at once: serve
|
12
|
+
multiple clients, process multiple background jobs, talk to multiple external
|
13
|
+
services. Concurrency is the property of our programming environment allowing us
|
14
|
+
to schedule and control multiple ongoing operations.
|
15
|
+
|
16
|
+
Traditionally, concurrency has been achieved by using multiple processes or
|
17
|
+
threads. Both approaches have proven problematic. Processes consume relatively a
|
18
|
+
lot of memory, and are relatively difficult to coordinate. Threads consume less
|
19
|
+
memory than processes and make it difficult to synchronize access to shared
|
20
|
+
resources, often leading to race conditions and memory corruption. Using threads
|
21
|
+
often necessitates either using special-purpose thread-safe data structures, or
|
22
|
+
otherwise protecting shared resource access using mutexes and critical sections.
|
23
|
+
In addition, dynamic languages such as Ruby and Python will synchronize multiple
|
24
|
+
threads using a global interpreter lock, which means thread execution cannot be
|
25
|
+
parallelized. Furthermore, the amount of threads and processes on a single
|
26
|
+
system is relatively limited, to the order of several hundreds or a few thousand
|
27
|
+
at most.
|
28
|
+
|
29
|
+
Polyphony offers a third way to write concurrent programs, by using a Ruby
|
30
|
+
construct called [fibers](https://ruby-doc.org/core-2.6.5/Fiber.html). Fibers,
|
31
|
+
based on the idea of [coroutines](https://en.wikipedia.org/wiki/Coroutine),
|
32
|
+
provide a way to run a computation that can be suspended and resumed at any
|
33
|
+
moment. For example, a computation waiting for a reply from a database can
|
34
|
+
suspend itself, transferring control to another ongoing computation, and be
|
35
|
+
resumed once the database has sent back its reply. Meanwhile, another
|
36
|
+
computation is started that opens a socket to a remote service, and then
|
37
|
+
suspends itself, waiting for the connection to be established.
|
38
|
+
|
39
|
+
This form of concurrency, called cooperative concurrency \(in contrast to
|
40
|
+
pre-emptive concurrency, like threads and processes\), offers many advantages,
|
41
|
+
especially for applications that are [I/O
|
42
|
+
bound](https://en.wikipedia.org/wiki/I/O_bound). Fibers are very lightweight
|
43
|
+
\(starting at about 20KB\), can be context-switched faster than threads or
|
44
|
+
processes, and literally millions of them can be created on a single system -
|
45
|
+
the only limiting factor is available memory.
|
46
|
+
|
47
|
+
Polyphony takes Ruby's fibers and adds a way to schedule and switch between
|
48
|
+
fibers automatically whenever a blocking operation is started, such as waiting
|
49
|
+
for a TCP connection to be established, or waiting for an I/O object to be
|
50
|
+
readable, or waiting for a timer to elapse. In addition, Polyphony patches the
|
51
|
+
stock Ruby classes to support its concurrency model, letting developers use all
|
52
|
+
of Ruby's stdlib, for example `Net::HTTP` and `Mail` while reaping the benefits
|
53
|
+
of lightweight, highly performant, fiber-based concurrency.
|
54
|
+
|
55
|
+
Writing concurrent applications using Polyphony's fiber-based concurrency model
|
56
|
+
offers a significant performance advantage. Computational tasks can be broken
|
57
|
+
down into many fine-grained concurrent operations that cost very little in
|
58
|
+
memory and context-switching time. More importantly, this concurrency model lets
|
59
|
+
developers express their ideas in a sequential manner, leading to source code
|
60
|
+
that is easy to read and reason about.
|
14
61
|
|
15
62
|
## Fibers - Polyphony's basic unit of concurrency
|
16
63
|
|
17
|
-
Polyphony extends the core `Fiber` class with additional functionality that
|
64
|
+
Polyphony extends the core `Fiber` class with additional functionality that
|
65
|
+
allows scheduling, synchronizing, interrupting and otherwise controlling running
|
66
|
+
fibers. Polyphony makes sure any exception raised while a is running is [handled
|
67
|
+
correctly](exception-handling.md). Moreover, fibers can communicate with each
|
68
|
+
other using message passing, turning them into autonomous actors in a
|
69
|
+
fine-grained concurrent environment.
|
18
70
|
|
19
71
|
## Higher-Order Concurrency Constructs
|
20
72
|
|
21
|
-
Polyphony also provides several methods and constructs for controlling multiple
|
73
|
+
Polyphony also provides several methods and constructs for controlling multiple
|
74
|
+
fibers. Methods like `cancel_after` and `move_on_after` allow interrupting a
|
75
|
+
fiber that's blocking on any arbitrary operation.
|
22
76
|
|
23
|
-
Cancel scopes \(borrowed from the brilliant Python library
|
77
|
+
Cancel scopes \(borrowed from the brilliant Python library
|
78
|
+
[Trio](https://trio.readthedocs.io/en/stable/)\) allows cancelling ongoing
|
79
|
+
operations for any reason with more control over cancelling behaviour.
|
24
80
|
|
25
|
-
Supervisors allow controlling multiple fibers. They offer enhanced exception
|
81
|
+
Supervisors allow controlling multiple fibers. They offer enhanced exception
|
82
|
+
handling and can be nested to create complex supervision trees ala
|
83
|
+
[Erlang](https://adoptingerlang.org/docs/development/supervision_trees/).
|
26
84
|
|
27
85
|
Some other constructs offered by Polyphony:
|
28
86
|
|
@@ -41,5 +99,9 @@ Some other constructs offered by Polyphony:
|
|
41
99
|
|
42
100
|
— Yukihiro “Matz” Matsumoto
|
43
101
|
|
44
|
-
Polyphony's goal is to make programmers even happier by offering them an easy
|
102
|
+
Polyphony's goal is to make programmers even happier by offering them an easy
|
103
|
+
way to write concurrent applications in Ruby. Polyphony aims to show that Ruby
|
104
|
+
can be used for developing sufficiently high-performance applications, while
|
105
|
+
offering all the advantages of Ruby, with source code that is easy to read and
|
106
|
+
understand.
|
45
107
|
|