polyphony 0.33 → 0.34

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile.lock +1 -1
  4. data/TODO.md +93 -68
  5. data/bin/polyphony-debug +87 -0
  6. data/docs/_includes/nav.html +5 -1
  7. data/docs/_sass/overrides.scss +4 -1
  8. data/docs/api-reference.md +11 -0
  9. data/docs/api-reference/exception.md +27 -0
  10. data/docs/api-reference/fiber.md +407 -0
  11. data/docs/api-reference/io.md +36 -0
  12. data/docs/api-reference/object.md +99 -0
  13. data/docs/api-reference/polyphony-baseexception.md +33 -0
  14. data/docs/api-reference/polyphony-cancel.md +26 -0
  15. data/docs/api-reference/polyphony-moveon.md +24 -0
  16. data/docs/api-reference/polyphony-net.md +20 -0
  17. data/docs/api-reference/polyphony-process.md +28 -0
  18. data/docs/api-reference/polyphony-resourcepool.md +59 -0
  19. data/docs/api-reference/polyphony-restart.md +18 -0
  20. data/docs/api-reference/polyphony-terminate.md +18 -0
  21. data/docs/api-reference/polyphony-threadpool.md +67 -0
  22. data/docs/api-reference/polyphony-throttler.md +77 -0
  23. data/docs/api-reference/polyphony.md +36 -0
  24. data/docs/api-reference/thread.md +88 -0
  25. data/docs/getting-started/tutorial.md +59 -156
  26. data/docs/index.md +2 -0
  27. data/examples/core/forever_sleep.rb +19 -0
  28. data/examples/core/xx-caller.rb +12 -0
  29. data/examples/core/xx-exception-backtrace.rb +40 -0
  30. data/examples/core/xx-fork-spin.rb +42 -0
  31. data/examples/core/xx-spin-fork.rb +49 -0
  32. data/examples/core/xx-supervise-process.rb +30 -0
  33. data/ext/gyro/gyro.h +1 -0
  34. data/ext/gyro/selector.c +8 -0
  35. data/ext/gyro/thread.c +8 -2
  36. data/lib/polyphony.rb +64 -17
  37. data/lib/polyphony/adapters/process.rb +29 -0
  38. data/lib/polyphony/adapters/trace.rb +6 -4
  39. data/lib/polyphony/core/exceptions.rb +5 -0
  40. data/lib/polyphony/core/global_api.rb +15 -0
  41. data/lib/polyphony/extensions/fiber.rb +89 -59
  42. data/lib/polyphony/version.rb +1 -1
  43. data/test/test_fiber.rb +23 -75
  44. data/test/test_global_api.rb +39 -0
  45. data/test/test_kernel.rb +5 -7
  46. data/test/test_process_supervision.rb +46 -0
  47. data/test/test_signal.rb +2 -3
  48. data/test/test_supervise.rb +103 -0
  49. 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
+ ### #&lt;&lt;(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.write('you said: ', data.chomp, "!\n")
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. It makes your code robust,
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
- Here's the complete source code for our Polyphony-based echo server:
246
+ ## Implementing graceful shutdown
247
247
 
248
- ```ruby
249
- require 'polyphony/auto_run'
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
- server = TCPServer.open('127.0.0.1', 1234)
252
- puts 'Echoing on port 1234...'
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
- def handle_client(client)
255
- timeout = cancel_after(10)
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.reset
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
- We can wait for the fiber to terminate:
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
- And finally, we can interrupt the fiber with an exception raised in its current
330
- context:
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
- ```ruby
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
- def get_time(tzone)
351
- res = HTTParty.get("http://worldtimeapi.org/api/timezone/#{tzone}")
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
- zones = %w{
357
- Europe/London Europe/Paris Europe/Bucharest America/New_York Asia/Bangkok
358
- }
359
- zones.each do |tzone|
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
- suspend
367
- ```
368
-
369
- Now that we're familiar with the use of the `#spin` method, we know that all
370
- those HTTP requests will be processed concurrently, and we can expect those 5
371
- separate requests to occur within a fraction of a second (depending on our
372
- machine's location). Also notice how we just used `httparty` with fiber-level
373
- concurrency, without any boilerplate or employing special wrapper classes.
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
- get_times(zones).await.each do |tzone, time|
390
- puts "Time in #{tzone}: #{time}"
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
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ spin {
7
+ spin {
8
+ spin {
9
+ pp Fiber.current.caller
10
+ }.await
11
+ }.await
12
+ }.await
@@ -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