polyphony 0.19 → 0.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +87 -1
  4. data/CHANGELOG.md +35 -0
  5. data/Gemfile.lock +17 -6
  6. data/README.md +200 -139
  7. data/Rakefile +4 -4
  8. data/TODO.md +35 -7
  9. data/bin/poly +11 -0
  10. data/docs/getting-started/getting-started.md +1 -1
  11. data/docs/summary.md +3 -0
  12. data/docs/technical-overview/exception-handling.md +94 -0
  13. data/docs/technical-overview/fiber-scheduling.md +99 -0
  14. data/examples/core/cancel.rb +8 -4
  15. data/examples/core/channel_echo.rb +18 -17
  16. data/examples/core/defer.rb +12 -0
  17. data/examples/core/enumerator.rb +4 -4
  18. data/examples/core/fiber_error.rb +9 -0
  19. data/examples/core/fiber_error_with_backtrace.rb +73 -0
  20. data/examples/core/fork.rb +6 -6
  21. data/examples/core/genserver.rb +16 -8
  22. data/examples/core/lock.rb +3 -3
  23. data/examples/core/move_on.rb +4 -3
  24. data/examples/core/move_on_twice.rb +5 -5
  25. data/examples/core/move_on_with_ensure.rb +8 -11
  26. data/examples/core/move_on_with_value.rb +14 -0
  27. data/examples/core/{multiple_spawn.rb → multiple_spin.rb} +5 -5
  28. data/examples/core/nested_cancel.rb +5 -5
  29. data/examples/core/{nested_multiple_spawn.rb → nested_multiple_spin.rb} +6 -6
  30. data/examples/core/nested_spin.rb +17 -0
  31. data/examples/core/pingpong.rb +21 -0
  32. data/examples/core/pulse.rb +4 -5
  33. data/examples/core/resource.rb +6 -4
  34. data/examples/core/resource_cancel.rb +6 -9
  35. data/examples/core/resource_delegate.rb +3 -3
  36. data/examples/core/sleep.rb +3 -3
  37. data/examples/core/sleep_spin.rb +19 -0
  38. data/examples/core/snooze.rb +32 -0
  39. data/examples/core/spin.rb +14 -0
  40. data/examples/core/{spawn_cancel.rb → spin_cancel.rb} +6 -7
  41. data/examples/core/spin_error.rb +17 -0
  42. data/examples/core/spin_error_backtrace.rb +30 -0
  43. data/examples/core/spin_uncaught_error.rb +15 -0
  44. data/examples/core/supervisor.rb +8 -8
  45. data/examples/core/supervisor_with_cancel_scope.rb +7 -7
  46. data/examples/core/supervisor_with_error.rb +8 -8
  47. data/examples/core/supervisor_with_manual_move_on.rb +6 -7
  48. data/examples/core/suspend.rb +13 -0
  49. data/examples/core/thread.rb +1 -1
  50. data/examples/core/thread_cancel.rb +9 -11
  51. data/examples/core/thread_pool.rb +18 -14
  52. data/examples/core/throttle.rb +7 -7
  53. data/examples/core/timeout.rb +3 -3
  54. data/examples/fs/read.rb +7 -9
  55. data/examples/http/config.ru +7 -3
  56. data/examples/http/cuba.ru +22 -0
  57. data/examples/http/happy_eyeballs.rb +6 -4
  58. data/examples/http/http_client.rb +1 -1
  59. data/examples/http/http_get.rb +1 -1
  60. data/examples/http/http_parse_experiment.rb +21 -16
  61. data/examples/http/http_proxy.rb +28 -26
  62. data/examples/http/http_server.rb +10 -10
  63. data/examples/http/http_server_forked.rb +6 -5
  64. data/examples/http/http_server_throttled.rb +3 -3
  65. data/examples/http/http_ws_server.rb +11 -11
  66. data/examples/http/https_raw_client.rb +1 -1
  67. data/examples/http/https_server.rb +8 -8
  68. data/examples/http/https_wss_server.rb +13 -11
  69. data/examples/http/rack_server.rb +2 -2
  70. data/examples/http/rack_server_https.rb +4 -4
  71. data/examples/http/rack_server_https_forked.rb +5 -5
  72. data/examples/http/websocket_secure_server.rb +6 -6
  73. data/examples/http/websocket_server.rb +5 -5
  74. data/examples/interfaces/pg_client.rb +4 -4
  75. data/examples/interfaces/pg_pool.rb +13 -6
  76. data/examples/interfaces/pg_transaction.rb +5 -4
  77. data/examples/interfaces/redis_channels.rb +15 -11
  78. data/examples/interfaces/redis_client.rb +2 -2
  79. data/examples/interfaces/redis_pubsub.rb +2 -1
  80. data/examples/interfaces/redis_pubsub_perf.rb +13 -9
  81. data/examples/io/backticks.rb +11 -0
  82. data/examples/io/cat.rb +4 -5
  83. data/examples/io/echo_client.rb +9 -4
  84. data/examples/io/echo_client_from_stdin.rb +20 -0
  85. data/examples/io/echo_pipe.rb +7 -8
  86. data/examples/io/echo_server.rb +8 -6
  87. data/examples/io/echo_server_with_timeout.rb +13 -10
  88. data/examples/io/echo_stdin.rb +3 -3
  89. data/examples/io/httparty.rb +2 -2
  90. data/examples/io/httparty_multi.rb +8 -4
  91. data/examples/io/httparty_threaded.rb +6 -2
  92. data/examples/io/io_read.rb +2 -2
  93. data/examples/io/irb.rb +16 -4
  94. data/examples/io/net-http.rb +3 -3
  95. data/examples/io/open.rb +17 -0
  96. data/examples/io/system.rb +3 -3
  97. data/examples/io/tcpserver.rb +15 -0
  98. data/examples/io/tcpsocket.rb +6 -5
  99. data/examples/performance/multi_snooze.rb +29 -0
  100. data/examples/performance/{perf_snooze.rb → snooze.rb} +7 -5
  101. data/examples/performance/snooze_raw.rb +39 -0
  102. data/ext/gyro/async.c +165 -0
  103. data/ext/gyro/child.c +167 -0
  104. data/ext/{ev → gyro}/extconf.rb +4 -3
  105. data/ext/gyro/gyro.c +316 -0
  106. data/ext/{ev/ev.h → gyro/gyro.h} +12 -7
  107. data/ext/gyro/gyro_ext.c +23 -0
  108. data/ext/{ev → gyro}/io.c +65 -57
  109. data/ext/{ev → gyro}/libev.h +0 -0
  110. data/ext/gyro/signal.c +117 -0
  111. data/ext/{ev → gyro}/socket.c +61 -6
  112. data/ext/gyro/timer.c +199 -0
  113. data/ext/libev/Changes +35 -0
  114. data/ext/libev/README +2 -1
  115. data/ext/libev/ev.c +213 -151
  116. data/ext/libev/ev.h +95 -88
  117. data/ext/libev/ev_epoll.c +26 -15
  118. data/ext/libev/ev_kqueue.c +11 -5
  119. data/ext/libev/ev_linuxaio.c +642 -0
  120. data/ext/libev/ev_poll.c +13 -8
  121. data/ext/libev/ev_port.c +5 -2
  122. data/ext/libev/ev_vars.h +14 -3
  123. data/ext/libev/ev_wrap.h +16 -0
  124. data/lib/ev_ext.bundle +0 -0
  125. data/lib/polyphony.rb +46 -50
  126. data/lib/polyphony/auto_run.rb +12 -0
  127. data/lib/polyphony/core/cancel_scope.rb +11 -7
  128. data/lib/polyphony/core/channel.rb +16 -9
  129. data/lib/polyphony/core/coprocess.rb +101 -51
  130. data/lib/polyphony/core/exceptions.rb +14 -12
  131. data/lib/polyphony/core/resource_pool.rb +21 -8
  132. data/lib/polyphony/core/supervisor.rb +10 -5
  133. data/lib/polyphony/core/sync.rb +7 -6
  134. data/lib/polyphony/core/thread.rb +4 -4
  135. data/lib/polyphony/core/thread_pool.rb +4 -4
  136. data/lib/polyphony/core/throttler.rb +6 -4
  137. data/lib/polyphony/extensions/core.rb +253 -0
  138. data/lib/polyphony/extensions/io.rb +28 -16
  139. data/lib/polyphony/extensions/openssl.rb +2 -1
  140. data/lib/polyphony/extensions/socket.rb +47 -52
  141. data/lib/polyphony/http.rb +4 -3
  142. data/lib/polyphony/http/agent.rb +68 -57
  143. data/lib/polyphony/http/server.rb +5 -5
  144. data/lib/polyphony/http/server/http1.rb +268 -0
  145. data/lib/polyphony/http/server/http2.rb +62 -0
  146. data/lib/polyphony/http/server/http2_stream.rb +104 -0
  147. data/lib/polyphony/http/server/rack.rb +64 -0
  148. data/lib/polyphony/http/server/request.rb +119 -0
  149. data/lib/polyphony/net.rb +26 -15
  150. data/lib/polyphony/postgres.rb +17 -13
  151. data/lib/polyphony/redis.rb +16 -15
  152. data/lib/polyphony/version.rb +1 -1
  153. data/lib/polyphony/websocket.rb +11 -4
  154. data/polyphony.gemspec +13 -9
  155. data/test/eg.rb +27 -0
  156. data/test/helper.rb +25 -0
  157. data/test/run.rb +5 -0
  158. data/test/test_async.rb +33 -0
  159. data/test/test_coprocess.rb +239 -77
  160. data/test/test_core.rb +95 -61
  161. data/test/test_gyro.rb +148 -0
  162. data/test/test_http_server.rb +313 -0
  163. data/test/test_io.rb +79 -27
  164. data/test/test_kernel.rb +22 -12
  165. data/test/test_signal.rb +36 -0
  166. data/test/test_timer.rb +24 -0
  167. metadata +89 -33
  168. data/examples/core/nested_async.rb +0 -17
  169. data/examples/core/next_tick.rb +0 -12
  170. data/examples/core/sleep_spawn.rb +0 -19
  171. data/examples/core/spawn.rb +0 -14
  172. data/examples/core/spawn_error.rb +0 -28
  173. data/examples/performance/perf_multi_snooze.rb +0 -21
  174. data/ext/ev/async.c +0 -168
  175. data/ext/ev/child.c +0 -169
  176. data/ext/ev/ev_ext.c +0 -23
  177. data/ext/ev/ev_module.c +0 -242
  178. data/ext/ev/signal.c +0 -119
  179. data/ext/ev/timer.c +0 -197
  180. data/lib/polyphony/core/fiber_pool.rb +0 -98
  181. data/lib/polyphony/extensions/kernel.rb +0 -169
  182. data/lib/polyphony/http/http1_adapter.rb +0 -254
  183. data/lib/polyphony/http/http2_adapter.rb +0 -157
  184. data/lib/polyphony/http/rack.rb +0 -25
  185. data/lib/polyphony/http/request.rb +0 -66
  186. data/test/test_ev.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e79ca0a896fc0b5feffa415e44b9229074d145c2142155caf47f243b6c85078d
4
- data.tar.gz: 6aa58f404376be6e6d472e588be4349d6fcaeb3031e5786a0a5ffe0970d9fa5d
3
+ metadata.gz: ca0e1fc13842ec06d7272fb19e0edc1621eddb4a007ff9b97e07794e60a9d814
4
+ data.tar.gz: 78cab004907a41474ff1920cbb9f5aceb6851797e6805f9c16256f3d48457eff
5
5
  SHA512:
6
- metadata.gz: accbeddc33fd7b1de40f411a421c445bc847e8ee744d98dcd1cf20b0a8fe3fc5c880ca373a020b026a0d68aac9cfc74f179bde2c3cebd1b0733a5adbe2b435b0
7
- data.tar.gz: 7549c7ac4528bf4747049fb6c4eecbfe500f6aeed3586f8181bbf21506f8725ebb51c1170e369f15b24c748d59be439a2d076d26de1e5b1a1b6b34ae3e344ade
6
+ metadata.gz: 96de0a203333388768a41d8c81c333e80b9ab20ebf001b744e1a5d0dd8e6bb036ebc5b4d4abe7a85b4016e264afd4f3609cc8aeed9c8bbfc401010966c8090f2
7
+ data.tar.gz: 30507005291c431dd277d30829f6e4c219d2b176a41eef111caadcc8975adaeb335dc4104dc08cb65c58649863bd7a39d21565adee55a795c58d84894419026d
data/.gitignore CHANGED
@@ -52,4 +52,4 @@ build-iPhoneSimulator/
52
52
  test.rb
53
53
  .vscode
54
54
 
55
- lib/ev_ext.bundle
55
+ lib/gyro_ext.bundle
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.19)
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.2.2)
22
+ mime-types (3.3)
20
23
  mime-types-data (~> 3.2015)
21
- mime-types-data (3.2019.0331)
24
+ mime-types-data (3.2019.1009)
22
25
  minitest (5.11.3)
23
- modulation (0.25)
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
- rake (12.3.2)
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.17.2
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.5 and newer) and supports Linux and MacOS only.
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 and automatic ALPN protocol selection**.
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, such as `sleep` or
68
- `read` into a concurrent operation, which yields control to an event reactor.
69
- The reactor, in turn, may schedule other operations once they can be resumed. In
70
- that manner, multiple ongoing operations may be processed concurrently.
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
- There are multiple ways to start a concurrent operation, the most common of
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 lack of
93
- any boilerplate relating to concurrency. Each `spin` block starts a
94
- *coprocess*, and is executed in sequential manner.
95
-
96
- > **Coprocesses - the basic unit of concurrency**: In Polyphony, concurrent
97
- > operations take place inside coprocesses. A `Coprocess` is executed on top of
98
- > a `Fiber`, which allows it to be suspended whenever a blocking operation is
99
- > called, and resumed once that operation has been completed. Coprocesses offer
100
- > significant advantages over threads - they consume only about 10KB, switching
101
- > between them is much faster than switching threads, and literally millions of
102
- > them can be spinned off without affecting performance*. Besides, Ruby does not
103
- > yet allow parallel execution of threads (courtesy of the Ruby GVL).
104
- >
105
- > \* *This is a totally unsubstantiated claim which has not been proved in
106
- > practice*.
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
- To take matters further, let's see how networking can be done using Polyphony.
111
- Here's a bare-bones echo server written using Polyphony:
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
- # coproc starts a new coprocess on a separate fiber
119
- spin {
120
- while data = client.read rescue nil
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 will
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 cause
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 runs a libev-based event
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 'modulation'
428
+ require 'polyphony'
368
429
 
369
430
  def handle_client(client)
370
431
  parser = Http::Parser.new