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