polyphony 0.13

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 (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.