README.md in polyphony-0.20 vs README.md in polyphony-0.21

- old
+ new

@@ -1,65 +1,36 @@ -# Polyphony - lightweight concurrency for Ruby +# Polyphony - Easy Concurrency for Ruby -[INSTALL](#installing-polyphony) | -[TUTORIAL](#getting-started) | -[EXAMPLES](examples) | -[TEHNICAL OVERVIEW](#how-polyphony-works---a-technical-overview) | -[REFERENCE](#api-reference) | -[EXTENDING](#extending-polyphony) +[DOCS](https://dfab.gitbook.io/polyphony) | +[EXAMPLES](examples) -> Polyphony | pəˈlɪf(ə)ni | *Music* - the style of simultaneously combining a -> number of parts, each forming an individual melody and harmonizing with each -> other. +> Polyphony \| pəˈlɪf\(ə\)ni \| _Music_ - the style of simultaneously combining a number of parts, each forming an individual melody and harmonizing with each other. -**Note**: Polyphony is experimental software. It is designed to work with recent -versions of Ruby (2.6 and newer) and supports Linux and MacOS only. - ## What is Polyphony -Polyphony is a library for building concurrent applications in Ruby. Polyphony -harnesses the power of -[Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html) to provide a -cooperative, sequential coprocess-based concurrency model. Under the hood, -Polyphony uses [libev](https://github.com/enki/libev) as a high-performance event -reactor that provides timers, I/O watchers and other asynchronous event -primitives. +Polyphony is a library for building concurrent applications in Ruby. Polyphony harnesses the power of [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html) to provide a cooperative, sequential coprocess-based concurrency model. Under the hood, Polyphony uses [libev](https://github.com/enki/libev) as a high-performance event reactor that provides timers, I/O watchers and other asynchronous event primitives. -Polyphony makes it possible to use normal Ruby built-in classes like `IO`, and -`Socket` in a concurrent fashion without having to resort to threads. Polyphony -takes care of context-switching automatically whenever a blocking call like -`Socket#accept` or `IO#read` is issued. +Polyphony makes it possible to use normal Ruby built-in classes like `IO`, and `Socket` in a concurrent fashion without having to resort to threads. Polyphony takes care of context-switching automatically whenever a blocking call like `Socket#accept` or `IO#read` is issued. ## Features -- **Full-blown, integrated, high-performance HTTP 1 / HTTP 2 / WebSocket server +* **Full-blown, integrated, high-performance HTTP 1 / HTTP 2 / WebSocket server with TLS/SSL termination, automatic ALPN protocol selection, and body streaming**. -- Co-operative scheduling of concurrent tasks using Ruby fibers. -- High-performance event reactor for handling I/O events and timers. -- Natural, sequential programming style that makes it easy to reason about +* Co-operative scheduling of concurrent tasks using Ruby fibers. +* High-performance event reactor for handling I/O events and timers. +* Natural, sequential programming style that makes it easy to reason about concurrent code. -- Abstractions and constructs for controlling the execution of concurrent code: +* Abstractions and constructs for controlling the execution of concurrent code: coprocesses, supervisors, cancel scopes, throttling, resource pools etc. -- Code can use native networking classes and libraries, growing support for +* Code can use native networking classes and libraries, growing support for third-party gems such as `pg` and `redis`. -- Use stdlib classes such as `TCPServer`, `TCPSocket` and -- HTTP 1 / HTTP 2 client agent with persistent connections. -- Competitive performance and scalability characteristics, in terms of both +* Use stdlib classes such as `TCPServer`, `TCPSocket` and +* HTTP 1 / HTTP 2 client agent with persistent connections. +* Competitive performance and scalability characteristics, in terms of both throughput and memory consumption. -## Why you should not use Polyphony - -- Polyphony does weird things to Ruby, like patching methods like `IO.read`, - `Kernel#sleep`, and `Timeout.timeout` so they'll work concurrently without - using threads. -- Error backtraces might look weird. -- There's currently no support for threads - any IO operations in threads will - likely cause a bad crash. -- Debugging might be confusing or not work at all. -- The API is currently unstable. - ## Prior Art Polyphony draws inspiration from the following, in no particular order: * [nio4r](https://github.com/socketry/nio4r/) and [async](https://github.com/socketry/async) @@ -68,411 +39,9 @@ * [EventMachine](https://github.com/eventmachine/eventmachine) * [Trio](https://trio.readthedocs.io/) * [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually, Erlang in general) -## Installing Polyphony +## Documentation -```bash -$ gem install polyphony -``` - -Or add it to your Gemfile, you know the drill. - -## Getting Started - -Polyphony is designed to help you write high-performance, concurrent code in -Ruby, without using threads. It does so by turning every call which might block, -such as `Kernel#sleep` or `IO#read` into a concurrent operation, which yields -control to an event reactor. The reactor, in turn, may schedule other operations -once they can be resumed. In that manner, multiple ongoing operations may be -processed concurrently. - -The simplest way to start a concurrent operation is using `Kernel#spin`: - -```ruby -require 'polyphony' - -spin do - puts "A going to sleep" - sleep 1 - puts "A woken up" -end - -spin do - puts "B going to sleep" - sleep 1 - puts "B woken up" -end -``` - -In the above example, both `sleep` calls will be executed concurrently, and thus -the program will take approximately only 1 second to execute. Note how the logic -flow inside each `spin` block is purely sequential, and how the concurrent -nature of the two blocks is expressed simply and cleanly. - -## Coprocesses - Polyphony's basic unit of concurrency - -In Polyphony, concurrent operations take place inside coprocesses. A `Coprocess` -is executed on top of a `Fiber`, which allows it to be suspended whenever a -blocking operation is called, and resumed once that operation has been -completed. Coprocesses offer significant advantages over threads - they consume -only about 10KB, switching between them is much faster than switching threads, -and literally millions of them can be spinned off without affecting -performance*. Besides, Ruby does not yet allow parallel execution of threads -(courtesy of the Ruby GVL). - -\* *This is a totally unsubstantiated claim and has not been proven in practice*. - -## An echo server in Polyphony - -Let's now examine how networking is done using Polyphony. Here's a bare-bones -echo server written using Polyphony: - -```ruby -require 'polyphony' - -server = TCPServer.open(1234) -while client = server.accept - spin do - while (data = client.gets) - client << data - end - end -end -``` - -This example demonstrates several features of Polyphony: - -- The code uses the native `TCPServer` class from Ruby's stdlib, to setup a TCP - server. The result of `server.accept` is also a native `TCPSocket` object. - There are no wrapper classes being used. -- The only hint of the code being concurrent is the use of `Kernel#spin`, - which starts a new coprocess on a dedicated fiber. This allows serving - multiple clients at once. Whenever a blocking call is issued, such as - `#accept` or `#read`, execution is *yielded* to the event reactor loop, which - will resume only those coprocesses which are ready to be resumed. -- Exception handling is done using the normal Ruby constructs `raise`, `rescue` - and `ensure`. Exceptions never go unhandled (as might be the case with Ruby - threads), and must be dealt with explicitly. An unhandled exception will by - default cause the Ruby process to exit. - -## Additional concurrency constructs - -In order to facilitate writing concurrent code, Polyphony provides additional -mechanisms that make it easier to create and control concurrent tasks. - -### Cancel scopes - -Cancel scopes, an idea borrowed from Python's -[Trio](https://trio.readthedocs.io/) library, are used to cancel the execution -of one or more coprocesses. The most common use of cancel scopes is a for -implementing a timeout for the completion of a task. Any blocking operation can -be cancelled. The programmer may choose to raise a `Cancel` exception when an -operation has been cancelled, or alternatively to move on without any exception. - -Cancel scopes are typically started using `Kernel#cancel_after` and -`Kernel#move_on_after` for cancelling with or without an exception, -respectively. Cancel scopes will take a block of code to execute and run it, -providing a reference to the cancel scope: - -```ruby -puts "going to sleep (but really only for 1 second)..." -cancel_after(1) do - sleep(60) -end -``` - -Patterns like closing a connection after X seconds of activity are greatly -facilitated by timeout-based cancel scopes, which can be easily reset: - -```ruby -def echoer(client) - # close connection after 10 seconds of inactivity - move_on_after(10) do |scope| - scope.when_cancelled { puts "closing connection due to inactivity" } - loop do - data = client.read - scope.reset_timeout - client.write - end - end - client.close -end -``` - -Cancel scopes may also be manually cancelled by calling `CancelScope#cancel!` -at any time: - -```ruby -def echoer(client) - move_on_after(60) do |scope| - loop do - data = client.read - scope.cancel! if data == 'stop' - client.write - end - end - client.close -end -``` - -### Resource pools - -A resource pool is used to control access to one or more shared, usually -identical resources. For example, a resource pool can be used to control -concurrent access to database connections, or to limit concurrent -requests to an external API: - -```ruby -# up to 5 concurrent connections -Pool = Polyphony::ResourcePool.new(limit: 5) { - # the block sets up the resource - PG.connect(...) -} - -1000.times { - spin { - Pool.acquire { |db| p db.query('select 1') } - } -} -``` - -You can also call arbitrary methods on the resource pool, which will be -delegated to the resource using `#method_missing`: - -```ruby -# up to 5 concurrent connections -Pool = Polyphony::ResourcePool.new(limit: 5) { - # the block sets up the resource - PG.connect(...) -} - -1000.times { - spin { p Pool.query('select pg_sleep(0.01);') } -} -``` - -### Supervisors - -A supervisor is used to control one or more coprocesses. It can be used to -start, stop, restart and await the completion of multiple coprocesses. It is -normally started using `Kernel#supervise`: - -```ruby -supervise { |s| - s.spin { sleep 1 } - s.spin { sleep 2 } - s.spin { sleep 3 } -} -puts "done sleeping" -``` - -The `Kernel#supervise` method will await the completion of all supervised -coprocesses. If any supervised coprocess raises an error, the supervisor will -automatically cancel all other supervised coprocesses. - -### Throttlers - -A throttler is a mechanism for controlling the speed of an arbitrary task, -such as sending of emails, or crawling a website. A throttler is normally -created using `Kernel#throttle` or `Kernel#throttled_loop`, and can even be used -to throttle operations across multiple coprocesses: - -```ruby -server = Polyphony::Net.tcp_listen(1234) - -# a shared throttler, up to 10 times per second -throttler = throttle(rate: 10) - -while client = server.accept - spin do - throttler.call do - while data = client.read - client.write(data) - end - end - end -end -``` - -`Kernel#throttled_loop` can be used to run throttled infinite loops: - -```ruby -throttled_loop(3) do - STDOUT << '.' -end -``` - -## Going further - -To learn more about using Polyphony to build concurrent applications, read the -technical overview below, or look at the [included examples](examples). A -thorough reference is forthcoming. - -## How Polyphony Works - a Technical Overview - -### Fiber-based concurrency - -The built-in `Fiber` class provides a very elegant, if low-level, foundation for -implementing cooperative, light-weight concurrency (it can also be used for other stuff like generators). Fiber or continuation-based concurrency can be -considered as the -[*third way*](https://en.wikipedia.org/wiki/Fiber_(computer_science)) -of writing concurrent programs (the other two being multi-process concurrency -and multi-thread concurrency), and can provide very good performance -characteristics for I/O-bound applications. - -In contrast to callback-based concurrency (e.g. Node.js or EventMachine), fibers -allow writing concurrent code in a sequential manner without having to split -your logic into different locations, or submitting to -[callback hell](http://callbackhell.com/). - -Polyphony builds on the foundation of Ruby fibers in order to facilitate writing -high-performance I/O-bound applications in Ruby. - -### Context-switching on blocking calls - -Ruby monkey-patches existing methods such as `sleep` or `IO#read` to setup an -IO watcher and suspend the current fiber until the IO object is ready to be -read. Once the IO watcher is signalled, the associated fiber is resumed and the -method call can continue. Here's a simplified implementation of -[`IO#read`](lib/polyphony/io.rb#24-36): - -```ruby -class IO - def read(max = 8192) - loop do - result = read_nonblock(max, exception: false) - case result - when nil then raise IOError - when :wait_readable then read_watcher.await - else return result - end - end - end -end -``` - -The magic starts in [`IOWatcher#await`](ext/ev/io.c#157-179), where the watcher -is started and the current fiber is suspended (it "yields" in Ruby parlance). -Here's a naïve implementation (the actual implementation is written in C): - -```ruby -class IOWatcher - def await - @fiber = Fiber.current - start - yield_to_reactor_fiber - end -end -``` - -> **Running a high-performance event loop**: Polyphony runs a libev-based event -> loop that watches events such as IO-readiness, elapsed timers, received -> signals and other asynchronous happenings, and uses them to control fiber -> execution. The event loop itself is run on a separate fiber, allowing the main -> fiber as well to perform blocking operations. - -When the IO watcher is [signalled](ext/ev/io.c#99-116): the fiber associated -with the watcher is resumed, and control is given back to the calling method. -Here's a naïve implementation: - -```ruby -class IOWatcher - def signal - @fiber.transfer - end -end -``` - -## API Reference - -To be continued... - -## Extending Polyphony - -Polyphony was designed to ease the transition from blocking APIs and -callback-based API to non-blocking, fiber-based ones. It is important to -understand that not all blocking calls can be easily converted into -non-blocking calls. That might be the case with Ruby gems based on C-extensions, -such as database libraries. In that case, Polyphony's built-in -[thread pool](#threadpool) might be used for offloading such blocking calls. - -### Adapting callback-based APIs - -Some of the most common patterns in Ruby APIs is the callback pattern, in which -the API takes a block as a callback to be called upon completion of a task. One -such example can be found in the excellent -[http_parser.rb](https://github.com/tmm1/http_parser.rb/) gem, which is used by -Polyphony itself to provide HTTP 1 functionality. The `HTTP:Parser` provides -multiple hooks, or callbacks, for being notified when an HTTP request is -complete. The typical callback-based setup is as follows: - -```ruby -require 'http/parser' -@parser = Http::Parser.new - -def on_receive(data) - @parser < data -end - -@parser.on_message_complete do |env| - process_request(env) -end -``` - -A program using `http_parser.rb` in conjunction with Polyphony might do the -following: - -```ruby -require 'http/parser' -require 'polyphony' - -def handle_client(client) - parser = Http::Parser.new - req = nil - parser.on_message_complete { |env| req = env } - loop do - parser << client.read - if req - handle_request(req) - req = nil - end - end -end -``` - -Another possibility would be to monkey-patch `Http::Parser` in order to -encapsulate the state of the request: - -```ruby -class Http::Parser - def setup - self.on_message_complete = proc { @request_complete = true } - end - - def parser(data) - self << data - return nil unless @request_complete - - @request_complete = nil - self - end -end - -def handle_client(client) - parser = Http::Parser.new - loop do - if req == parser.parse(client.read) - handle_request(req) - end - end -end -``` - -### Contributing to Polyphony - -If there's some blocking behavior you'd like to see handled by Polyphony, please -let us know by -[creating an issue](https://github.com/digital-fabric/polyphony/issues). Our aim -is for Polyphony to be a comprehensive solution for writing concurrent Ruby -programs. \ No newline at end of file +The complete documentation for Polyphony could be found on the +[Polyphony website](https://dfab.gitbook.io/polyphony).