polyphony 0.40 → 0.43.2

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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +11 -2
  3. data/.gitignore +2 -2
  4. data/.rubocop.yml +30 -0
  5. data/CHANGELOG.md +29 -2
  6. data/Gemfile.lock +13 -10
  7. data/README.md +0 -1
  8. data/Rakefile +3 -3
  9. data/TODO.md +27 -97
  10. data/docs/_config.yml +56 -7
  11. data/docs/_sass/custom/custom.scss +6 -26
  12. data/docs/_sass/overrides.scss +0 -46
  13. data/docs/{user-guide → _user-guide}/all-about-timers.md +0 -0
  14. data/docs/_user-guide/index.md +9 -0
  15. data/docs/{user-guide → _user-guide}/web-server.md +0 -0
  16. data/docs/api-reference/fiber.md +2 -2
  17. data/docs/api-reference/index.md +9 -0
  18. data/docs/api-reference/polyphony-process.md +1 -1
  19. data/docs/api-reference/thread.md +1 -1
  20. data/docs/faq.md +21 -11
  21. data/docs/favicon.ico +0 -0
  22. data/docs/getting-started/index.md +10 -0
  23. data/docs/getting-started/installing.md +2 -6
  24. data/docs/getting-started/overview.md +486 -0
  25. data/docs/getting-started/tutorial.md +27 -19
  26. data/docs/index.md +6 -2
  27. data/docs/main-concepts/concurrency.md +0 -5
  28. data/docs/main-concepts/design-principles.md +69 -21
  29. data/docs/main-concepts/extending.md +1 -1
  30. data/docs/main-concepts/index.md +9 -0
  31. data/docs/polyphony-logo.png +0 -0
  32. data/examples/adapters/redis_blpop.rb +12 -0
  33. data/examples/core/01-spinning-up-fibers.rb +1 -0
  34. data/examples/core/03-interrupting.rb +4 -1
  35. data/examples/core/04-handling-signals.rb +19 -0
  36. data/examples/core/xx-agent.rb +102 -0
  37. data/examples/core/xx-sleeping.rb +14 -6
  38. data/examples/io/xx-irb.rb +1 -1
  39. data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
  40. data/examples/performance/thread-vs-fiber/polyphony_server.rb +13 -36
  41. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
  42. data/examples/performance/xx-array.rb +11 -0
  43. data/examples/performance/xx-fiber-switch.rb +9 -0
  44. data/examples/performance/xx-snooze.rb +15 -0
  45. data/ext/{gyro → polyphony}/extconf.rb +2 -2
  46. data/ext/{gyro → polyphony}/fiber.c +15 -22
  47. data/ext/{gyro → polyphony}/libev.c +0 -0
  48. data/ext/{gyro → polyphony}/libev.h +0 -0
  49. data/ext/polyphony/libev_agent.c +725 -0
  50. data/ext/polyphony/libev_queue.c +217 -0
  51. data/ext/{gyro/gyro.c → polyphony/polyphony.c} +12 -37
  52. data/ext/polyphony/polyphony.h +90 -0
  53. data/ext/polyphony/polyphony_ext.c +21 -0
  54. data/ext/{gyro → polyphony}/thread.c +34 -151
  55. data/ext/{gyro → polyphony}/tracing.c +1 -1
  56. data/lib/polyphony.rb +19 -12
  57. data/lib/polyphony/adapters/irb.rb +1 -1
  58. data/lib/polyphony/adapters/postgres.rb +6 -5
  59. data/lib/polyphony/adapters/process.rb +5 -5
  60. data/lib/polyphony/adapters/redis.rb +3 -2
  61. data/lib/polyphony/adapters/trace.rb +28 -28
  62. data/lib/polyphony/core/channel.rb +3 -3
  63. data/lib/polyphony/core/exceptions.rb +1 -1
  64. data/lib/polyphony/core/global_api.rb +13 -11
  65. data/lib/polyphony/core/resource_pool.rb +3 -3
  66. data/lib/polyphony/core/sync.rb +2 -2
  67. data/lib/polyphony/core/thread_pool.rb +6 -6
  68. data/lib/polyphony/core/throttler.rb +13 -6
  69. data/lib/polyphony/event.rb +27 -0
  70. data/lib/polyphony/extensions/core.rb +22 -14
  71. data/lib/polyphony/extensions/fiber.rb +4 -4
  72. data/lib/polyphony/extensions/io.rb +59 -25
  73. data/lib/polyphony/extensions/openssl.rb +36 -16
  74. data/lib/polyphony/extensions/socket.rb +28 -10
  75. data/lib/polyphony/extensions/thread.rb +16 -9
  76. data/lib/polyphony/net.rb +9 -9
  77. data/lib/polyphony/version.rb +1 -1
  78. data/polyphony.gemspec +3 -3
  79. data/test/helper.rb +12 -1
  80. data/test/test_agent.rb +130 -0
  81. data/test/{test_async.rb → test_event.rb} +13 -7
  82. data/test/test_ext.rb +25 -4
  83. data/test/test_fiber.rb +19 -10
  84. data/test/test_global_api.rb +6 -6
  85. data/test/test_io.rb +46 -24
  86. data/test/test_queue.rb +74 -0
  87. data/test/test_signal.rb +3 -40
  88. data/test/test_socket.rb +34 -0
  89. data/test/test_thread.rb +37 -16
  90. data/test/test_trace.rb +6 -5
  91. metadata +39 -41
  92. data/docs/_includes/nav.html +0 -51
  93. data/docs/_includes/prevnext.html +0 -17
  94. data/docs/_layouts/default.html +0 -106
  95. data/docs/api-reference.md +0 -11
  96. data/docs/api-reference/gyro-async.md +0 -57
  97. data/docs/api-reference/gyro-child.md +0 -29
  98. data/docs/api-reference/gyro-queue.md +0 -44
  99. data/docs/api-reference/gyro-timer.md +0 -51
  100. data/docs/api-reference/gyro.md +0 -25
  101. data/docs/getting-started.md +0 -10
  102. data/docs/main-concepts.md +0 -10
  103. data/docs/user-guide.md +0 -10
  104. data/examples/core/forever_sleep.rb +0 -19
  105. data/ext/gyro/async.c +0 -132
  106. data/ext/gyro/child.c +0 -108
  107. data/ext/gyro/gyro.h +0 -158
  108. data/ext/gyro/gyro_ext.c +0 -33
  109. data/ext/gyro/io.c +0 -457
  110. data/ext/gyro/queue.c +0 -146
  111. data/ext/gyro/selector.c +0 -205
  112. data/ext/gyro/signal.c +0 -99
  113. data/ext/gyro/socket.c +0 -213
  114. data/ext/gyro/timer.c +0 -115
  115. data/test/test_timer.rb +0 -56
@@ -1,30 +1,10 @@
1
- $nav-child-link-color: #9d9b9e;
2
- $link-color: $blue-000;
3
1
 
4
- .img-figure {
5
- text-align: center;
2
+ h1.logo-title {
3
+ font-size: 42px !important;
4
+ font-weight: bold;
6
5
  }
7
6
 
8
- #prevnext {
9
- padding-top: 1em;
7
+ h2.logo-title {
8
+ margin-top: 0.25em;
9
+ margin-bottom: 1em;
10
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
- }
@@ -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
- }
@@ -0,0 +1,9 @@
1
+ ---
2
+ layout: page
3
+ title: User Guide
4
+ has_children: true
5
+ nav_order: 4
6
+ ---
7
+
8
+ # User Guide
9
+ {: .no_toc }
@@ -71,7 +71,7 @@ f << 2
71
71
  result = receive #=> 20
72
72
  ```
73
73
 
74
- ### #auto_async → async
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.auto_async
87
+ async = Fiber.current.auto_watcher
88
88
  spin { work(async) }
89
89
  async.await
90
90
  ```
@@ -0,0 +1,9 @@
1
+ ---
2
+ layout: page
3
+ title: API Reference
4
+ has_children: true
5
+ nav_order: 5
6
+ ---
7
+
8
+ # API Reference
9
+ {: .no_toc }
@@ -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 event selector. While running
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.
@@ -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
- ### Why not just use callbacks instead of fibers?
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
- ### If callbacks suck, why not use promises?
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
- ### Why is awaiting implicit? Why not use explicit async/await?
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
- ### Why use `Fiber#transfer` and not `Fiber#resume`?
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
- ### Why does Polyphony reimplements core APIs such as `IO#read` and `Kernel#sleep`?
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
- ### Why is Polyphony not split into multiple gems?
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
- ### Can I use Polyphony in a multithreaded program?
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
- ### How Does Polyphony Fit Into the Ruby's Future Concurrency Plans
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
- ### Can I run Rails using Polyphony?
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
- ### How can I contribute to Polyphony?
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
- ### Who is behind this project?
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
Binary file
@@ -0,0 +1,10 @@
1
+ ---
2
+ layout: page
3
+ title: Getting Started
4
+ description: Getting started with Polyphony
5
+ has_children: true
6
+ nav_order: 2
7
+ ---
8
+
9
+ # Getting Started
10
+ {: .no_toc }
@@ -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
- permalink: /getting-started/installing/
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 (sorry, there are no plans to support Windows for the time
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