polyphony 0.19 → 0.20
Sign up to get free protection for your applications and to get access to all the features.
- 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
|