polyphony 1.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/docs/overview.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Polyphony is a new Ruby library for building concurrent applications in Ruby.
8
8
  Polyphony provides a comprehensive, structured concurrency model based on Ruby
9
- fibers and using libev as a high-performance event reactor.
9
+ fibers and using `io_uring` or `libev` for high-performance I/O operations.
10
10
 
11
11
  Polyphony is designed to maximize developer happiness. It provides a natural and
12
12
  fluent API for writing concurrent Ruby apps while using the stock Ruby APIs such
@@ -15,11 +15,6 @@ multi-fiber environment. In addition, Polyphony offers a solid
15
15
  exception-handling experience that builds on and enhances Ruby's
16
16
  exception-handling mechanisms.
17
17
 
18
- Polyphony includes a full-blown HTTP server implementation with integrated
19
- support for HTTP 1 & 2, WebSockets, TLS/SSL termination and more. Polyphony also
20
- provides fiber-aware adapters for connecting to PostgreSQL and Redis servers.
21
- More adapters are being actively developed.
22
-
23
18
  ## Taking Polyphony for a Spin
24
19
 
25
20
  Polyphony is different from other reactor-based solutions for Ruby in that
@@ -60,32 +55,69 @@ come in on `STDIN` and then switches control to the `counter` fiber. When the
60
55
  other work. If no other fiber is ready to run, Polyphony simply waits for at
61
56
  least one event to occur, and then resumes the corresponding fiber.
62
57
 
58
+ ## What are Fibers and What are They Good For?
59
+
60
+ Fibers are some of Ruby's most underappreciated hidden gems. Up until now,
61
+ fibers have been used mostly as the underlying mechanism for implementing
62
+ lazy enumerators and asynchronous generators. Fibers encapsulate, in short,
63
+ an execution context that can be paused and resumed at will.
64
+
65
+ Fibers are also at the heart of Polyphony's concurrency model. Polyphony employs
66
+ fibers as a way to run multiple tasks at once, each task advancing at its own
67
+ pace, pausing when waiting for an event to occur, and automatically resuming
68
+ when that event has occurred.
69
+
70
+ Take for example a web app: in order to fulfil an incoming request, multiple
71
+ steps are required: querying the database, fetching cached entries from Redis,
72
+ talking to third-party services such as Twilio or AWS S3. Each step can last
73
+ tens of milliseconds, and blocks the current thread. Such an app is said to be
74
+ I/O-bound, that is, it mostly spends its time waiting for some external
75
+ services.
76
+
77
+ The traditional approach to handling multiple requests concurrently is to employ
78
+ multiple threads or processes, but this approach has numerous disavantages:
79
+
80
+ - Both threads and processes are heavyweight, in both memory consmption and
81
+ the cost associated with context-switching.
82
+ - Threads introduce hard-to-debug race conditions, and do not offer true
83
+ parallelism, owing to Ruby's GVL.
84
+ - Processes are more difficult to coordinate, since they do not share memory.
85
+ - Both threads and processes are limited to a few thousand at best on a single
86
+ machine. Trying to spawn a thread per client essentially limits the scaling
87
+ capacity of your system.
88
+
89
+ Polyphony eschews both threads and processes in favor of fibers as the basic
90
+ unit of concurrency. The idea is that any time a blocking I/O operation occurs,
91
+ the current fiber is paused, and another fiber which has been marked as
92
+ *runnable* is resumed. This way, your Ruby code can keep on handling incoming
93
+ HTTP requests as they come with a scaling capacity that is virtually only
94
+ limited by available memory.
95
+
63
96
  ## Fibers vs Threads
64
97
 
65
- Most Ruby developers are familiar with threads, but fibers remain a little
66
- explored and little understood concept in the Ruby language. While A thread is
67
- an OS abstraction that is controlled by the OS, a fiber represents an execution
68
- context that can be paused and resumed by the application, and has no
69
- counterpart at the OS level.
98
+ A thread is an OS abstraction that is controlled by the OS, while a fiber
99
+ represents an execution context that can be paused and resumed by the
100
+ application, and has no counterpart at the OS level.
70
101
 
71
102
  When used for writing concurrent programming, fibers offer multiple benefits
72
- over threads. They consume much less RAM than threads, and switching between
73
- them is faster than switching between threads. In addition, since fibers require
74
- no cooperation from the OS, an application can create literally millions of them
75
- given enough RAM. Those advantages make fibers a compelling solution for creating
76
- pervasively concurrent applications, even when using a dynamic high-level "slow"
77
- language such as Ruby.
103
+ over threads. They consume less RAM than threads, and switching between them is
104
+ faster than switching between threads. In addition, since fibers require no
105
+ cooperation from the OS, an application can create literally millions of them
106
+ given enough RAM. Those advantages make fibers a compelling solution for
107
+ creating pervasively concurrent applications, even when using a dynamic
108
+ high-level "slow" language such as Ruby.
78
109
 
79
110
  Ruby programs will only partly benefit from using mutiple threads for processing
80
- work loads (due to the GVL), but fibers are a great match mostly for programs
81
- that are I/O bound (that means spending most of their time talking to the
82
- outside world). A fiber-based web-server, for example, can juggle thousands of
83
- active concurrent connections, each advancing at its own pace, consuming only a
84
- single CPU core.
111
+ work loads (due to the GVL), but fibers are a great match for programs that are
112
+ I/O bound (that means spending most of their time talking to the outside world).
113
+ A fiber-based web-server, for example, can juggle tens of thousands of active
114
+ concurrent connections, each advancing at its own pace, consuming minimal CPU
115
+ time.
85
116
 
86
- Nevertheless, Polyphony fully supports multithreading, with each thread having
87
- its own fiber run queue and its own libev event loop. Polyphony even enables
88
- cross-thread communication using [fiber messaging](#message-passing).
117
+ That said, Polyphony fully supports multithreading, with each thread having its
118
+ own fiber run queue and its own `io_uring` or `libev`-based I/O backend.
119
+ Polyphony even enables cross-thread communication using [fiber
120
+ messaging](#message-passing).
89
121
 
90
122
  ## Fibers vs Callbacks
91
123
 
@@ -111,6 +143,89 @@ in the business logic, as local variables. And finally, the sequential
111
143
  programming style makes it much easier to debug your code, since stack traces
112
144
  contain the entire history of execution from the app's inception.
113
145
 
146
+ ## Switchpoints and the Fiber-Switching Dance
147
+
148
+ In order to make pausing and resuming fibers completely automatic and painfree,
149
+ we need to know when an operation is going to block, and when it can be
150
+ completed without blocking. Operations that might block execution are considered
151
+ *switchpoints*. A switchpoint is a point in time at which control might switch
152
+ from the currently running fiber to another fiber that is in a runnable state.
153
+ Switchpoints may occur in any of the following cases:
154
+
155
+ - On a call to any blocking operation, such as `#sleep`, `Fiber#await`,
156
+ `Thread#join` etc.
157
+ - On fiber termination
158
+ - On a call to `#suspend`
159
+ - On a call to `#snooze`
160
+ - On a call to `Thread#switch_fiber`
161
+
162
+ At any switchpoint, the following takes place:
163
+
164
+ - Check if any fiber is runnable, that is, ready to continue processing.
165
+ - If no fiber is runnable, watch for events (see below) and wait for at least
166
+ one fiber to become runnable.
167
+ - Pause the current fiber and switch to the first runnable fiber, which resumes
168
+ at the point it was last paused.
169
+
170
+ The automatic switching between fibers is complemented by employing
171
+ [libev](http://software.schmorp.de/pkg/libev.html), a multi-platform high
172
+ performance event reactor that allows listening to I/O, timer and other events.
173
+ At every switchpoint where no fibers are runnable, the libev evet loop is run
174
+ until events occur, which in turn cause the relevant fibers to become runnable.
175
+
176
+ Let's examine a simple example:
177
+
178
+ ```ruby
179
+ require 'polyphony'
180
+
181
+ spin do
182
+ puts "Going to sleep..."
183
+ sleep 1
184
+ puts "Woke up"
185
+ end
186
+
187
+ suspend
188
+ puts "We're done"
189
+ ```
190
+
191
+ The above program does nothing exceptional, it just sleeps for 1 second and
192
+ prints a bunch of messages. But it is enough to demonstrate how concurrency
193
+ works in Polyphony. Here's a flow chart of the transfer of control:
194
+
195
+ <img src="https://github.com/digital-fabric/polyphony/raw/master/docs/assets/sleeping-fiber.svg">
196
+
197
+ Here's the actual sequence of execution (in pseudo-code)
198
+
199
+ ```ruby
200
+ # (main fiber)
201
+ sleeper = spin { ... } # The main fiber spins up a new fiber marked as runnable
202
+ suspend # The main fiber suspends, waiting for all other work to finish
203
+ Thread.current.switch_fiber # Polyphony looks for other runnable fibers
204
+
205
+ # (sleeper fiber)
206
+ puts "Going to sleep..." # The sleeper fiber starts running
207
+ sleep 1 # The sleeper fiber goes to sleep
208
+ Gyro::Timer.new(1, 0).await # A timer event watcher is setup and yields
209
+ Thread.current.switch_fiber # Polyphony looks for other runnable fibers
210
+ Thread.current.backend.poll # With no work left, the event loop is ran
211
+ fiber.schedule # The timer event fires, scheduling the sleeper fiber
212
+ # <= The sleep method returns
213
+ puts "Woke up"
214
+ Thread.current.switch_fiber # With the fiber done, Polyphony looks for work
215
+
216
+ # with no more work, control is returned to the main fiber
217
+ # (main fiber)
218
+ # <=
219
+ # With no more work left, the main fiber is resumed and the suspend call returns
220
+ puts "We're done"
221
+ ```
222
+
223
+ What we have done in fact is we multiplexed two different contexts of execution
224
+ (fibers) onto a single thread, each fiber continuing at its own pace and
225
+ yielding control when waiting for something to happen. This context-switching
226
+ dance, performed automatically by Polyphony behind the scenes, enables building
227
+ highly-concurrent Ruby apps, with minimal impact on performance.
228
+
114
229
  ## Structured Concurrency
115
230
 
116
231
  Polyphony's tagline is "fine-grained concurrency for Ruby", because it makes it
@@ -445,13 +560,3 @@ connection loops waiting for data to be read from the socket. Once the data
445
560
  arrives, it is fed to the HTTP parser. The HTTP parser will call the
446
561
  `on_headers_complete` callback, which simply adds a request to the requests
447
562
  queue. The code then continues to handle any requests still in the queue.
448
-
449
- ## Future Directions
450
-
451
- Polyphony is a young project, and will still need a lot of development effort to
452
- reach version 1.0. Here are some of the exciting directions we're working on.
453
-
454
- - Support for more core and stdlib APIs
455
- - More adapters for gems with C-extensions, such as `mysql`, `sqlite3` etc
456
- - Use `io_uring` backend as alternative to the libev backend
457
- - More concurrency constructs for building highly concurrent applications
data/docs/readme.md CHANGED
@@ -77,9 +77,10 @@ $ gem install polyphony
77
77
 
78
78
  ## Usage
79
79
 
80
- - {file:/docs/overview.md Overview}
81
- - {file:/docs/tutorial.md Tutorial}
82
- - {file:/docs/faq.md FAQ}
80
+ - {file:/docs/overview.md Overview}
81
+ - {file:/docs/tutorial.md Tutorial}
82
+ - {file:/docs/cheat-sheet.md Cheat-Sheet}
83
+ - {file:/docs/faq.md FAQ}
83
84
 
84
85
  ## Technical Discussion
85
86
 
data/docs/tutorial.md CHANGED
@@ -2,137 +2,15 @@
2
2
 
3
3
  # Tutorial
4
4
 
5
- Polyphony is a new Ruby library aimed at making writing concurrent Ruby apps
6
- easy and fun. In this article, we'll introduce Polyphony's fiber-based
7
- concurrency model, some of Polyphony's API, and demonstrate how to solve some
8
- simple situations related to concurrent computing.
9
-
10
- ## What are Fibers and What are They Good For?
11
-
12
- Fibers are some of Ruby's most underappreciated hidden gems. Up until now,
13
- fibers have been used mostly as the underlying mechanism for implementing
14
- lazy enumerators and asynchronous generators. Fibers encapsulate, in short,
15
- an execution context that can be paused and resumed at will.
16
-
17
- Fibers are also at the heart of Polyphony's concurrency model. Polyphony employs
18
- fibers as a way to run multiple tasks at once, each task advancing at its own
19
- pace, pausing when waiting for an event to occur, and automatically resuming
20
- when that event has occurred.
21
-
22
- Take for example a web app: in order to fulfil an incoming request, multiple
23
- steps are required: querying the database, fetching cached entries from Redis,
24
- talking to third-party services such as Twilio or AWS S3. Each step can last
25
- tens of milliseconds, and blocks the current thread. Such an app is said to be
26
- I/O-bound, that is, it mostly spends its time waiting for some external
27
- services.
28
-
29
- The traditional approach to handling multiple requests concurrently is to employ
30
- multiple threads or processes, but this approach has numerous disavantages:
31
-
32
- - Both threads and processes are heavyweight, in both memory consmption and
33
- the cost associated with context-switching.
34
- - Threads introduce hard-to-debug race conditions, and do not offer true
35
- parallelism, owing to Ruby's GVL.
36
- - Processes are more difficult to coordinate, since they do not share memory.
37
- - Both threads and processes are limited to a few thousand at best on a single
38
- machine. Trying to spawn a thread per client essentially limits the scaling
39
- capacity of your system.
40
-
41
- Polyphony eschews both threads and processes in favor of fibers as the basic
42
- unit of concurrency. The idea is that any time a blocking I/O operation occurs,
43
- the current fiber is paused, and another fiber which has been marked as
44
- *runnable* is resumed. This way, your Ruby code can keep on handling incoming
45
- HTTP requests as they come with a scaling capacity that is virtually only
46
- limited by available memory.
47
-
48
- ## Switchpoints and the Fiber-Switching Dance
49
-
50
- In order to make pausing and resuming fibers completely automatic and painfree,
51
- we need to know when an operation is going to block, and when it can be
52
- completed without blocking. Operations that might block execution are considered
53
- *switchpoints*. A switchpoint is a point in time at which control might switch
54
- from the currently running fiber to another fiber that is in a runnable state.
55
- Switchpoints may occur in any of the following cases:
56
-
57
- - On a call to any blocking operation, such as `#sleep`, `Fiber#await`,
58
- `Thread#join` etc.
59
- - On fiber termination
60
- - On a call to `#suspend`
61
- - On a call to `#snooze`
62
- - On a call to `Thread#switch_fiber`
63
-
64
- At any switchpoint, the following takes place:
65
-
66
- - Check if any fiber is runnable, that is, ready to continue processing.
67
- - If no fiber is runnable, watch for events (see below) and wait for at least
68
- one fiber to become runnable.
69
- - Pause the current fiber and switch to the first runnable fiber, which resumes
70
- at the point it was last paused.
71
-
72
- The automatic switching between fibers is complemented by employing
73
- [libev](http://software.schmorp.de/pkg/libev.html), a multi-platform high
74
- performance event reactor that allows listening to I/O, timer and other events.
75
- At every switchpoint where no fibers are runnable, the libev evet loop is run
76
- until events occur, which in turn cause the relevant fibers to become runnable.
77
-
78
- Let's examine a simple example:
79
-
80
- ```ruby
81
- require 'polyphony'
82
-
83
- spin do
84
- puts "Going to sleep..."
85
- sleep 1
86
- puts "Woke up"
87
- end
88
-
89
- suspend
90
- puts "We're done"
91
- ```
92
-
93
- The above program does nothing exceptional, it just sleeps for 1 second and
94
- prints a bunch of messages. But it is enough to demonstrate how concurrency
95
- works in Polyphony. Here's a flow chart of the transfer of control:
96
-
97
- <img src="https://github.com/digital-fabric/polyphony/raw/master/docs/assets/sleeping-fiber.png">
98
-
99
- Here's the actual sequence of execution (in pseudo-code)
100
-
101
- ```ruby
102
- # (main fiber)
103
- sleeper = spin { ... } # The main fiber spins up a new fiber marked as runnable
104
- suspend # The main fiber suspends, waiting for all other work to finish
105
- Thread.current.switch_fiber # Polyphony looks for other runnable fibers
106
-
107
- # (sleeper fiber)
108
- puts "Going to sleep..." # The sleeper fiber starts running
109
- sleep 1 # The sleeper fiber goes to sleep
110
- Gyro::Timer.new(1, 0).await # A timer event watcher is setup and yields
111
- Thread.current.switch_fiber # Polyphony looks for other runnable fibers
112
- Thread.current.backend.poll # With no work left, the event loop is ran
113
- fiber.schedule # The timer event fires, scheduling the sleeper fiber
114
- # <= The sleep method returns
115
- puts "Woke up"
116
- Thread.current.switch_fiber # With the fiber done, Polyphony looks for work
117
-
118
- # with no more work, control is returned to the main fiber
119
- # (main fiber)
120
- # <=
121
- # With no more work left, the main fiber is resumed and the suspend call returns
122
- puts "We're done"
123
- ```
124
-
125
- What we have done in fact is we multiplexed two different contexts of execution
126
- (fibers) onto a single thread, each fiber continuing at its own pace and
127
- yielding control when waiting for something to happen. This context-switching
128
- dance, performed automatically by Polyphony behind the scenes, enables building
129
- highly-concurrent Ruby apps, with minimal impact on performance.
5
+ In this tutorial we'll show how to build a simple fiber-based server using
6
+ Polyphony, how to make it concurrent and how to make it resilient to errors.
7
+ We'll assume you have read the [overview](./overview.md). If you haven't yet,
8
+ please go and read it now before continuing with this tutorial.
130
9
 
131
10
  ## Building a Simple Echo Server with Polyphony
132
11
 
133
- Let's now turn our attention to something a bit more useful: a concurrent echo
134
- server. Our server will accept TCP connections and send back whatever it receives
135
- from the client.
12
+ Here's what we want to build: a concurrent echo server. Our server will accept
13
+ TCP connections and send back whatever it receives from the client.
136
14
 
137
15
  We'll start by opening a server socket:
138
16
 
@@ -189,7 +67,7 @@ innocent call to `#spin`.
189
67
 
190
68
  Here's a flow chart showing the transfer of control between the different fibers:
191
69
 
192
- <img src="https://github.com/digital-fabric/polyphony/raw/master/docs/assets/echo-fibers.png">
70
+ <img src="https://github.com/digital-fabric/polyphony/raw/master/docs/assets/echo-fibers.svg">
193
71
 
194
72
  Let's consider the advantage of the Polyphony concurrency model:
195
73
 
@@ -205,14 +83,15 @@ Let's consider the advantage of the Polyphony concurrency model:
205
83
 
206
84
  Now that we have a working concurrent echo server, let's add some bells and
207
85
  whistles. First of all, let's get rid of clients that are not active. We'll do
208
- this by setting up a timeout fiber that cancels the fiber dealing with the connection
86
+ this by wrapping our read loop in a call to `cancel_after`:
209
87
 
210
88
  ```ruby
211
89
  def handle_client(client)
212
- timeout = cancel_after(10)
213
- while (data = client.gets)
214
- timeout.restart
215
- client << data
90
+ cancel_after(10) do |timeout|
91
+ while (data = client.gets)
92
+ timeout.restart
93
+ client << data
94
+ end
216
95
  end
217
96
  rescue Polyphony::Cancel
218
97
  client.puts 'Closing connection due to inactivity.'
@@ -228,14 +107,15 @@ then cancel its parent. The call to `client.gets` blocks until new data is
228
107
  available. If no new data is available, the `timeout` fiber will finish
229
108
  sleeping, and then cancel the client handling fiber by raising a
230
109
  `Polyphony::Cancel` exception. However, if new data is received, the `timeout`
231
- fiber is restarted, causing to begin sleeping again for 10 seconds. If the
110
+ fiber is restarted, causing it to begin sleeping again for 10 seconds. If the
232
111
  client has closed the connection, or some other exception occurs, the `timeout`
233
- fiber is automatically stopped as it is a child of the client handling fiber.
112
+ fiber is automatically stopped as it is a child of the fiber running the
113
+ `handle_client` method.
234
114
 
235
115
  The habit of always cleaning up using `ensure` in the face of potential
236
116
  interruptions is a fundamental element of using Polyphony correctly. This makes
237
117
  your code robust, even in a highly chaotic concurrent execution environment
238
- where tasks can be started, restarted and interrupted at any time.
118
+ where fibers can be started, restarted and interrupted at any time.
239
119
 
240
120
  ## Implementing graceful shutdown
241
121
 
@@ -261,8 +141,7 @@ def client_loop(client, timeout = nil)
261
141
  end
262
142
 
263
143
  def handle_client(client)
264
- timeout = cancel_after(10)
265
- client_loop(client, timeout)
144
+ cancel_after(10) { |timeout| client_loop(client, timeout) }
266
145
  rescue Polyphony::Cancel
267
146
  client.puts 'Closing connection due to inactivity.'
268
147
  rescue Polyphony::Terminate
@@ -304,8 +183,7 @@ def client_loop(client, timeout = nil)
304
183
  end
305
184
 
306
185
  def handle_client(client)
307
- timeout = cancel_after(10)
308
- client_loop(client, timeout)
186
+ cancel_after(10) { |timeout| client_loop(client, timeout) }
309
187
  rescue Polyphony::Cancel
310
188
  client.puts 'Closing connection due to inactivity.'
311
189
  rescue Polyphony::Terminate
@@ -324,11 +202,3 @@ while (client = server.accept)
324
202
  spin { handle_client(client) }
325
203
  end
326
204
  ```
327
-
328
- ## What Else Can I Do with Polyphony?
329
-
330
- Polyphony currently provides support for any library that uses Ruby's stock
331
- `socket` and `openssl` classes. Polyphony also includes adapters for the `pg`,
332
- `redis` and `irb` gems. It also includes an implementation of an integrated HTTP
333
- 1 / HTTP 2 / websockets web server with support for TLS termination, ALPN
334
- protocol selection and preliminary rack support.
@@ -260,7 +260,8 @@ VALUE Polyphony_backend_sleep(VALUE self, VALUE duration) {
260
260
  }
261
261
 
262
262
  /* Splices data from the given source to the given destination, returning the
263
- * number of bytes spliced.
263
+ * number of bytes spliced. If maxlen is negative, splices repeatedly
264
+ * using absolute value of maxlen until EOF is encountered.
264
265
  *
265
266
  * @param src [IO] source
266
267
  * @param dest [IO] destination