polyphony 0.33 → 0.34
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/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +93 -68
- data/bin/polyphony-debug +87 -0
- data/docs/_includes/nav.html +5 -1
- data/docs/_sass/overrides.scss +4 -1
- data/docs/api-reference.md +11 -0
- data/docs/api-reference/exception.md +27 -0
- data/docs/api-reference/fiber.md +407 -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/getting-started/tutorial.md +59 -156
- data/docs/index.md +2 -0
- data/examples/core/forever_sleep.rb +19 -0
- data/examples/core/xx-caller.rb +12 -0
- data/examples/core/xx-exception-backtrace.rb +40 -0
- data/examples/core/xx-fork-spin.rb +42 -0
- data/examples/core/xx-spin-fork.rb +49 -0
- data/examples/core/xx-supervise-process.rb +30 -0
- data/ext/gyro/gyro.h +1 -0
- data/ext/gyro/selector.c +8 -0
- data/ext/gyro/thread.c +8 -2
- data/lib/polyphony.rb +64 -17
- data/lib/polyphony/adapters/process.rb +29 -0
- data/lib/polyphony/adapters/trace.rb +6 -4
- data/lib/polyphony/core/exceptions.rb +5 -0
- data/lib/polyphony/core/global_api.rb +15 -0
- data/lib/polyphony/extensions/fiber.rb +89 -59
- data/lib/polyphony/version.rb +1 -1
- data/test/test_fiber.rb +23 -75
- data/test/test_global_api.rb +39 -0
- data/test/test_kernel.rb +5 -7
- data/test/test_process_supervision.rb +46 -0
- data/test/test_signal.rb +2 -3
- data/test/test_supervise.rb +103 -0
- metadata +29 -2
@@ -0,0 +1,36 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Polyphony
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/polyphony/
|
6
|
+
---
|
7
|
+
# Polyphony
|
8
|
+
|
9
|
+
The `Polyphony` module acts as a namespace containing general Polyphony
|
10
|
+
functionalities.
|
11
|
+
|
12
|
+
## Class Methods
|
13
|
+
|
14
|
+
### #emit_signal_exception(exception, fiber = Thread.main.main_fiber) → thread
|
15
|
+
|
16
|
+
Emits an exception to the given fiber from a signal handler.
|
17
|
+
|
18
|
+
### #fork({ block }) → pid
|
19
|
+
|
20
|
+
Forks a child process running the given block. Due to the way Ruby implements
|
21
|
+
fibers, along with how signals interact with them, Polyphony-based applications
|
22
|
+
should use `Polyphony#fork` rather than `Kernel#fork`. In order to continue
|
23
|
+
handling fiber scheduling and signal handling correctly, the child process does
|
24
|
+
the following:
|
25
|
+
|
26
|
+
- A new fiber is created using `Fiber#new` and control is transferred to it.
|
27
|
+
- Notify the event loop that a fork has occurred (by calling `ev_loop_fork`).
|
28
|
+
- Setup the current fiber as the main thread's main fiber.
|
29
|
+
- Setup fiber scheduling for the main thread.
|
30
|
+
- Install fiber-aware signal handlers for the `TERM` and `INT` signals.
|
31
|
+
- Run the block.
|
32
|
+
- Correctly handle uncaught exceptions, including `SystemExit` and `Interrupt`.
|
33
|
+
|
34
|
+
### #watch_process(cmd = nil, { block })
|
35
|
+
|
36
|
+
Alternative for [`Polyphony::Process.watch`](../polyphony-process/#watchcmd--nil--block-).
|
@@ -0,0 +1,88 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: ::Thread
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/thread/
|
6
|
+
---
|
7
|
+
# ::Thread
|
8
|
+
|
9
|
+
[Ruby core Thread documentation](https://ruby-doc.org/core-2.7.0/Thread.html)
|
10
|
+
|
11
|
+
Polyphony enhances the core `Thread` class with APIs for switching and
|
12
|
+
scheduling fibers, and reimplements some of its APIs such as `Thread#raise`
|
13
|
+
using fibers which, incidentally, make it safe.
|
14
|
+
|
15
|
+
Each thread has its own run queue and its own event selector. While running
|
16
|
+
multiple threads does not result in true parallelism in MRI Ruby, sometimes
|
17
|
+
multithreading is inevitable, for instance when using third-party gems that
|
18
|
+
spawn threads, or when calling blocking APIs that are not fiber-aware.
|
19
|
+
|
20
|
+
## Class Methods
|
21
|
+
|
22
|
+
## Instance methods
|
23
|
+
|
24
|
+
### #<<(object) → fiber<br>#send(object) → fiber
|
25
|
+
|
26
|
+
Sends a message to the thread's main fiber. For further details see
|
27
|
+
[`Fiber#<<`](../fiber/#object--fibersendobject--fiber).
|
28
|
+
|
29
|
+
### #fiber_scheduling_stats → stats
|
30
|
+
|
31
|
+
Returns statistics relating to fiber scheduling for the thread with the
|
32
|
+
following entries:
|
33
|
+
|
34
|
+
- `:scheduled_fibers` - number of fibers currently in the run queue
|
35
|
+
- `:pending_watchers` - number of currently pending event watchers
|
36
|
+
|
37
|
+
### #join → object<br>#await → object
|
38
|
+
|
39
|
+
Waits for the thread to finish running. If the thread has terminated with an
|
40
|
+
uncaught exception, it will be reraised in the context of the calling fiber. If
|
41
|
+
no excecption is raised, returns the thread's result.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
t = Thread.new { sleep 1 }
|
45
|
+
t.join
|
46
|
+
```
|
47
|
+
|
48
|
+
### #main_fiber → fiber
|
49
|
+
|
50
|
+
Returns the main fiber for the thread.
|
51
|
+
|
52
|
+
### #result → object
|
53
|
+
|
54
|
+
Returns the result of the thread's main fiber.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
t = Thread.new { 'foo' }
|
58
|
+
t.join
|
59
|
+
t.result #=> 'foo'
|
60
|
+
```
|
61
|
+
|
62
|
+
### #switch_fiber
|
63
|
+
|
64
|
+
invokes a switchpoint, selecting and resuming the next fiber to run. The
|
65
|
+
switching algorithm works as follows:
|
66
|
+
|
67
|
+
- If the run queue is not empty, conditionally run the event loop a single time
|
68
|
+
in order to prevent event starvation when there's always runnable fibers
|
69
|
+
waiting to be resumed.
|
70
|
+
- If the run queue is empty, run the event loop until a fiber is put on the run
|
71
|
+
queue.
|
72
|
+
- Switch to the first fiber in the run queue.
|
73
|
+
|
74
|
+
This method is normally not called directly by the application. Calling
|
75
|
+
`Thread#switch_fiber` means the current fiber has no more work to do and would
|
76
|
+
like yield to other fibers. Note that if the current fiber needs to resume at a
|
77
|
+
later time, it should be scheduled before calling `Thread#switch_fiber`.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# schedule current fiber to be resumed later
|
81
|
+
Fiber.current.schedule
|
82
|
+
|
83
|
+
# switch to another fiber
|
84
|
+
Thread.current.switch_fiber
|
85
|
+
|
86
|
+
# the fiber is resumed
|
87
|
+
resume_work
|
88
|
+
```
|
@@ -163,7 +163,7 @@ The `handle_client` method is almost trivial:
|
|
163
163
|
```ruby
|
164
164
|
def handle_client(client)
|
165
165
|
while (data = client.gets)
|
166
|
-
client
|
166
|
+
client << data
|
167
167
|
end
|
168
168
|
rescue Errno::ECONNRESET
|
169
169
|
puts 'Connection reset by client'
|
@@ -239,195 +239,98 @@ thus reset the cancel scope timer.
|
|
239
239
|
In addition, we use an `ensure` block to make sure the client connection is
|
240
240
|
closed, whether or not it was interrupted by the cancel scope timer. The habit
|
241
241
|
of always cleaning up using `ensure` in the face of potential interruptions is a
|
242
|
-
fundamental element of using Polyphony correctly.
|
242
|
+
fundamental element of using Polyphony correctly. This makes your code robust,
|
243
243
|
even in a highly chaotic concurrent execution environment where tasks can be
|
244
244
|
interrupted at any time.
|
245
245
|
|
246
|
-
|
246
|
+
## Implementing graceful shutdown
|
247
247
|
|
248
|
-
|
249
|
-
|
248
|
+
Let's now add graceful shutdown to our server. This means that when the server
|
249
|
+
is stopped we'll first stop accepting new connections, but we'll let any already
|
250
|
+
connected clients keep their sessions.
|
250
251
|
|
251
|
-
|
252
|
-
|
252
|
+
Polyphony's concurrency model is structured. Fibers are limited to the lifetime
|
253
|
+
of their direct parent. When the main fiber terminates (on program exit), it
|
254
|
+
will terminate all its child fibers, each of which will in turn terminate its
|
255
|
+
own children. The termination of child fibers is implemented by sending each
|
256
|
+
child fiber a `Polyphony::Terminate` exception. We can implement custom
|
257
|
+
termination logic simply by adding an exception handler for
|
258
|
+
`Polyphony::Terminate`:
|
253
259
|
|
254
|
-
|
255
|
-
|
260
|
+
```ruby
|
261
|
+
# We first refactor the echo loop into a method
|
262
|
+
def client_loop(client, timeout = nil)
|
256
263
|
while (data = client.gets)
|
257
|
-
timeout
|
264
|
+
timeout&.reset
|
258
265
|
client << data
|
259
266
|
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def handle_client(client)
|
270
|
+
timeout = cancel_after(10)
|
271
|
+
client_loop(client, timeout)
|
260
272
|
rescue Polyphony::Cancel
|
261
273
|
client.puts 'Closing connection due to inactivity.'
|
274
|
+
rescue Polyphony::Terminate
|
275
|
+
# We add a handler for the Terminate exception, and give
|
276
|
+
client.puts 'Server is shutting down. You have 5 more seconds...'
|
277
|
+
move_on_after(5) do
|
278
|
+
client_loop(client)
|
279
|
+
end
|
262
280
|
rescue Errno::ECONNRESET
|
263
281
|
puts 'Connection reset by client'
|
264
282
|
ensure
|
265
283
|
timeout.stop
|
266
284
|
client.close
|
267
285
|
end
|
268
|
-
|
269
|
-
while (client = server.accept)
|
270
|
-
spin { handle_client(client) }
|
271
|
-
end
|
272
|
-
```
|
273
|
-
|
274
|
-
## Waiting and Interrupting
|
275
|
-
|
276
|
-
Polyphony makes it very easy to run multiple concurrent fibers. You can
|
277
|
-
basically start a fiber for any operation that involves talking to the outside
|
278
|
-
world - running a database query, making an HTTP request, sending off a webhook
|
279
|
-
invocation etc. While it's trivial to spin off thousands of fibers, we'd also
|
280
|
-
like a way to control all those fibers.
|
281
|
-
|
282
|
-
Polyphony provides a number of tools for controlling fiber execution. Let's
|
283
|
-
examine some of these tools and how they work. Suppose we have a fiber that was
|
284
|
-
previously spun:
|
285
|
-
|
286
|
-
```ruby
|
287
|
-
fiber = spin { do_some_work }
|
288
286
|
```
|
289
287
|
|
290
|
-
|
291
|
-
|
292
|
-
```ruby
|
293
|
-
fiber.await # alternatively fiber.join
|
294
|
-
```
|
295
|
-
|
296
|
-
Notice that the await call returns the return value of the fiber block:
|
297
|
-
|
298
|
-
```ruby
|
299
|
-
fiber = spin { 2 + 2}
|
300
|
-
fiber.await #=> 4
|
301
|
-
```
|
302
|
-
|
303
|
-
We can also stop the fiber at any point:
|
304
|
-
|
305
|
-
```ruby
|
306
|
-
fiber.stop # or fiber.interrupt
|
307
|
-
```
|
308
|
-
|
309
|
-
We can inject a return value for the fiber using `stop`:
|
310
|
-
|
311
|
-
```ruby
|
312
|
-
fiber = spin do
|
313
|
-
sleep 1
|
314
|
-
1 + 1
|
315
|
-
end
|
316
|
-
|
317
|
-
spin { puts "1 + 1 = #{fiber.await} wha?" }
|
318
|
-
|
319
|
-
fiber.stop(3)
|
320
|
-
suspend
|
321
|
-
```
|
322
|
-
|
323
|
-
We can also *cancel* the fiber, which raises a `Polyphony::Cancel` exception:
|
324
|
-
|
325
|
-
```ruby
|
326
|
-
fiber.cancel!
|
327
|
-
```
|
288
|
+
## Conclusion
|
328
289
|
|
329
|
-
|
330
|
-
|
290
|
+
In this tutorial, we have shown how Polyphony can be used to create robust,
|
291
|
+
highly concurrent Ruby applications. As we have discussed above, Polyphony
|
292
|
+
provides a comprehensive set of tools that make it simple and intuitive to write
|
293
|
+
concurrent applications, with features such as structured concurrency (for
|
294
|
+
controlling fiber lifetime), timeouts (for handling inactive or slow clients),
|
295
|
+
custom termination logic (for implementing graceful shutdown).
|
331
296
|
|
332
|
-
|
333
|
-
fiber.raise 'foo'
|
334
|
-
```
|
335
|
-
|
336
|
-
For more information on how exceptions are handled in Polyphony, see [exception
|
337
|
-
handling](../../main-concepts/exception-handling/).
|
338
|
-
|
339
|
-
## Supervising - controlling multiple fibers at once
|
340
|
-
|
341
|
-
Here's a simple example that we'll use to demonstrate some of the tools provided
|
342
|
-
by Polyphony for controlling fibers. Let's build a script that fetches the local
|
343
|
-
time for multiple time zones:
|
297
|
+
Here's the complete source code for our Polyphony-based echo server:
|
344
298
|
|
345
299
|
```ruby
|
346
|
-
require 'polyphony'
|
347
|
-
require 'httparty'
|
348
|
-
require 'json'
|
300
|
+
require 'polyphony/auto_run'
|
349
301
|
|
350
|
-
|
351
|
-
|
352
|
-
json = JSON.parse(res.body)
|
353
|
-
Time.parse(json['datetime'])
|
354
|
-
end
|
302
|
+
server = TCPServer.open('127.0.0.1', 1234)
|
303
|
+
puts 'Echoing on port 1234...'
|
355
304
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
spin do
|
361
|
-
time = get_time(tzone)
|
362
|
-
puts "Time in #{tzone}: #{time}"
|
305
|
+
def client_loop(client, timeout = nil)
|
306
|
+
while (data = client.gets)
|
307
|
+
timeout&.reset
|
308
|
+
client << data
|
363
309
|
end
|
364
310
|
end
|
365
311
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
Just as before, we suspend the main fiber after spinning off the worker fibers,
|
376
|
-
in order to wait for everything else to be done. But what if we needed to do
|
377
|
-
other work? For example, we might want to collect the different local times into
|
378
|
-
a hash to be processed later. In that case, we can use a `Supervisor`:
|
379
|
-
|
380
|
-
```ruby
|
381
|
-
def get_times(zones)
|
382
|
-
Polyphony::Supervisor.new do |s|
|
383
|
-
zones.each do |tzone|
|
384
|
-
s.spin { [tzone, get_time(tzone)] }
|
385
|
-
end
|
312
|
+
def handle_client(client)
|
313
|
+
timeout = cancel_after(10)
|
314
|
+
client_loop(client, timeout)
|
315
|
+
rescue Polyphony::Cancel
|
316
|
+
client.puts 'Closing connection due to inactivity.'
|
317
|
+
rescue Polyphony::Terminate
|
318
|
+
client.puts 'Server is shutting down. You have 5 more seconds...'
|
319
|
+
move_on_after(5) do
|
320
|
+
client_loop(client)
|
386
321
|
end
|
322
|
+
rescue Errno::ECONNRESET
|
323
|
+
puts 'Connection reset by client'
|
324
|
+
ensure
|
325
|
+
timeout.stop
|
326
|
+
client.close
|
387
327
|
end
|
388
328
|
|
389
|
-
|
390
|
-
|
391
|
-
end
|
392
|
-
```
|
393
|
-
|
394
|
-
There's quite a bit going on here, so let's break it down. We first construct a
|
395
|
-
supervisor and spin our fibers in its context using `Supervisor#spin`.
|
396
|
-
|
397
|
-
```ruby
|
398
|
-
Polyphony::Supervisor.new do |s|
|
399
|
-
...
|
400
|
-
s.spin { ... }
|
401
|
-
...
|
329
|
+
while (client = server.accept)
|
330
|
+
spin { handle_client(client) }
|
402
331
|
end
|
403
332
|
```
|
404
333
|
|
405
|
-
Once our worker fibers are spun, the supervisor can be used to control them. We
|
406
|
-
can wait for all fibers to terminate using `Supervisor#await`, which returns an
|
407
|
-
array with the return values of all fibers (in the above example, each fiber
|
408
|
-
returns the time zone and the local time).
|
409
|
-
|
410
|
-
```ruby
|
411
|
-
results = supervisor.await
|
412
|
-
```
|
413
|
-
|
414
|
-
We can also select the result of the first fiber that has finished executing.
|
415
|
-
All the other fibers will be interrupted:
|
416
|
-
|
417
|
-
```ruby
|
418
|
-
result, fiber = supervisor.select
|
419
|
-
```
|
420
|
-
|
421
|
-
(Notice how `Supervisor#select` returns both the fiber's return value and the
|
422
|
-
fiber itself).
|
423
|
-
|
424
|
-
We can also interrupt all the supervised fibers by using `Supervisor#interrupt`
|
425
|
-
(or `#stop`) just like with single fibers:
|
426
|
-
|
427
|
-
```ruby
|
428
|
-
supervisor.interrupt
|
429
|
-
```
|
430
|
-
|
431
334
|
## What Else Can I Do with Polyphony?
|
432
335
|
|
433
336
|
Polyphony currently provides support for any library that uses Ruby's stock
|
data/docs/index.md
CHANGED
@@ -14,7 +14,9 @@ implements a comprehensive
|
|
14
14
|
using [libev](https://github.com/enki/libev) as a high-performance event reactor
|
15
15
|
for I/O, timers, and other asynchronous events.
|
16
16
|
|
17
|
+
[FAQ](faq){: .btn .btn-green .text-gamma }
|
17
18
|
[Take the tutorial](getting-started/tutorial){: .btn .btn-blue .text-gamma }
|
19
|
+
[Source code](https://github.com/digital-fabric/polyphony){: .btn .btn-purple .text-gamma target="_blank" }
|
18
20
|
{: .mt-6 .h-align-center }
|
19
21
|
|
20
22
|
## Focused on Developer Happiness
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
|
6
|
+
trap('TERM') do
|
7
|
+
# do nothing
|
8
|
+
end
|
9
|
+
|
10
|
+
trap('INT') do
|
11
|
+
# do nothing
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "go to sleep"
|
15
|
+
begin
|
16
|
+
sleep
|
17
|
+
ensure
|
18
|
+
puts "done sleeping"
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
class E < Exception
|
6
|
+
def initialize(msg)
|
7
|
+
super
|
8
|
+
# set_backtrace(caller)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias_method :orig_backtrace, :backtrace
|
12
|
+
def backtrace
|
13
|
+
b = orig_backtrace
|
14
|
+
p [:backtrace, b, caller]
|
15
|
+
b
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def e1
|
20
|
+
e2
|
21
|
+
end
|
22
|
+
|
23
|
+
def e2
|
24
|
+
E.new('foo')
|
25
|
+
end
|
26
|
+
|
27
|
+
def e3
|
28
|
+
raise E, 'bar'
|
29
|
+
end
|
30
|
+
|
31
|
+
e = e1
|
32
|
+
p e
|
33
|
+
puts e.backtrace&.join("\n")
|
34
|
+
|
35
|
+
begin
|
36
|
+
e3
|
37
|
+
rescue Exception => e
|
38
|
+
p e
|
39
|
+
puts e.backtrace.join("\n")
|
40
|
+
end
|