polyphony 0.19 → 0.20
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.rubocop.yml +87 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile.lock +17 -6
- data/README.md +200 -139
- data/Rakefile +4 -4
- data/TODO.md +35 -7
- data/bin/poly +11 -0
- data/docs/getting-started/getting-started.md +1 -1
- data/docs/summary.md +3 -0
- data/docs/technical-overview/exception-handling.md +94 -0
- data/docs/technical-overview/fiber-scheduling.md +99 -0
- data/examples/core/cancel.rb +8 -4
- data/examples/core/channel_echo.rb +18 -17
- data/examples/core/defer.rb +12 -0
- data/examples/core/enumerator.rb +4 -4
- data/examples/core/fiber_error.rb +9 -0
- data/examples/core/fiber_error_with_backtrace.rb +73 -0
- data/examples/core/fork.rb +6 -6
- data/examples/core/genserver.rb +16 -8
- data/examples/core/lock.rb +3 -3
- data/examples/core/move_on.rb +4 -3
- data/examples/core/move_on_twice.rb +5 -5
- data/examples/core/move_on_with_ensure.rb +8 -11
- data/examples/core/move_on_with_value.rb +14 -0
- data/examples/core/{multiple_spawn.rb → multiple_spin.rb} +5 -5
- data/examples/core/nested_cancel.rb +5 -5
- data/examples/core/{nested_multiple_spawn.rb → nested_multiple_spin.rb} +6 -6
- data/examples/core/nested_spin.rb +17 -0
- data/examples/core/pingpong.rb +21 -0
- data/examples/core/pulse.rb +4 -5
- data/examples/core/resource.rb +6 -4
- data/examples/core/resource_cancel.rb +6 -9
- data/examples/core/resource_delegate.rb +3 -3
- data/examples/core/sleep.rb +3 -3
- data/examples/core/sleep_spin.rb +19 -0
- data/examples/core/snooze.rb +32 -0
- data/examples/core/spin.rb +14 -0
- data/examples/core/{spawn_cancel.rb → spin_cancel.rb} +6 -7
- data/examples/core/spin_error.rb +17 -0
- data/examples/core/spin_error_backtrace.rb +30 -0
- data/examples/core/spin_uncaught_error.rb +15 -0
- data/examples/core/supervisor.rb +8 -8
- data/examples/core/supervisor_with_cancel_scope.rb +7 -7
- data/examples/core/supervisor_with_error.rb +8 -8
- data/examples/core/supervisor_with_manual_move_on.rb +6 -7
- data/examples/core/suspend.rb +13 -0
- data/examples/core/thread.rb +1 -1
- data/examples/core/thread_cancel.rb +9 -11
- data/examples/core/thread_pool.rb +18 -14
- data/examples/core/throttle.rb +7 -7
- data/examples/core/timeout.rb +3 -3
- data/examples/fs/read.rb +7 -9
- data/examples/http/config.ru +7 -3
- data/examples/http/cuba.ru +22 -0
- data/examples/http/happy_eyeballs.rb +6 -4
- data/examples/http/http_client.rb +1 -1
- data/examples/http/http_get.rb +1 -1
- data/examples/http/http_parse_experiment.rb +21 -16
- data/examples/http/http_proxy.rb +28 -26
- data/examples/http/http_server.rb +10 -10
- data/examples/http/http_server_forked.rb +6 -5
- data/examples/http/http_server_throttled.rb +3 -3
- data/examples/http/http_ws_server.rb +11 -11
- data/examples/http/https_raw_client.rb +1 -1
- data/examples/http/https_server.rb +8 -8
- data/examples/http/https_wss_server.rb +13 -11
- data/examples/http/rack_server.rb +2 -2
- data/examples/http/rack_server_https.rb +4 -4
- data/examples/http/rack_server_https_forked.rb +5 -5
- data/examples/http/websocket_secure_server.rb +6 -6
- data/examples/http/websocket_server.rb +5 -5
- data/examples/interfaces/pg_client.rb +4 -4
- data/examples/interfaces/pg_pool.rb +13 -6
- data/examples/interfaces/pg_transaction.rb +5 -4
- data/examples/interfaces/redis_channels.rb +15 -11
- data/examples/interfaces/redis_client.rb +2 -2
- data/examples/interfaces/redis_pubsub.rb +2 -1
- data/examples/interfaces/redis_pubsub_perf.rb +13 -9
- data/examples/io/backticks.rb +11 -0
- data/examples/io/cat.rb +4 -5
- data/examples/io/echo_client.rb +9 -4
- data/examples/io/echo_client_from_stdin.rb +20 -0
- data/examples/io/echo_pipe.rb +7 -8
- data/examples/io/echo_server.rb +8 -6
- data/examples/io/echo_server_with_timeout.rb +13 -10
- data/examples/io/echo_stdin.rb +3 -3
- data/examples/io/httparty.rb +2 -2
- data/examples/io/httparty_multi.rb +8 -4
- data/examples/io/httparty_threaded.rb +6 -2
- data/examples/io/io_read.rb +2 -2
- data/examples/io/irb.rb +16 -4
- data/examples/io/net-http.rb +3 -3
- data/examples/io/open.rb +17 -0
- data/examples/io/system.rb +3 -3
- data/examples/io/tcpserver.rb +15 -0
- data/examples/io/tcpsocket.rb +6 -5
- data/examples/performance/multi_snooze.rb +29 -0
- data/examples/performance/{perf_snooze.rb → snooze.rb} +7 -5
- data/examples/performance/snooze_raw.rb +39 -0
- data/ext/gyro/async.c +165 -0
- data/ext/gyro/child.c +167 -0
- data/ext/{ev → gyro}/extconf.rb +4 -3
- data/ext/gyro/gyro.c +316 -0
- data/ext/{ev/ev.h → gyro/gyro.h} +12 -7
- data/ext/gyro/gyro_ext.c +23 -0
- data/ext/{ev → gyro}/io.c +65 -57
- data/ext/{ev → gyro}/libev.h +0 -0
- data/ext/gyro/signal.c +117 -0
- data/ext/{ev → gyro}/socket.c +61 -6
- data/ext/gyro/timer.c +199 -0
- data/ext/libev/Changes +35 -0
- data/ext/libev/README +2 -1
- data/ext/libev/ev.c +213 -151
- data/ext/libev/ev.h +95 -88
- data/ext/libev/ev_epoll.c +26 -15
- data/ext/libev/ev_kqueue.c +11 -5
- data/ext/libev/ev_linuxaio.c +642 -0
- data/ext/libev/ev_poll.c +13 -8
- data/ext/libev/ev_port.c +5 -2
- data/ext/libev/ev_vars.h +14 -3
- data/ext/libev/ev_wrap.h +16 -0
- data/lib/ev_ext.bundle +0 -0
- data/lib/polyphony.rb +46 -50
- data/lib/polyphony/auto_run.rb +12 -0
- data/lib/polyphony/core/cancel_scope.rb +11 -7
- data/lib/polyphony/core/channel.rb +16 -9
- data/lib/polyphony/core/coprocess.rb +101 -51
- data/lib/polyphony/core/exceptions.rb +14 -12
- data/lib/polyphony/core/resource_pool.rb +21 -8
- data/lib/polyphony/core/supervisor.rb +10 -5
- data/lib/polyphony/core/sync.rb +7 -6
- data/lib/polyphony/core/thread.rb +4 -4
- data/lib/polyphony/core/thread_pool.rb +4 -4
- data/lib/polyphony/core/throttler.rb +6 -4
- data/lib/polyphony/extensions/core.rb +253 -0
- data/lib/polyphony/extensions/io.rb +28 -16
- data/lib/polyphony/extensions/openssl.rb +2 -1
- data/lib/polyphony/extensions/socket.rb +47 -52
- data/lib/polyphony/http.rb +4 -3
- data/lib/polyphony/http/agent.rb +68 -57
- data/lib/polyphony/http/server.rb +5 -5
- data/lib/polyphony/http/server/http1.rb +268 -0
- data/lib/polyphony/http/server/http2.rb +62 -0
- data/lib/polyphony/http/server/http2_stream.rb +104 -0
- data/lib/polyphony/http/server/rack.rb +64 -0
- data/lib/polyphony/http/server/request.rb +119 -0
- data/lib/polyphony/net.rb +26 -15
- data/lib/polyphony/postgres.rb +17 -13
- data/lib/polyphony/redis.rb +16 -15
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony/websocket.rb +11 -4
- data/polyphony.gemspec +13 -9
- data/test/eg.rb +27 -0
- data/test/helper.rb +25 -0
- data/test/run.rb +5 -0
- data/test/test_async.rb +33 -0
- data/test/test_coprocess.rb +239 -77
- data/test/test_core.rb +95 -61
- data/test/test_gyro.rb +148 -0
- data/test/test_http_server.rb +313 -0
- data/test/test_io.rb +79 -27
- data/test/test_kernel.rb +22 -12
- data/test/test_signal.rb +36 -0
- data/test/test_timer.rb +24 -0
- metadata +89 -33
- data/examples/core/nested_async.rb +0 -17
- data/examples/core/next_tick.rb +0 -12
- data/examples/core/sleep_spawn.rb +0 -19
- data/examples/core/spawn.rb +0 -14
- data/examples/core/spawn_error.rb +0 -28
- data/examples/performance/perf_multi_snooze.rb +0 -21
- data/ext/ev/async.c +0 -168
- data/ext/ev/child.c +0 -169
- data/ext/ev/ev_ext.c +0 -23
- data/ext/ev/ev_module.c +0 -242
- data/ext/ev/signal.c +0 -119
- data/ext/ev/timer.c +0 -197
- data/lib/polyphony/core/fiber_pool.rb +0 -98
- data/lib/polyphony/extensions/kernel.rb +0 -169
- data/lib/polyphony/http/http1_adapter.rb +0 -254
- data/lib/polyphony/http/http2_adapter.rb +0 -157
- data/lib/polyphony/http/rack.rb +0 -25
- data/lib/polyphony/http/request.rb +0 -66
- data/test/test_ev.rb +0 -110
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca0e1fc13842ec06d7272fb19e0edc1621eddb4a007ff9b97e07794e60a9d814
|
4
|
+
data.tar.gz: 78cab004907a41474ff1920cbb9f5aceb6851797e6805f9c16256f3d48457eff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96de0a203333388768a41d8c81c333e80b9ab20ebf001b744e1a5d0dd8e6bb036ebc5b4d4abe7a85b4016e264afd4f3609cc8aeed9c8bbfc401010966c8090f2
|
7
|
+
data.tar.gz: 30507005291c431dd277d30829f6e4c219d2b176a41eef111caadcc8975adaeb335dc4104dc08cb65c58649863bd7a39d21565adee55a795c58d84894419026d
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -7,6 +7,7 @@ AllCops:
|
|
7
7
|
- 'test/**/*.rb'
|
8
8
|
- 'examples/**/*.rb'
|
9
9
|
- 'Gemfile*'
|
10
|
+
- lib/polyphony/http/agent.rb
|
10
11
|
|
11
12
|
Style/LambdaCall:
|
12
13
|
Enabled: false
|
@@ -46,4 +47,89 @@ Style/NumericPredicate:
|
|
46
47
|
Enabled: false
|
47
48
|
|
48
49
|
Style/TrivialAccessors:
|
49
|
-
Enabled: false
|
50
|
+
Enabled: false
|
51
|
+
|
52
|
+
Style/MethodMissingSuper:
|
53
|
+
Enabled: false
|
54
|
+
|
55
|
+
Style/GlobalVars:
|
56
|
+
Exclude:
|
57
|
+
- lib/polyphony/auto_run.rb
|
58
|
+
- lib/polyphony/extensions/core.rb
|
59
|
+
- examples/**/*.rb
|
60
|
+
|
61
|
+
Style/ClassVars:
|
62
|
+
Exclude:
|
63
|
+
- lib/polyphony/core/coprocess.rb
|
64
|
+
|
65
|
+
Layout/AlignHash:
|
66
|
+
EnforcedColonStyle: table
|
67
|
+
EnforcedHashRocketStyle: table
|
68
|
+
|
69
|
+
Naming/AccessorMethodName:
|
70
|
+
Exclude:
|
71
|
+
- lib/polyphony/http/server/http1.rb
|
72
|
+
- lib/polyphony/http/server/http2_stream.rb
|
73
|
+
- examples/**/*.rb
|
74
|
+
|
75
|
+
Naming/MethodName:
|
76
|
+
Exclude:
|
77
|
+
- test/test_signal.rb
|
78
|
+
|
79
|
+
Lint/HandleExceptions:
|
80
|
+
Exclude:
|
81
|
+
- lib/polyphony/http/server/http1.rb
|
82
|
+
- lib/polyphony/http/server/http2.rb
|
83
|
+
- lib/polyphony/http/server.rb
|
84
|
+
- examples/**/*.rb
|
85
|
+
|
86
|
+
Metrics/MethodLength:
|
87
|
+
Exclude:
|
88
|
+
- lib/polyphony/http/server/rack.rb
|
89
|
+
- lib/polyphony/extensions/io.rb
|
90
|
+
- test/**/*.rb
|
91
|
+
- examples/**/*.rb
|
92
|
+
|
93
|
+
Metrics/ModuleLength:
|
94
|
+
Exclude:
|
95
|
+
- lib/polyphony/extensions/core.rb
|
96
|
+
- examples/**/*.rb
|
97
|
+
|
98
|
+
Metrics/ClassLength:
|
99
|
+
Exclude:
|
100
|
+
- lib/polyphony/http/server/http1.rb
|
101
|
+
- test/**/*.rb
|
102
|
+
- examples/**/*.rb
|
103
|
+
|
104
|
+
Style/RegexpLiteral:
|
105
|
+
Enabled: false
|
106
|
+
|
107
|
+
Style/RescueModifier:
|
108
|
+
Exclude:
|
109
|
+
- lib/polyphony/auto_run.rb
|
110
|
+
- test/**/*.rb
|
111
|
+
- examples/**/*.rb
|
112
|
+
|
113
|
+
Style/Documentation:
|
114
|
+
Exclude:
|
115
|
+
- test/**/*.rb
|
116
|
+
- examples/**/*.rb
|
117
|
+
|
118
|
+
Style/FormatString:
|
119
|
+
Exclude:
|
120
|
+
- test/**/*.rb
|
121
|
+
- examples/**/*.rb
|
122
|
+
|
123
|
+
Style/FormatStringToken:
|
124
|
+
Exclude:
|
125
|
+
- test/**/*.rb
|
126
|
+
- examples/**/*.rb
|
127
|
+
|
128
|
+
Naming/UncommunicativeMethodParamName:
|
129
|
+
Exclude:
|
130
|
+
- test/**/*.rb
|
131
|
+
- examples/**/*.rb
|
132
|
+
|
133
|
+
Security/MarshalLoad:
|
134
|
+
Exclude:
|
135
|
+
- examples/**/*.rb
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,38 @@
|
|
1
|
+
0.20 2019-11-27
|
2
|
+
---------------
|
3
|
+
|
4
|
+
* Refactor and improve CancelScope, ResourcePool
|
5
|
+
* Reimplement cancel_after, move_on_after using plain timers
|
6
|
+
* Use Timer#await instead of Timer#start in Pulser
|
7
|
+
* Rename Fiber.main to Fiber.root
|
8
|
+
* Replace use of defer with proper fiber scheduling
|
9
|
+
* Improve Coprocess resume, interrupt, cancel methods
|
10
|
+
* Cleanup code using Rubocop
|
11
|
+
* Update and cleanup examples
|
12
|
+
* Remove fiber pool
|
13
|
+
* Rename `CoprocessInterrupt` to `Interrupt`
|
14
|
+
* Fix ResourcePool, Mutex, Thread, ThreadPool
|
15
|
+
* Fix coprocess message passing behaviour
|
16
|
+
* Add HTTP::Request#consume API
|
17
|
+
* Use bundler 2.x
|
18
|
+
* Remove separate parse loop fiber in HTTP 1, HTTP 2 adapters
|
19
|
+
* Fix handling of exceptions in coprocesses
|
20
|
+
* Implement synthetic, sanitized exception backtrace showing control flow across
|
21
|
+
fibers
|
22
|
+
* Fix channels
|
23
|
+
* Fix HTTP1 connection shutdown and error states
|
24
|
+
* Workaround for IO#read without length
|
25
|
+
* Rename `next_tick` to `defer`
|
26
|
+
* Fix race condition in firing of deferred items, use linked list instead of
|
27
|
+
array for deferred items
|
28
|
+
* Rename `EV` module to `Gyro`
|
29
|
+
* Keep track of main fiber when forking
|
30
|
+
* Add `<<` alias for `send_chunk` in HTTP::Request
|
31
|
+
* Implement Socket#accept in C
|
32
|
+
* Better conformance of rack adapter to rack spec (WIP)
|
33
|
+
* Fix HTTP1 adapter
|
34
|
+
* Better support for debugging with ruby-debug-ide (WIP)
|
35
|
+
|
1
36
|
0.19 2019-06-12
|
2
37
|
---------------
|
3
38
|
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
polyphony (0.
|
4
|
+
polyphony (0.20)
|
5
5
|
http-2 (= 0.10.0)
|
6
6
|
http_parser.rb (= 0.6.0)
|
7
7
|
modulation (~> 0.25)
|
8
|
+
rack
|
8
9
|
|
9
10
|
GEM
|
10
11
|
remote: https://rubygems.org/
|
11
12
|
specs:
|
13
|
+
ansi (1.5.0)
|
14
|
+
builder (3.2.3)
|
12
15
|
hiredis (0.6.3)
|
13
16
|
http-2 (0.10.0)
|
14
17
|
http_parser.rb (0.6.0)
|
@@ -16,17 +19,24 @@ GEM
|
|
16
19
|
mime-types (~> 3.0)
|
17
20
|
multi_xml (>= 0.5.2)
|
18
21
|
localhost (1.1.4)
|
19
|
-
mime-types (3.
|
22
|
+
mime-types (3.3)
|
20
23
|
mime-types-data (~> 3.2015)
|
21
|
-
mime-types-data (3.2019.
|
24
|
+
mime-types-data (3.2019.1009)
|
22
25
|
minitest (5.11.3)
|
23
|
-
|
26
|
+
minitest-reporters (1.4.2)
|
27
|
+
ansi
|
28
|
+
builder
|
29
|
+
minitest (>= 5.0)
|
30
|
+
ruby-progressbar
|
31
|
+
modulation (0.34)
|
24
32
|
multi_xml (0.6.0)
|
25
33
|
pg (1.1.3)
|
26
|
-
|
34
|
+
rack (2.0.7)
|
35
|
+
rake (13.0.1)
|
27
36
|
rake-compiler (1.0.5)
|
28
37
|
rake
|
29
38
|
redis (4.1.0)
|
39
|
+
ruby-progressbar (1.10.1)
|
30
40
|
websocket (1.2.8)
|
31
41
|
|
32
42
|
PLATFORMS
|
@@ -37,6 +47,7 @@ DEPENDENCIES
|
|
37
47
|
httparty (= 0.17.0)
|
38
48
|
localhost (= 1.1.4)
|
39
49
|
minitest (= 5.11.3)
|
50
|
+
minitest-reporters (= 1.4.2)
|
40
51
|
pg (= 1.1.3)
|
41
52
|
polyphony!
|
42
53
|
rake-compiler (= 1.0.5)
|
@@ -44,4 +55,4 @@ DEPENDENCIES
|
|
44
55
|
websocket (= 1.2.8)
|
45
56
|
|
46
57
|
BUNDLED WITH
|
47
|
-
1.
|
58
|
+
2.1.0.pre.2
|
data/README.md
CHANGED
@@ -12,7 +12,7 @@
|
|
12
12
|
> other.
|
13
13
|
|
14
14
|
**Note**: Polyphony is experimental software. It is designed to work with recent
|
15
|
-
versions of Ruby (2.
|
15
|
+
versions of Ruby (2.6 and newer) and supports Linux and MacOS only.
|
16
16
|
|
17
17
|
## What is Polyphony
|
18
18
|
|
@@ -32,7 +32,8 @@ takes care of context-switching automatically whenever a blocking call like
|
|
32
32
|
## Features
|
33
33
|
|
34
34
|
- **Full-blown, integrated, high-performance HTTP 1 / HTTP 2 / WebSocket server
|
35
|
-
with TLS/SSL termination
|
35
|
+
with TLS/SSL termination, automatic ALPN protocol selection, and body
|
36
|
+
streaming**.
|
36
37
|
- Co-operative scheduling of concurrent tasks using Ruby fibers.
|
37
38
|
- High-performance event reactor for handling I/O events and timers.
|
38
39
|
- Natural, sequential programming style that makes it easy to reason about
|
@@ -41,15 +42,29 @@ takes care of context-switching automatically whenever a blocking call like
|
|
41
42
|
coprocesses, supervisors, cancel scopes, throttling, resource pools etc.
|
42
43
|
- Code can use native networking classes and libraries, growing support for
|
43
44
|
third-party gems such as `pg` and `redis`.
|
45
|
+
- Use stdlib classes such as `TCPServer`, `TCPSocket` and
|
44
46
|
- HTTP 1 / HTTP 2 client agent with persistent connections.
|
45
47
|
- Competitive performance and scalability characteristics, in terms of both
|
46
48
|
throughput and memory consumption.
|
47
49
|
|
50
|
+
## Why you should not use Polyphony
|
51
|
+
|
52
|
+
- Polyphony does weird things to Ruby, like patching methods like `IO.read`,
|
53
|
+
`Kernel#sleep`, and `Timeout.timeout` so they'll work concurrently without
|
54
|
+
using threads.
|
55
|
+
- Error backtraces might look weird.
|
56
|
+
- There's currently no support for threads - any IO operations in threads will
|
57
|
+
likely cause a bad crash.
|
58
|
+
- Debugging might be confusing or not work at all.
|
59
|
+
- The API is currently unstable.
|
60
|
+
|
48
61
|
## Prior Art
|
49
62
|
|
50
63
|
Polyphony draws inspiration from the following, in no particular order:
|
51
64
|
|
52
65
|
* [nio4r](https://github.com/socketry/nio4r/) and [async](https://github.com/socketry/async)
|
66
|
+
(Polyphony's C-extension code is largely a spinoff of
|
67
|
+
[nio4r's](https://github.com/socketry/nio4r/tree/master/ext))
|
53
68
|
* [EventMachine](https://github.com/eventmachine/eventmachine)
|
54
69
|
* [Trio](https://trio.readthedocs.io/)
|
55
70
|
* [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually,
|
@@ -61,16 +76,18 @@ Polyphony draws inspiration from the following, in no particular order:
|
|
61
76
|
$ gem install polyphony
|
62
77
|
```
|
63
78
|
|
79
|
+
Or add it to your Gemfile, you know the drill.
|
80
|
+
|
64
81
|
## Getting Started
|
65
82
|
|
66
83
|
Polyphony is designed to help you write high-performance, concurrent code in
|
67
|
-
Ruby. It does so by turning every call which might block,
|
68
|
-
`read` into a concurrent operation, which yields
|
69
|
-
The reactor, in turn, may schedule other operations
|
70
|
-
that manner, multiple ongoing operations may be
|
84
|
+
Ruby, without using threads. It does so by turning every call which might block,
|
85
|
+
such as `Kernel#sleep` or `IO#read` into a concurrent operation, which yields
|
86
|
+
control to an event reactor. The reactor, in turn, may schedule other operations
|
87
|
+
once they can be resumed. In that manner, multiple ongoing operations may be
|
88
|
+
processed concurrently.
|
71
89
|
|
72
|
-
|
73
|
-
which is `Kernel#spin`:
|
90
|
+
The simplest way to start a concurrent operation is using `Kernel#spin`:
|
74
91
|
|
75
92
|
```ruby
|
76
93
|
require 'polyphony'
|
@@ -89,38 +106,38 @@ end
|
|
89
106
|
```
|
90
107
|
|
91
108
|
In the above example, both `sleep` calls will be executed concurrently, and thus
|
92
|
-
the program will take approximately only 1 second to execute. Note the
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
109
|
+
the program will take approximately only 1 second to execute. Note how the logic
|
110
|
+
flow inside each `spin` block is purely sequential, and how the concurrent
|
111
|
+
nature of the two blocks is expressed simply and cleanly.
|
112
|
+
|
113
|
+
## Coprocesses - Polyphony's basic unit of concurrency
|
114
|
+
|
115
|
+
In Polyphony, concurrent operations take place inside coprocesses. A `Coprocess`
|
116
|
+
is executed on top of a `Fiber`, which allows it to be suspended whenever a
|
117
|
+
blocking operation is called, and resumed once that operation has been
|
118
|
+
completed. Coprocesses offer significant advantages over threads - they consume
|
119
|
+
only about 10KB, switching between them is much faster than switching threads,
|
120
|
+
and literally millions of them can be spinned off without affecting
|
121
|
+
performance*. Besides, Ruby does not yet allow parallel execution of threads
|
122
|
+
(courtesy of the Ruby GVL).
|
123
|
+
|
124
|
+
\* *This is a totally unsubstantiated claim and has not been proven in practice*.
|
107
125
|
|
108
126
|
## An echo server in Polyphony
|
109
127
|
|
110
|
-
|
111
|
-
|
128
|
+
Let's now examine how networking is done using Polyphony. Here's a bare-bones
|
129
|
+
echo server written using Polyphony:
|
112
130
|
|
113
131
|
```ruby
|
114
132
|
require 'polyphony'
|
115
133
|
|
116
134
|
server = TCPServer.open(1234)
|
117
135
|
while client = server.accept
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
client.write(data)
|
136
|
+
spin do
|
137
|
+
while (data = client.gets)
|
138
|
+
client << data
|
122
139
|
end
|
123
|
-
|
140
|
+
end
|
124
141
|
end
|
125
142
|
```
|
126
143
|
|
@@ -132,12 +149,159 @@ This example demonstrates several features of Polyphony:
|
|
132
149
|
- The only hint of the code being concurrent is the use of `Kernel#spin`,
|
133
150
|
which starts a new coprocess on a dedicated fiber. This allows serving
|
134
151
|
multiple clients at once. Whenever a blocking call is issued, such as
|
135
|
-
`#accept` or `#read`, execution is *yielded* to the event loop, which
|
136
|
-
resume only those coprocesses which are ready to be resumed.
|
152
|
+
`#accept` or `#read`, execution is *yielded* to the event reactor loop, which
|
153
|
+
will resume only those coprocesses which are ready to be resumed.
|
137
154
|
- Exception handling is done using the normal Ruby constructs `raise`, `rescue`
|
138
155
|
and `ensure`. Exceptions never go unhandled (as might be the case with Ruby
|
139
|
-
threads), and must be dealt with explicitly. An unhandled exception will
|
140
|
-
the Ruby process to exit.
|
156
|
+
threads), and must be dealt with explicitly. An unhandled exception will by
|
157
|
+
default cause the Ruby process to exit.
|
158
|
+
|
159
|
+
## Additional concurrency constructs
|
160
|
+
|
161
|
+
In order to facilitate writing concurrent code, Polyphony provides additional
|
162
|
+
mechanisms that make it easier to create and control concurrent tasks.
|
163
|
+
|
164
|
+
### Cancel scopes
|
165
|
+
|
166
|
+
Cancel scopes, an idea borrowed from Python's
|
167
|
+
[Trio](https://trio.readthedocs.io/) library, are used to cancel the execution
|
168
|
+
of one or more coprocesses. The most common use of cancel scopes is a for
|
169
|
+
implementing a timeout for the completion of a task. Any blocking operation can
|
170
|
+
be cancelled. The programmer may choose to raise a `Cancel` exception when an
|
171
|
+
operation has been cancelled, or alternatively to move on without any exception.
|
172
|
+
|
173
|
+
Cancel scopes are typically started using `Kernel#cancel_after` and
|
174
|
+
`Kernel#move_on_after` for cancelling with or without an exception,
|
175
|
+
respectively. Cancel scopes will take a block of code to execute and run it,
|
176
|
+
providing a reference to the cancel scope:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
puts "going to sleep (but really only for 1 second)..."
|
180
|
+
cancel_after(1) do
|
181
|
+
sleep(60)
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
Patterns like closing a connection after X seconds of activity are greatly
|
186
|
+
facilitated by timeout-based cancel scopes, which can be easily reset:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
def echoer(client)
|
190
|
+
# close connection after 10 seconds of inactivity
|
191
|
+
move_on_after(10) do |scope|
|
192
|
+
scope.when_cancelled { puts "closing connection due to inactivity" }
|
193
|
+
loop do
|
194
|
+
data = client.read
|
195
|
+
scope.reset_timeout
|
196
|
+
client.write
|
197
|
+
end
|
198
|
+
end
|
199
|
+
client.close
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
Cancel scopes may also be manually cancelled by calling `CancelScope#cancel!`
|
204
|
+
at any time:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
def echoer(client)
|
208
|
+
move_on_after(60) do |scope|
|
209
|
+
loop do
|
210
|
+
data = client.read
|
211
|
+
scope.cancel! if data == 'stop'
|
212
|
+
client.write
|
213
|
+
end
|
214
|
+
end
|
215
|
+
client.close
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
### Resource pools
|
220
|
+
|
221
|
+
A resource pool is used to control access to one or more shared, usually
|
222
|
+
identical resources. For example, a resource pool can be used to control
|
223
|
+
concurrent access to database connections, or to limit concurrent
|
224
|
+
requests to an external API:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
# up to 5 concurrent connections
|
228
|
+
Pool = Polyphony::ResourcePool.new(limit: 5) {
|
229
|
+
# the block sets up the resource
|
230
|
+
PG.connect(...)
|
231
|
+
}
|
232
|
+
|
233
|
+
1000.times {
|
234
|
+
spin {
|
235
|
+
Pool.acquire { |db| p db.query('select 1') }
|
236
|
+
}
|
237
|
+
}
|
238
|
+
```
|
239
|
+
|
240
|
+
You can also call arbitrary methods on the resource pool, which will be
|
241
|
+
delegated to the resource using `#method_missing`:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
# up to 5 concurrent connections
|
245
|
+
Pool = Polyphony::ResourcePool.new(limit: 5) {
|
246
|
+
# the block sets up the resource
|
247
|
+
PG.connect(...)
|
248
|
+
}
|
249
|
+
|
250
|
+
1000.times {
|
251
|
+
spin { p Pool.query('select pg_sleep(0.01);') }
|
252
|
+
}
|
253
|
+
```
|
254
|
+
|
255
|
+
### Supervisors
|
256
|
+
|
257
|
+
A supervisor is used to control one or more coprocesses. It can be used to
|
258
|
+
start, stop, restart and await the completion of multiple coprocesses. It is
|
259
|
+
normally started using `Kernel#supervise`:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
supervise { |s|
|
263
|
+
s.spin { sleep 1 }
|
264
|
+
s.spin { sleep 2 }
|
265
|
+
s.spin { sleep 3 }
|
266
|
+
}
|
267
|
+
puts "done sleeping"
|
268
|
+
```
|
269
|
+
|
270
|
+
The `Kernel#supervise` method will await the completion of all supervised
|
271
|
+
coprocesses. If any supervised coprocess raises an error, the supervisor will
|
272
|
+
automatically cancel all other supervised coprocesses.
|
273
|
+
|
274
|
+
### Throttlers
|
275
|
+
|
276
|
+
A throttler is a mechanism for controlling the speed of an arbitrary task,
|
277
|
+
such as sending of emails, or crawling a website. A throttler is normally
|
278
|
+
created using `Kernel#throttle` or `Kernel#throttled_loop`, and can even be used
|
279
|
+
to throttle operations across multiple coprocesses:
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
server = Polyphony::Net.tcp_listen(1234)
|
283
|
+
|
284
|
+
# a shared throttler, up to 10 times per second
|
285
|
+
throttler = throttle(rate: 10)
|
286
|
+
|
287
|
+
while client = server.accept
|
288
|
+
spin do
|
289
|
+
throttler.call do
|
290
|
+
while data = client.read
|
291
|
+
client.write(data)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
`Kernel#throttled_loop` can be used to run throttled infinite loops:
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
throttled_loop(3) do
|
302
|
+
STDOUT << '.'
|
303
|
+
end
|
304
|
+
```
|
141
305
|
|
142
306
|
## Going further
|
143
307
|
|
@@ -202,7 +366,7 @@ class IOWatcher
|
|
202
366
|
end
|
203
367
|
```
|
204
368
|
|
205
|
-
> **Running a high-performance event loop**: Polyphony
|
369
|
+
> **Running a high-performance event loop**: Polyphony runs a libev-based event
|
206
370
|
> loop that watches events such as IO-readiness, elapsed timers, received
|
207
371
|
> signals and other asynchronous happenings, and uses them to control fiber
|
208
372
|
> execution. The event loop itself is run on a separate fiber, allowing the main
|
@@ -220,109 +384,6 @@ class IOWatcher
|
|
220
384
|
end
|
221
385
|
```
|
222
386
|
|
223
|
-
### Additional concurrency constructs
|
224
|
-
|
225
|
-
In order to facilitate writing concurrent code, Polyphony provides additional
|
226
|
-
constructs that make it easier to create and control concurrent tasks.
|
227
|
-
|
228
|
-
`CancelScope` - an abstraction used to cancel the execution of one or more
|
229
|
-
coprocesses or supervisors. It usually works by defining a timeout for the
|
230
|
-
completion of a task. Any blocking operation can be cancelled, including
|
231
|
-
a coprocess or a supervisor. The developer may choose to cancel with or without
|
232
|
-
an exception with `cancel` or `move_on`, respectively. Cancel scopes are
|
233
|
-
typically started using `Kernel.cancel_after` and `Kernel.move_on`:
|
234
|
-
|
235
|
-
```ruby
|
236
|
-
def echoer(client)
|
237
|
-
# cancel after 10 seconds if inactivity
|
238
|
-
move_on_after(10) { |scope|
|
239
|
-
loop {
|
240
|
-
data = client.read
|
241
|
-
scope.reset_timeout
|
242
|
-
client.write
|
243
|
-
}
|
244
|
-
}
|
245
|
-
}
|
246
|
-
```
|
247
|
-
|
248
|
-
`ResourcePool` - a class used to control access to shared resources. It can be
|
249
|
-
used to control concurrent access to database connections, or to limit
|
250
|
-
concurrent requests to an external API:
|
251
|
-
|
252
|
-
```ruby
|
253
|
-
# up to 5 concurrent connections
|
254
|
-
Pool = Polyphony::ResourcePool.new(limit: 5) {
|
255
|
-
# the block sets up the resource
|
256
|
-
PG.connect(...)
|
257
|
-
}
|
258
|
-
|
259
|
-
1000.times {
|
260
|
-
spin {
|
261
|
-
Pool.acquire { |db| p db.query('select 1') }
|
262
|
-
}
|
263
|
-
}
|
264
|
-
```
|
265
|
-
|
266
|
-
You can also call arbitrary methods on the resource pool, which will be
|
267
|
-
delegated to the resource using `#method_missing`:
|
268
|
-
|
269
|
-
```ruby
|
270
|
-
# up to 5 concurrent connections
|
271
|
-
Pool = Polyphony::ResourcePool.new(limit: 5) {
|
272
|
-
# the block sets up the resource
|
273
|
-
PG.connect(...)
|
274
|
-
}
|
275
|
-
|
276
|
-
1000.times {
|
277
|
-
spin { p Pool.query('select 1') }
|
278
|
-
}
|
279
|
-
```
|
280
|
-
|
281
|
-
`Supervisor` - a class used to control one or more `Coprocess`s. It can be used
|
282
|
-
to start, stop and restart multiple coprocesses. A supervisor can also be
|
283
|
-
used for awaiting the completion of multiple coprocesses. It is usually started
|
284
|
-
using `Kernel.supervise`:
|
285
|
-
|
286
|
-
```ruby
|
287
|
-
supervise { |s|
|
288
|
-
s.spin { sleep 1 }
|
289
|
-
s.spin { sleep 2 }
|
290
|
-
s.spin { sleep 3 }
|
291
|
-
}
|
292
|
-
puts "done sleeping"
|
293
|
-
```
|
294
|
-
|
295
|
-
`ThreadPool` - a pool of threads used to run any operation that cannot be
|
296
|
-
implemented using non-blocking calls, such as file system calls. The operation
|
297
|
-
is offloaded to a worker thread, allowing the event loop to continue processing
|
298
|
-
other tasks. For example, `IO.read` and `File.stat` are both reimplemented
|
299
|
-
using the Polyphony thread pool. You can easily use the thread pool to run your
|
300
|
-
own blocking operations as follows:
|
301
|
-
|
302
|
-
```ruby
|
303
|
-
result = Polyphony::ThreadPool.process { long_running_process }
|
304
|
-
```
|
305
|
-
|
306
|
-
`Throttler` - a mechanism for throttling an arbitrary task, such as sending of
|
307
|
-
emails, or crawling a website. A throttler is normally created using
|
308
|
-
`Kernel.throttle`, and can even be used to throttle operations across multiple
|
309
|
-
coprocesses:
|
310
|
-
|
311
|
-
```ruby
|
312
|
-
server = Net.tcp_listen(1234)
|
313
|
-
throttler = throttle(rate: 10) # up to 10 times per second
|
314
|
-
|
315
|
-
while client = server.accept
|
316
|
-
spin {
|
317
|
-
throttler.call {
|
318
|
-
while data = client.read
|
319
|
-
client.write(data)
|
320
|
-
end
|
321
|
-
}
|
322
|
-
}
|
323
|
-
end
|
324
|
-
```
|
325
|
-
|
326
387
|
## API Reference
|
327
388
|
|
328
389
|
To be continued...
|
@@ -364,7 +425,7 @@ following:
|
|
364
425
|
|
365
426
|
```ruby
|
366
427
|
require 'http/parser'
|
367
|
-
require '
|
428
|
+
require 'polyphony'
|
368
429
|
|
369
430
|
def handle_client(client)
|
370
431
|
parser = Http::Parser.new
|