polyphony 0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +86 -0
  3. data/README.md +400 -0
  4. data/ext/ev/extconf.rb +19 -0
  5. data/lib/polyphony.rb +26 -0
  6. data/lib/polyphony/core.rb +45 -0
  7. data/lib/polyphony/core/async.rb +36 -0
  8. data/lib/polyphony/core/cancel_scope.rb +61 -0
  9. data/lib/polyphony/core/channel.rb +39 -0
  10. data/lib/polyphony/core/coroutine.rb +106 -0
  11. data/lib/polyphony/core/exceptions.rb +24 -0
  12. data/lib/polyphony/core/fiber_pool.rb +98 -0
  13. data/lib/polyphony/core/supervisor.rb +75 -0
  14. data/lib/polyphony/core/sync.rb +20 -0
  15. data/lib/polyphony/core/thread.rb +49 -0
  16. data/lib/polyphony/core/thread_pool.rb +58 -0
  17. data/lib/polyphony/core/throttler.rb +38 -0
  18. data/lib/polyphony/extensions/io.rb +62 -0
  19. data/lib/polyphony/extensions/kernel.rb +161 -0
  20. data/lib/polyphony/extensions/postgres.rb +96 -0
  21. data/lib/polyphony/extensions/redis.rb +68 -0
  22. data/lib/polyphony/extensions/socket.rb +85 -0
  23. data/lib/polyphony/extensions/ssl.rb +73 -0
  24. data/lib/polyphony/fs.rb +22 -0
  25. data/lib/polyphony/http/agent.rb +214 -0
  26. data/lib/polyphony/http/http1.rb +124 -0
  27. data/lib/polyphony/http/http1_request.rb +71 -0
  28. data/lib/polyphony/http/http2.rb +66 -0
  29. data/lib/polyphony/http/http2_request.rb +69 -0
  30. data/lib/polyphony/http/rack.rb +27 -0
  31. data/lib/polyphony/http/server.rb +43 -0
  32. data/lib/polyphony/line_reader.rb +82 -0
  33. data/lib/polyphony/net.rb +59 -0
  34. data/lib/polyphony/net_old.rb +299 -0
  35. data/lib/polyphony/resource_pool.rb +56 -0
  36. data/lib/polyphony/server_task.rb +18 -0
  37. data/lib/polyphony/testing.rb +34 -0
  38. data/lib/polyphony/version.rb +5 -0
  39. metadata +170 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f95ac584e1a686e3ff45ec50878bce9cc3ea0457f2894e0839e69c79621e3ab9
4
+ data.tar.gz: 189eb3effd86a54ab16eec64cd4159ce851fdbd249a6b227c02ecfedfd35c987
5
+ SHA512:
6
+ metadata.gz: e935934c596936a1fcce5ecf5f87cb288245755bd4e08f9dce6fde16cffd486134f66bc209774418b1942b00c13e015f6bfa6c55026e1233fe7a4533aa3411b6
7
+ data.tar.gz: a95b472caf17fa3c5a1f7afb8757e891e445a9afe7770131054a1db49c4a4529afa85acc723e1b621f4ae1200de4bfaef42a6a72b8002decbba4324be21ea3d5
data/CHANGELOG.md ADDED
@@ -0,0 +1,86 @@
1
+ 0.13 2019-01-05
2
+ ---------------
3
+
4
+ * Rename Rubato to Polyphony (I know, this is getting silly...)
5
+
6
+ 0.12 2019-01-01
7
+ ---------------
8
+
9
+ * Add Coroutine#resume
10
+ * Improve startup time
11
+ * Accept rate: or interval: arguments for throttle
12
+ * Set correct backtrace for errors
13
+ * Improve handling of uncaught raised errors
14
+ * Implement HTTP 1.1/2 client agent with connection management
15
+
16
+ 0.11 2018-12-27
17
+ ---------------
18
+
19
+ * Move reactor loop to secondary fiber, allow blocking operations on main
20
+ fiber.
21
+ * Example implementation of erlang-style generic server pattern (implement
22
+ async API to a coroutine)
23
+ * Implement coroutine mailboxes, Coroutine#<<, Coroutine#receive, Kernel.receive
24
+ for message passing
25
+ * Add Coroutine.current for getting current coroutine
26
+
27
+ 0.10 2018-11-20
28
+ ---------------
29
+
30
+ * Rewrite Rubato core for simpler code and better performance
31
+ * Implement EV.snooze (sleep until next tick)
32
+ * Coroutine encapsulates a task spawned on a separate fiber
33
+ * Supervisor supervises multiple coroutines
34
+ * CancelScope used to cancel an ongoing task (usually with a timeout)
35
+ * Rate throttling
36
+ * Implement async SSL server
37
+
38
+ 0.9 2018-11-14
39
+ --------------
40
+
41
+ * Rename Nuclear to Rubato
42
+
43
+ 0.8 2018-10-04
44
+ --------------
45
+
46
+ * Replace nio4r with in-house extension based on libev, with better API,
47
+ better performance, support for IO, timer, signal and async watchers
48
+ * Fix mem leak coming from nio4r (probably related to code in Selector#select)
49
+
50
+ 0.7 2018-09-13
51
+ --------------
52
+
53
+ * Implement resource pool
54
+ * transaction method for pg cient
55
+ * Async connect for pg client
56
+ * Add testing module for testing async code
57
+ * Improve HTTP server performance
58
+ * Proper promise chaining
59
+
60
+ 0.6 2018-09-11
61
+ --------------
62
+
63
+ * Add http, redis, pg dependencies
64
+ * Move ALPN code inside net module
65
+
66
+ 0.4 2018-09-10
67
+ --------------
68
+
69
+ * Code refactored and reogranized
70
+ * Fix recursion in next_tick
71
+ * HTTP 2 server with support for ALPN protocol negotiation and HTTP upgrade
72
+ * OpenSSL server
73
+
74
+ 0.3 2018-09-06
75
+ --------------
76
+
77
+ * Event reactor
78
+ * Timers
79
+ * Promises
80
+ * async/await syntax for promises
81
+ * IO and read/write stream
82
+ * TCP server/client
83
+ * Promised threads
84
+ * HTTP server
85
+ * Redis interface
86
+ * PostgreSQL interface
data/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # Polyphony - Fiber-Based Concurrency for Ruby
2
+
3
+ [INSTALL](#installing-polyphony) |
4
+ [TUTORIAL](#getting-started) |
5
+ [EXAMPLES](examples) |
6
+ [TEHNICAL OVERVIEW](#how-polyphony-works---a-technical-overview) |
7
+ [REFERENCE](#api-reference) |
8
+ [EXTENDING](#extending-polyphony)
9
+
10
+ > Polyphony | pəˈlɪf(ə)ni | *Music* - the style of simultaneously combining a
11
+ > number of parts, each forming an individual melody and harmonizing with each
12
+ > other.
13
+
14
+ **Note**: Polyphony is designed to work with recent versions of Ruby and
15
+ supports Linux and MacOS only. This software is currently at the alpha stage.
16
+
17
+ ## What is Polyphony
18
+
19
+ Polyphony is a library for building concurrent applications in Ruby. Polyphony
20
+ harnesses the power of
21
+ [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html) to provide a
22
+ cooperative, sequential coroutine-based concurrency model. Under the hood,
23
+ Polyphony uses [libev](https://github.com/enki/libev) as a high-performance event
24
+ reactor that provides timers, I/O watchers and other asynchronous event
25
+ primitives.
26
+
27
+ Polyphony makes it possible to use normal Ruby built-in classes like `IO`, and
28
+ `Socket` in a concurrent fashion without having to resort to threads. Polyphony
29
+ takes care of context-switching automatically whenever a blocking call like
30
+ `Socket#accept` or `IO#read` is issued.
31
+
32
+ ## Features
33
+
34
+ - Co-operative scheduling of concurrent tasks using Ruby fibers.
35
+ - High-performance event reactor for handling I/O events and timers.
36
+ - Natural, sequential programming style that makes it easy to reason about
37
+ concurrent code.
38
+ - Higher-order constructs for controlling the execution of concurrent code:
39
+ coprocesses, supervisors, cancel scopes, throttling, resource pools etc.
40
+ - Code can use native networking classes and libraries, growing support for
41
+ third-party gems such as `pg` and `redis`.
42
+ - Comprehensive HTTP 1.0 / 1.1 / 2 client and server APIs.
43
+ - Excellent performance and scalability characteristics, in terms of both
44
+ throughput and memory consumption.
45
+
46
+ ## Prior Art
47
+
48
+ Polyphony draws inspiration from the following, in no particular order:
49
+
50
+ * [nio4r](https://github.com/socketry/nio4r/) and [async](https://github.com/socketry/async)
51
+ * [EventMachine](https://github.com/eventmachine/eventmachine)
52
+ * [Trio](https://trio.readthedocs.io/)
53
+ * [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually,
54
+ Erlang in general)
55
+
56
+ ## Installing Polyphony
57
+
58
+ ```bash
59
+ $ gem install polyphony
60
+ ```
61
+
62
+ ## Getting Started
63
+
64
+ Polyphony is designed to help you write high-performance, concurrent code in
65
+ Ruby. It does so by turning every call which might block, such as `sleep` or
66
+ `read` into a concurrent operation, which yields control to an event reactor.
67
+ The reactor, in turn, may schedule other operations once they can be resumed. In
68
+ that manner, multiple ongoing operations may be processed concurrently.
69
+
70
+ There are multiple ways to start a concurrent operation, the most common of
71
+ which is `Kernel#spawn`:
72
+
73
+ ```ruby
74
+ require 'polyphony'
75
+
76
+ spawn do
77
+ puts "A going to sleep"
78
+ sleep 1
79
+ puts "A woken up"
80
+ end
81
+
82
+ spawn do
83
+ puts "B going to sleep"
84
+ sleep 1
85
+ puts "B woken up"
86
+ end
87
+ ```
88
+
89
+ In the above example, both `sleep` calls will be executed concurrently, and thus
90
+ the program will take approximately only 1 second to execute. Note the lack of
91
+ any boilerplate relating to concurrency. Each `spawn` block starts a
92
+ *coroutine*, and is executed in sequential manner.
93
+
94
+ > **Coroutines - the basic unit of concurrency**: In Polyphony, concurrent
95
+ > operations take place inside coroutines. A `Coroutine` is executed on top of a
96
+ > `Fiber`, which allows it to be suspended whenever a blocking operation is
97
+ > called, and resumed once that operation has been completed. Coroutines offer
98
+ > significant advantages over threads - they consume only about 10KB, switching
99
+ > between them is much faster than switching threads, and literally millions of
100
+ > them can be spawned without affecting performance*. Besides, Ruby does not yet
101
+ > allow parallel execution of threads.
102
+ >
103
+ > \* *This is a totally unsubstantiated claim which has not been proved in
104
+ > practice*.
105
+
106
+ ## An echo server in Polyphony
107
+
108
+ To take matters further, let's see how networking can be done using Polyphony.
109
+ Here's a bare-bones echo server written using Polyphony:
110
+
111
+ ```ruby
112
+ require 'polyphony'
113
+
114
+ server = TCPServer.open(1234)
115
+ while client = server.accept
116
+ # spawn starts a new coroutine on a separate fiber
117
+ spawn {
118
+ while data = client.read rescue nil
119
+ client.write(data)
120
+ end
121
+ }
122
+ end
123
+ ```
124
+
125
+ This example demonstrates several features of Polyphony:
126
+
127
+ - The code uses the native `TCPServer` class from Ruby's stdlib, to setup a TCP
128
+ server. The result of `server.accept` is also a native `TCPSocket` object.
129
+ There are no wrapper classes being used.
130
+ - The only hint of the code being concurrent is the use of `Kernel#spawn`,
131
+ which starts a new coroutine on a dedicated fiber. This allows serving
132
+ multiple clients at once. Whenever a blocking call is issued, such as
133
+ `#accept` or `#read`, execution is *yielded* to the event loop, which will
134
+ resume only those coroutines which are ready to be resumed.
135
+ - Exception handling is done using the normal Ruby constructs `raise`, `rescue`
136
+ and `ensure`. Exceptions never go unhandled (as might be the case with Ruby
137
+ threads), and must be dealt with explicitly. An unhandled exception will cause
138
+ the Ruby process to exit.
139
+
140
+ ## Going further
141
+
142
+ To learn more about using Polyphony to build concurrent applications, read the
143
+ technical overview below, or look at the [included examples](examples). A
144
+ thorough reference is forthcoming.
145
+
146
+ ## How Polyphony Works - a Technical Overview
147
+
148
+ ### Fiber-based concurrency
149
+
150
+ The built-in `Fiber` class provides a very elegant, if low-level, foundation for
151
+ implementing cooperative, light-weight concurrency (it can also be used for other stuff like generators). Fiber or continuation-based concurrency can be
152
+ considered as the
153
+ [*third way*](https://en.wikipedia.org/wiki/Fiber_(computer_science))
154
+ of writing concurrent programs (the other two being multi-process concurrency
155
+ and multi-thread concurrency), and can provide very good performance
156
+ characteristics for I/O-bound applications.
157
+
158
+ In contrast to callback-based concurrency (e.g. Node.js or EventMachine), fibers
159
+ allow writing concurrent code in a sequential manner without having to split
160
+ your logic into different locations, or submitting to
161
+ [callback hell](http://callbackhell.com/).
162
+
163
+ Polyphony builds on the foundation of Ruby fibers in order to facilitate writing
164
+ high-performance I/O-bound applications in Ruby.
165
+
166
+ ### Context-switching on blocking calls
167
+
168
+ Ruby monkey-patches existing methods such as `sleep` or `IO#read` to setup an
169
+ IO watcher and suspend the current fiber until the IO object is ready to be
170
+ read. Once the IO watcher is signalled, the associated fiber is resumed and the
171
+ method call can continue. Here's a simplified implementation of
172
+ [`IO#read`](lib/polyphony/io.rb#24-36):
173
+
174
+ ```ruby
175
+ class IO
176
+ def read(max = 8192)
177
+ loop do
178
+ result = read_nonblock(max, exception: false)
179
+ case result
180
+ when nil then raise IOError
181
+ when :wait_readable then read_watcher.await
182
+ else return result
183
+ end
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
189
+ The magic starts in [`IOWatcher#await`](ext/ev/io.c#157-179), where the watcher
190
+ is started and the current fiber is suspended (it "yields" in Ruby parlance).
191
+ Here's a naïve implementation (the actual implementation is written in C):
192
+
193
+ ```ruby
194
+ class IOWatcher
195
+ def await
196
+ @fiber = Fiber.current
197
+ start
198
+ yield_to_reactor_fiber
199
+ end
200
+ end
201
+ ```
202
+
203
+ > **Running a high-performance event loop**: Polyphony runs a libev-based event
204
+ > loop that watches events such as IO-readiness, elapsed timers, received
205
+ > signals and other asynchronous happenings, and uses them to control fiber
206
+ > execution. The event loop itself is run on a separate fiber, allowing the main
207
+ > fiber as well to perform blocking operations.
208
+
209
+ When the IO watcher is [signalled](ext/ev/io.c#99-116): the fiber associated
210
+ with the watcher is resumed, and control is given back to the calling method.
211
+ Here's a naïve implementation:
212
+
213
+ ```ruby
214
+ class IOWatcher
215
+ def signal
216
+ @fiber.transfer
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### Additional concurrency constructs
222
+
223
+ In order to facilitate writing concurrent code, Polyphony provides additional
224
+ constructs that make it easier to spawn concurrent tasks and to control them.
225
+
226
+ `CancelScope` - an abstraction used to cancel the execution of one or more
227
+ coroutines or supervisors. It usually works by defining a timeout for the
228
+ completion of a task. Any blocking operation can be cancelled, including
229
+ a coroutine or a supervisor. The developer may choose to cancel with or without
230
+ an exception with `cancel` or `move_on`, respectively. Cancel scopes are
231
+ typically started using `Kernel.cancel_after` and `Kernel.move_on`:
232
+
233
+ ```ruby
234
+ def echoer(client)
235
+ # cancel after 10 seconds if inactivity
236
+ move_on_after(10) { |scope|
237
+ loop {
238
+ data = client.read
239
+ scope.reset_timeout
240
+ client.write
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ `ResourcePool` - a class used to control access to shared resources. It can be
247
+ used to control concurrent access to database connections, or to limit
248
+ concurrent requests to an external API:
249
+
250
+ ```ruby
251
+ # up to 5 concurrent connections
252
+ Pool = Polyphony::ResourcePool.new(limit: 5) {
253
+ # the block sets up the resource
254
+ PG.connect(...)
255
+ }
256
+
257
+ 1000.times {
258
+ spawn {
259
+ Pool.acquire { |db| p db.query('select 1') }
260
+ }
261
+ }
262
+ ```
263
+
264
+ `Supervisor` - a class used to control one or more `Coroutine`s. It can be used
265
+ to start, stop and restart multiple coroutines. A supervisor can also be
266
+ used for awaiting the completion of multiple coroutines. It is usually started
267
+ using `Kernel.supervise`:
268
+
269
+ ```ruby
270
+ supervise { |s|
271
+ s.spawn { sleep 1 }
272
+ s.spawn { sleep 2 }
273
+ s.spawn { sleep 3 }
274
+ }
275
+ puts "done sleeping"
276
+ ```
277
+
278
+ `ThreadPool` - a pool of threads used to run any operation that cannot be
279
+ implemented using non-blocking calls, such as file system calls. The operation
280
+ is offloaded to a worker thread, allowing the event loop to continue processing
281
+ other tasks. For example, `IO.read` and `File.stat` are both reimplemented
282
+ using the Polyphony thread pool. You can easily use the thread pool to run your
283
+ own blocking operations as follows:
284
+
285
+ ```ruby
286
+ result = Polyphony::ThreadPool.process { long_running_process }
287
+ ```
288
+
289
+ `Throttler` - a mechanism for throttling an arbitrary task, such as sending of
290
+ emails, or crawling a website. A throttler is normally created using
291
+ `Kernel.throttle`, and can even be used to throttle operations across multiple
292
+ coroutines:
293
+
294
+ ```ruby
295
+ server = Net.tcp_listen(1234)
296
+ throttler = throttle(rate: 10) # up to 10 times per second
297
+
298
+ while client = server.accept
299
+ spawn {
300
+ throttler.call {
301
+ while data = client.read
302
+ client.write(data)
303
+ end
304
+ }
305
+ }
306
+ end
307
+ ```
308
+
309
+ ## API Reference
310
+
311
+ To be continued...
312
+
313
+ ## Extending Polyphony
314
+
315
+ Polyphony was designed to ease the transition from blocking APIs and
316
+ callback-based API to non-blocking, fiber-based ones. It is important to
317
+ understand that not all blocking calls can be easily converted into
318
+ non-blocking calls. That might be the case with Ruby gems based on C-extensions,
319
+ such as database libraries. In that case, Polyphony's built-in
320
+ [thread pool](#threadpool) might be used for offloading such blocking calls.
321
+
322
+ ### Adapting callback-based APIs
323
+
324
+ Some of the most common patterns in Ruby APIs is the callback pattern, in which
325
+ the API takes a block as a callback to be called upon completion of a task. One
326
+ such example can be found in the excellent
327
+ [http_parser.rb](https://github.com/tmm1/http_parser.rb/) gem, which is used by
328
+ Polyphony itself to provide HTTP 1 functionality. The `HTTP:Parser` provides
329
+ multiple hooks, or callbacks, for being notified when an HTTP request is
330
+ complete. The typical callback-based setup is as follows:
331
+
332
+ ```ruby
333
+ require 'http/parser'
334
+ @parser = Http::Parser.new
335
+
336
+ def on_receive(data)
337
+ @parser < data
338
+ end
339
+
340
+ @parser.on_message_complete do |env|
341
+ process_request(env)
342
+ end
343
+ ```
344
+
345
+ A program using `http_parser.rb` in conjunction with Polyphony might do the
346
+ following:
347
+
348
+ ```ruby
349
+ require 'http/parser'
350
+ require 'modulation'
351
+
352
+ def handle_client(client)
353
+ parser = Http::Parser.new
354
+ req = nil
355
+ parser.on_message_complete { |env| req = env }
356
+ loop do
357
+ parser << client.read
358
+ if req
359
+ handle_request(req)
360
+ req = nil
361
+ end
362
+ end
363
+ end
364
+ ```
365
+
366
+ Another possibility would be to monkey-patch `Http::Parser` in order to
367
+ encapsulate the state of the request:
368
+
369
+ ```ruby
370
+ class Http::Parser
371
+ def setup
372
+ self.on_message_complete = proc { @request_complete = true }
373
+ end
374
+
375
+ def parser(data)
376
+ self << data
377
+ return nil unless @request_complete
378
+
379
+ @request_complete = nil
380
+ self
381
+ end
382
+ end
383
+
384
+ def handle_client(client)
385
+ parser = Http::Parser.new
386
+ loop do
387
+ if req == parser.parse(client.read)
388
+ handle_request(req)
389
+ end
390
+ end
391
+ end
392
+ ```
393
+
394
+ ### Contributing to Polyphony
395
+
396
+ If there's some blocking behavior you'd like to see handled by Polyphony, please
397
+ let us know by
398
+ [creating an issue](https://github.com/digital-fabric/polyphony/issues). Our aim
399
+ is for Polyphony to be a comprehensive solution for writing concurrent Ruby
400
+ programs.