polyphony 0.33 → 0.34
Sign up to get free protection for your applications and to get access to all the features.
- 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
|