iodine 0.7.60 → 0.8.0

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +15 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -3
  5. data/CHANGELOG.md +132 -12
  6. data/Gemfile +5 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +147 -671
  9. data/Rakefile +9 -16
  10. data/examples/chatroom.nru +9 -0
  11. data/examples/client +74 -0
  12. data/examples/config-hello.nru +9 -0
  13. data/examples/config-hello.ru +11 -0
  14. data/examples/config-hijack.ru +13 -0
  15. data/examples/config-shootout.ru +43 -0
  16. data/examples/config.nru +120 -0
  17. data/examples/config.rb +37 -0
  18. data/examples/config.ru +42 -53
  19. data/examples/fake +24 -0
  20. data/examples/hello_neorack +137 -0
  21. data/examples/routes.nru +29 -0
  22. data/examples/routes.ru +42 -0
  23. data/exe/iodine +218 -258
  24. data/ext/iodine/extconf.rb +27 -100
  25. data/ext/iodine/fio-stl.h +128336 -0
  26. data/ext/iodine/fio-stl.md +26396 -0
  27. data/ext/iodine/iodine.c +213 -1393
  28. data/ext/iodine/iodine.h +250 -55
  29. data/ext/iodine/iodine_arg_helper.h +329 -0
  30. data/ext/iodine/iodine_caller.h +234 -25
  31. data/ext/iodine/iodine_cli.h +434 -0
  32. data/ext/iodine/iodine_compression.h +312 -0
  33. data/ext/iodine/iodine_connection.h +3484 -34
  34. data/ext/iodine/iodine_core.h +363 -0
  35. data/ext/iodine/iodine_crypto.h +1441 -0
  36. data/ext/iodine/iodine_defer.h +225 -4
  37. data/ext/iodine/iodine_json.h +738 -4
  38. data/ext/iodine/iodine_listener.h +263 -0
  39. data/ext/iodine/iodine_log.h +297 -0
  40. data/ext/iodine/iodine_minimap.h +543 -0
  41. data/ext/iodine/iodine_mustache.h +461 -4
  42. data/ext/iodine/iodine_pubsub_eng.h +729 -0
  43. data/ext/iodine/iodine_pubsub_history.h +547 -0
  44. data/ext/iodine/iodine_pubsub_msg.h +228 -0
  45. data/ext/iodine/iodine_redis.h +322 -0
  46. data/ext/iodine/iodine_store.h +418 -16
  47. data/ext/iodine/iodine_threads.h +279 -0
  48. data/ext/iodine/iodine_tls.h +515 -6
  49. data/ext/iodine/iodine_utils.h +923 -0
  50. data/iodine.gemspec +37 -42
  51. data/lib/iodine/benchmark.rb +1690 -0
  52. data/lib/iodine/documentation.rb +3085 -0
  53. data/lib/iodine/version.rb +17 -1
  54. data/lib/iodine.rb +54 -255
  55. data/logo.svg +36 -0
  56. data/mustache.md +55 -0
  57. data/sig/iodine.rbs +4 -0
  58. metadata +58 -184
  59. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  60. data/.github/workflows/ruby.yml +0 -42
  61. data/.gitignore +0 -21
  62. data/LIMITS.md +0 -41
  63. data/SPEC-PubSub-Draft.md +0 -159
  64. data/SPEC-WebSocket-Draft.md +0 -239
  65. data/bin/console +0 -22
  66. data/bin/info.md +0 -353
  67. data/bin/mustache_bench.rb +0 -100
  68. data/bin/poc/Gemfile.lock +0 -23
  69. data/bin/poc/README.md +0 -37
  70. data/bin/poc/config.ru +0 -66
  71. data/bin/poc/gemfile +0 -1
  72. data/bin/poc/www/index.html +0 -57
  73. data/examples/async_task.ru +0 -92
  74. data/examples/bates/README.md +0 -3
  75. data/examples/bates/config.ru +0 -342
  76. data/examples/bates/david+bold.pdf +0 -0
  77. data/examples/bates/public/drop-pdf.png +0 -0
  78. data/examples/bates/public/index.html +0 -600
  79. data/examples/echo.ru +0 -59
  80. data/examples/etag.ru +0 -16
  81. data/examples/hello.ru +0 -29
  82. data/examples/pubsub_engine.ru +0 -81
  83. data/examples/rack3.ru +0 -12
  84. data/examples/redis.ru +0 -70
  85. data/examples/shootout.ru +0 -73
  86. data/examples/sub-protocols.ru +0 -90
  87. data/examples/tcp_client.rb +0 -66
  88. data/examples/x-sendfile.ru +0 -14
  89. data/ext/iodine/fio.c +0 -11995
  90. data/ext/iodine/fio.h +0 -6371
  91. data/ext/iodine/fio_cli.c +0 -431
  92. data/ext/iodine/fio_cli.h +0 -189
  93. data/ext/iodine/fio_json_parser.h +0 -688
  94. data/ext/iodine/fio_siphash.c +0 -157
  95. data/ext/iodine/fio_siphash.h +0 -37
  96. data/ext/iodine/fio_tls.h +0 -129
  97. data/ext/iodine/fio_tls_missing.c +0 -649
  98. data/ext/iodine/fio_tls_openssl.c +0 -1106
  99. data/ext/iodine/fio_tmpfile.h +0 -50
  100. data/ext/iodine/fiobj.h +0 -44
  101. data/ext/iodine/fiobj4fio.h +0 -21
  102. data/ext/iodine/fiobj_ary.c +0 -333
  103. data/ext/iodine/fiobj_ary.h +0 -139
  104. data/ext/iodine/fiobj_data.c +0 -1187
  105. data/ext/iodine/fiobj_data.h +0 -167
  106. data/ext/iodine/fiobj_hash.c +0 -409
  107. data/ext/iodine/fiobj_hash.h +0 -176
  108. data/ext/iodine/fiobj_json.c +0 -622
  109. data/ext/iodine/fiobj_json.h +0 -68
  110. data/ext/iodine/fiobj_mem.h +0 -71
  111. data/ext/iodine/fiobj_mustache.c +0 -317
  112. data/ext/iodine/fiobj_mustache.h +0 -62
  113. data/ext/iodine/fiobj_numbers.c +0 -344
  114. data/ext/iodine/fiobj_numbers.h +0 -127
  115. data/ext/iodine/fiobj_str.c +0 -433
  116. data/ext/iodine/fiobj_str.h +0 -172
  117. data/ext/iodine/fiobject.c +0 -620
  118. data/ext/iodine/fiobject.h +0 -655
  119. data/ext/iodine/hpack.h +0 -1923
  120. data/ext/iodine/http.c +0 -2743
  121. data/ext/iodine/http.h +0 -1002
  122. data/ext/iodine/http1.c +0 -910
  123. data/ext/iodine/http1.h +0 -29
  124. data/ext/iodine/http1_parser.h +0 -878
  125. data/ext/iodine/http_internal.c +0 -1279
  126. data/ext/iodine/http_internal.h +0 -237
  127. data/ext/iodine/http_mime_parser.h +0 -350
  128. data/ext/iodine/iodine_caller.c +0 -230
  129. data/ext/iodine/iodine_connection.c +0 -941
  130. data/ext/iodine/iodine_defer.c +0 -420
  131. data/ext/iodine/iodine_fiobj2rb.h +0 -120
  132. data/ext/iodine/iodine_helpers.c +0 -282
  133. data/ext/iodine/iodine_helpers.h +0 -12
  134. data/ext/iodine/iodine_http.c +0 -1181
  135. data/ext/iodine/iodine_http.h +0 -23
  136. data/ext/iodine/iodine_json.c +0 -302
  137. data/ext/iodine/iodine_mustache.c +0 -567
  138. data/ext/iodine/iodine_pubsub.c +0 -580
  139. data/ext/iodine/iodine_pubsub.h +0 -26
  140. data/ext/iodine/iodine_rack_io.c +0 -273
  141. data/ext/iodine/iodine_rack_io.h +0 -20
  142. data/ext/iodine/iodine_store.c +0 -148
  143. data/ext/iodine/iodine_tcp.c +0 -346
  144. data/ext/iodine/iodine_tcp.h +0 -13
  145. data/ext/iodine/iodine_tls.c +0 -261
  146. data/ext/iodine/mustache_parser.h +0 -1546
  147. data/ext/iodine/redis_engine.c +0 -957
  148. data/ext/iodine/redis_engine.h +0 -79
  149. data/ext/iodine/resp_parser.h +0 -317
  150. data/ext/iodine/websocket_parser.h +0 -506
  151. data/ext/iodine/websockets.c +0 -743
  152. data/ext/iodine/websockets.h +0 -185
  153. data/lib/iodine/connection.rb +0 -61
  154. data/lib/iodine/json.rb +0 -42
  155. data/lib/iodine/mustache.rb +0 -113
  156. data/lib/iodine/pubsub.rb +0 -55
  157. data/lib/iodine/rack_utils.rb +0 -43
  158. data/lib/iodine/tls.rb +0 -16
  159. data/lib/rack/handler/iodine.rb +0 -33
  160. data/logo.png +0 -0
@@ -1,239 +0,0 @@
1
- ### Draft State
2
-
3
- This draft is also implemented by [the Agoo server](https://github.com/ohler55/agoo) according to the specifications stated in [Rack PR#1272](https://github.com/rack/rack/pull/1272).
4
-
5
- ---
6
- ## Purpose
7
-
8
- This document details a Rack specification extension for WebSocket / EventSource servers.
9
-
10
- The purpose of this specification is:
11
-
12
- 1. To improve application safety by phasing out the use of `hijack` and replacing it with the use of application object callbacks.
13
-
14
- This should make it easer for applications to accept WebSocket and EventSource (SSE) connections without exposing themselves to risks and errors related to IO / network logic (such as slow client attacks, buffer flooding, etc').
15
-
16
- 2. To improve separation of concerns between servers and applications, moving the IO / network logic related to WebSocket and EventSource (SSE) back to the server.
17
-
18
- Simply put, when using a server that support this extension, the application / framework doesn’t need to have any knowledge about networking, transport protocols, IO streams, polling, etc'.
19
-
20
- ## Rack WebSockets / EventSource
21
-
22
- Servers that publish WebSocket and/or EventSource (SSE) support using the `env['rack.upgrade?']` value **MUST** follow the requirements set in this document.
23
-
24
- This document reserves the Rack `env` Hash keys of `rack.upgrade?` and `rack.upgrade`.
25
-
26
- A conforming server **MUST** set `env['rack.upgrade?']` to `:websocket` for incoming WebSocket connections and `:sse` for incoming EventSource (SSE) connections.
27
-
28
- If a connection is not "upgradeable", a conforming server **SHOULD** set `env['rack.upgrade?']` to either `nil` or `false`. Setting the `env['rack.upgrade?']` to either `false` or `nil` should make it easier for applications to test for server support during a normal HTTP request.
29
-
30
- If the connection is upgradeable and a client application set a value for `env['rack.upgrade']`:
31
-
32
- * the server **MUST** use that value as a WebSocket / EventSource Callback Object unless the response status code `>= 300` (redirection / error status code).
33
-
34
- * The response `body` **MUST NOT** be sent when switching to a Callback Object.
35
-
36
- If a connection is **NOT** upgradeable and a client application set a value for `env['rack.upgrade']`:
37
-
38
- * The server **SHOULD** ignore the Callback Object and process the response as if it did not exist.
39
-
40
- * A server **MAY** use the Callback Object to allow a client to "hijack" the data stream as raw data stream. Such behavior **MUST** be documented.
41
-
42
- ### The WebSocket / EventSource Callback Object
43
-
44
- WebSocket and EventSource connection upgrade and handling is performed using a Callback Object.
45
-
46
- The Callback Object could be a any object which implements any (of none) of the following callbacks:
47
-
48
- * `on_open(client)` **MUST** be called once the connection had been established and/or the Callback Object had been linked to the `client` object.
49
-
50
- * `on_message(client, data)` **MUST** be called when incoming WebSocket data is received.
51
-
52
- This callback is ignored for EventSource connections.
53
-
54
- `data` **MUST** be a String with an encoding of UTF-8 for text messages and `binary` encoding for non-text messages (as specified by the WebSocket Protocol).
55
-
56
- The *callback object* **MUST** assume that the `data` String will be a **recyclable buffer** and that it's content will be corrupted the moment the `on_message` callback returns.
57
-
58
- Servers **MAY**, optionally, implement a **recyclable buffer** for the `on_message` callback. However, this is optional, is *not* required and might result in issues in cases where the client code is less than pristine.
59
-
60
- * `on_drained(client)` **MAY** be called when the `client.write` buffer becomes empty. **If** `client.pending` ever returns a non-zero value (see later on), the `on_drained` callback **MUST** be called once the write buffer becomes empty.
61
-
62
- * `on_shutdown(client)` **MAY** be called during the server's graceful shutdown process, _before_ the connection is closed and in addition to the `on_close` function (which is called _after_ the connection is closed.
63
-
64
- * `on_close(client)` **MUST** be called _after_ the connection was closed for whatever reason (socket errors, parsing errors, timeouts, client disconnection, `client.close` being called, etc') or the Callback Object was replaced by another Callback Object.
65
-
66
-
67
- The server **MUST** provide the Callback Object with a `client` object, that supports the following methods (this approach promises applications could be server agnostic):
68
-
69
- * `env` **MUST** return the Rack `env` hash related to the originating HTTP request. Some changes to the `env` hash (such as removal of the IO hijacking support) **MAY** be implemented by the server.
70
-
71
- * `write(data)` **MUST** schedule **all** the data to be sent. `data` **MUST** be a String. Servers **MAY** silently convert non-String objects to JSON if an application attempts to `write` a non-String value, otherwise servers **SHOULD** throw an exception.
72
-
73
- A call to `write` only promises that the data is scheduled to be sent. Implementation details may differ across servers.
74
-
75
- `write` shall return `true` on success and `false` if the connection is closed.
76
-
77
- For WebSocket connections only (irrelevant for EventSource connections):
78
-
79
- * If `data` is UTF-8 encoded, the data will be sent as text.
80
-
81
- * If `data` is binary encoded it will be sent as non-text (as specified by the WebSocket Protocol).
82
-
83
- A server **SHOULD** document its concurrency model, allowing developers to know whether `write` will block or not, whether buffered IO is implemented, etc'.
84
-
85
- For example, evented servers are encouraged to avoid blocking and return immediately, deferring the actual `write` operation for later. However, (process/thread/fiber) per-connection based servers **MAY** choose to return only after all the data was sent. Documenting these differences will allows applications to choose the model that best fits their needs and environments.
86
-
87
- * `close` closes the connection once all the data scheduled using `write` was sent. If `close` is called while there is still data to be sent, `close` **SHOULD** return immediately and only take effect once the data was sent.
88
-
89
- `close` shall always return `nil`.
90
-
91
- * `open?` **MUST** return `false` **if** the connection was never open, is known to be closed or marked to be closed. Otherwise `true` **MUST** be returned.
92
-
93
- * `pending` **MUST** return -1 if the connection is closed. Otherwise, `pending` **SHOULD** return the number of pending writes (messages in the `write` queue\*) that need to be processed before the next time the `on_drained` callback is called.
94
-
95
- Servers **MAY** choose to always return the value `0` **ONLY IF** they never call the `on_drained` callback and the connection is open.
96
-
97
- Servers that return a positive number **MUST** call the `on_drained` callback when a call to `pending` would return the value `0`.
98
-
99
- \*Servers that divide large messages into a number of smaller messages (implement message fragmentation) **MAY** count each fragment separately, as if the fragmentation was performed by the user and `write` was called more than once per message.
100
-
101
- * `pubsub?` **MUST** return `false` **unless** the pub/sub extension is supported.
102
-
103
- Pub/Sub patterns are idiomatic for WebSockets and EventSource connections but their API is out of scope for this extension.
104
-
105
- * `class` **MUST** return the client's Class, allowing it be extended with additional features (such as Pub/Sub, etc').
106
-
107
- **Note**: Ruby adds this method automatically to every class, no need to do a thing.
108
-
109
- The server **MAY** support the following (optional) methods for the `client` object:
110
-
111
- * `handler` if implemented, **MUST** return the callback object linked to the `client` object.
112
-
113
- * `handler=` if implemented, **MUST** set a new Callback Object for `client`.
114
-
115
- This allows applications to switch from one callback object to another (i.e., in case of credential upgrades).
116
-
117
- Once a new Callback Object was set, the server **MUST** call the old handler's `on_close` callback and **afterwards** call the new handler's `on_open` callback.
118
-
119
- It is **RECOMMENDED** (but not required) that this also updates the value for `env['rack.upgrade']`.
120
-
121
- * `timeout` / `timeout=` allows applications to get / set connection timeouts dynamically and separately for each connection. Servers **SHOULD** provide a global setting for the default connection timeout. It is **RECOMMENDED** (but not required) that a global / default timeout setting be available from the command line (CLI).
122
-
123
- * `protocol` if implemented, **MUST** return the same value that was originally set by `env['rack.upgrade?']`.
124
-
125
-
126
- WebSocket `ping` / `pong`, timeouts and network considerations **SHOULD** be implemented by the server. It is **RECOMMENDED** (but not required) that the server send `ping`s to prevent connection timeouts and to detect network failure. Clients **SHOULD** also consider sending `ping`s to detect network errors (dropped connections).
127
-
128
- Server settings **MAY** be provided to allow for customization and adaptation for different network environments or WebSocket extensions. It is **RECOMMENDED** that any settings be available as command line arguments and **not** incorporated into the application's logic.
129
-
130
- ---
131
-
132
- ## Implementation Examples
133
-
134
- ### Server-Client Upgrade to WebSockets / EventSource
135
-
136
- * **Server**:
137
-
138
- When a regular HTTP request arrives (non-upgradeable), the server will set the `env['rack.upgrade?']` flag to `false`, indicating that: 1. this specific request is NOT upgradable; and 2. the server supports this specification for either WebSocket and/or EventSource connections.
139
-
140
- When a WebSocket upgrade request arrives, the server will set the `env['rack.upgrade?']` flag to `:websocket`, indicating that: 1. this specific request is upgradable; and 2. the server supports this specification for WebSocket connections.
141
-
142
- When an EventSource request arrives, the server will set the `env['rack.upgrade?']` flag to `:sse`, indicating that: 1. this specific request is an EventSource request; and 2. the server supports this specification for EventSource connections.
143
-
144
- * **Client**:
145
-
146
- If a client decides to upgrade a request, they will place an appropriate Callback Object in the `env['rack.upgrade']` Hash key.
147
-
148
- * **Server**:
149
-
150
- 1. If the application's response status indicates an error or a redirection (status code `>= 300`), the server shall ignore the Callback Object and/or remove it from the `env` Hash, ignoring the rest of the steps that follow.
151
-
152
- 2. The server will review the `env` Hash *before* sending the response. If the `env['rack.upgrade']` was set, the server will perform the upgrade.
153
-
154
- 3. The server will send the correct response status and headers, as well as any headers present in the response object. The server will also perform any required housekeeping, such as closing the response body, if it exists.
155
-
156
- The response status provided by the response object shall be ignored and the correct response status shall be set by the server.
157
-
158
- 4. Once the upgrade had completed, the server will call the `on_open` callback.
159
-
160
- No other callbacks shall be called until the `on_open` callback had returned.
161
-
162
- WebSocket messages shall be handled by the `on_message` callback in the same order in which they arrive and the `on_message` **SHOULD NOT** be executed concurrently for the same connection.
163
-
164
- The `on_close` callback **MUST NOT** be called while any other callback is running (`on_open`, `on_message`, `on_drained`, etc').
165
-
166
- The `on_drained` callback **MAY** be called concurrently with the `on_message` callback, allowing data to be sent even while incoming data is being processed. Multi-threading considerations apply.
167
-
168
- ## Example Usage
169
-
170
- The following is an example WebSocket echo server implemented using this specification:
171
-
172
- ```ruby
173
- module WSConnection
174
- def on_open(client)
175
- puts "WebSocket connection established (#{client.object_id})."
176
- end
177
- def on_message(client, data)
178
- client.write data # echo the data back
179
- puts "on_drained MUST be implemented if #{ pending } != 0."
180
- end
181
- def on_drained(client)
182
- puts "If this line prints out, on_drained is supported by the server."
183
- end
184
- def on_shutdown(client)
185
- client.write "The server is going away. Goodbye."
186
- end
187
- def on_close(client)
188
- puts "WebSocket connection closed (#{client.object_id})."
189
- end
190
- extend self
191
- end
192
-
193
- module App
194
- def self.call(env)
195
- if(env['rack.upgrade?'.freeze] == :websocket)
196
- env['rack.upgrade'.freeze] = WSConnection
197
- return [0, {}, []]
198
- end
199
- return [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Hello World!"]]
200
- end
201
- end
202
-
203
- run App
204
- ```
205
-
206
- The following example uses Push notifications for both WebSocket and SSE connections. The Pub/Sub API is subject to a separate Pub/Sub API extension and isn't part of this specification (it is, however, supported by iodine):
207
-
208
- ```ruby
209
- module Chat
210
- def on_open(client)
211
- client.class.prepend MyPubSubModule unless client.pubsub?
212
- client.subscribe "chat"
213
- client.publish "chat", "#{env[:nickname]} joined the chat."
214
- end
215
- def on_message(client, data)
216
- client.publish "chat", "#{env[:nickname]}: #{data}"
217
- end
218
- def on_close(client)
219
- client.publish "chat", "#{env[:nickname]}: left the chat."
220
- end
221
- extend self
222
- end
223
-
224
- module App
225
- def self.call(env)
226
- if(env['rack.upgrade?'.freeze])
227
- nickname = env['PATH_INFO'][1..-1]
228
- nickname = "Someone" if nickname == "".freeze
229
- env[:nickname] = nickname
230
- return [0, {}, []]
231
- end
232
- return [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Hello World!"]]
233
- end
234
- end
235
-
236
- run App
237
- ```
238
-
239
- Note that SSE connections will only be able to receive messages (the `on_message` callback is never called).
data/bin/console DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # this will compile Iodine and start a Ruby console with the Iodine gem loaded.
4
-
5
- Dir.chdir(File.expand_path(File.join('..', '..'), __FILE__))
6
- puts `rake clean`
7
- puts `rake compile`
8
-
9
- require 'benchmark'
10
- $LOAD_PATH.unshift File.expand_path(File.join('..', '..', 'lib'), __FILE__ )
11
- require "bundler/setup"
12
- require "iodine"
13
-
14
- # You can add fixtures and/or initialization code here to make experimenting
15
- # with your gem easier. You can also use a different console, if you like.
16
-
17
- # (If you use this, don't forget to add pry to your Gemfile!)
18
- # require "pry"
19
- # Pry.start
20
-
21
- require "irb"
22
- IRB.start
data/bin/info.md DELETED
@@ -1,353 +0,0 @@
1
- # Ruby's Rack Push: Decoupling the real-time web application from the web
2
-
3
- ![](https://bowild.files.wordpress.com/2018/05/6865783407_84f470ec02_o.jpg)
4
-
5
- Something exciting is coming.
6
-
7
- Everyone is talking about WebSockets and their older cousin EventSource / Server Sent Events (SSE). Faye and ActionCable are all the rage and real-time updates are becoming easier than ever.
8
-
9
- But it's all a mess. It's hard to set up, it's hard to maintain. The performance is meh. In short, the existing design is expensive - it's expensive in developer hours and it's expensive in hardware costs.
10
-
11
- However, [a new PR in the Rack repository](https://github.com/rack/rack/pull/1272) promises to change all that in the near future.
12
-
13
- This PR is a huge step towards simplifying our code base, improving real-time performance and lowering the overall cost of real-time web applications.
14
-
15
- In a sentence, it's an important step towards [decoupling](https://softwareengineering.stackexchange.com/a/244478/224017) the web application from the web.
16
-
17
- Remember, Rack is the interface Ruby frameworks (such and Rails and Sinatra) and web applications use to communicate with the Ruby application servers. It's everywhere. So this is a big deal.
18
-
19
- ## The Problem in a Nutshell
20
-
21
- The problem with the current standard approach, in a nutshell, is that each real-time application process has to run two servers in order to support real-time functionality.
22
-
23
- The two servers might be listening on the same port, they might be hidden away in some gem, but at the end of the day, two different IO event handling units have to run side by side.
24
-
25
- "Why?" you might ask. Well, since you asked, I'll tell you (if you didn't ask, skip to the solution).
26
-
27
- ### The story of the temporary `hijack`
28
-
29
- This is the story of a quick temporary solution coming up on it's 5th year as the only "standard" Rack solution available.
30
-
31
- At some point in our history, the Rack specification needed a way to support long polling and other HTTP techniques. Specifically, Rails 4.0 needed something for their "live stream" feature.
32
-
33
- For this purpose, [the Rack team came up with the `hijack` API approach](https://github.com/rack/rack/pull/481#issue-9702395).
34
-
35
- This approach allowed for a quick fix to a pressing need. was meant to be temporary, something quick until Rack 2.0 was released (5 years later, the Rack protocol is still at version 1.3).
36
-
37
- The `hijack` API offers applications complete control of the socket. Just hijack the socket away from the server and voilá, instant long polling / SSE support... sort of.
38
-
39
- That's where things started to get messy.
40
-
41
- To handle the (now "free") socket, a lot of network logic had to be copied from the server layer to the application layer (buffering `write` calls, handling incoming data, protocol management, timeout handling, etc').
42
-
43
- This is an obvious violation of the "**S**" in S.O.L.I.D (single responsibility), as it adds IO handling responsibilities to the application / framework.
44
-
45
- It also violates the DRY principle, since the IO handling logic is now duplicated (once within the server and once within the application / framework).
46
-
47
- Additionally, this approach has issues with HTTP/2 connections, since the network protocol and the application are now entangled.
48
-
49
- ### The obvious `hijack` price
50
-
51
- The `hijack` approach has many costs, some hidden, some more obvious.
52
-
53
- The most easily observed price is memory, performance and developer hours.
54
-
55
- Due to code duplication and extra work, the memory consumption for `hijack` based solutions is higher and their performance is slower (more system calls, more context switches, etc').
56
-
57
- Using `require 'faye'` will add WebSockets to your application, but it will take almost 9Mb just to load the gem (this is before any actual work was performed).
58
-
59
- On the other hand, using the `agoo` or `iodine` HTTP servers will add both WebScokets and SSE to your application without any extra memory consumption.
60
-
61
- To be more specific, using `iodine` will consume about 2Mb of memory, marginally less than Puma, while providing both HTTP and real-time capabilities.
62
-
63
- ### The hidden `hijack` price
64
-
65
- A more subtle price is higher hardware costs and a lower clients-per-machine ratio when using `hijack`.
66
-
67
- Why?
68
-
69
- Besides the degraded performance, the `hijack` approach allows some HTTP servers to lean on the `select` system call, (Puma used `select` last time I took a look).
70
-
71
- This system call [breaks down at around the 1024 open file limit](http://man7.org/linux/man-pages/man2/select.2.html#BUGS), possibly limiting each process to 1024 open connections.
72
-
73
- When a connection is hijacked, the sockets don't close as fast as the web server expects, eventually leading to breakage and possible crashes if the 1024 open file limit is exceeded.
74
-
75
- ## The Solution - Callbacks and Events
76
-
77
- [The new proposed Rack Push PR](https://github.com/rack/rack/pull/1272) offers a wonderful effective way to implement WebSockets and SSE while allowing an application to remain totally server agnostic.
78
-
79
- This new proposal leaves the responsibility for the network / IO handling with the server, simplifying the application's code base and decoupling it from the network protocol.
80
-
81
- By using a callback object, the application is notified of any events. Leaving the application free to focus on the data rather than the network layer.
82
-
83
- The callback object doesn't even need to know anything about the server running the application or the underlying protocol.
84
-
85
- ~~The callback object is automatically linked to the correct API using Ruby's `extend` approach, allowing the application to remain totally server agnostic.~~ **EDIT**: the PR was updated, replacing the `extend` approach with an extra `client` object.
86
-
87
- ### How it works
88
-
89
- Every Rack server uses a Hash type object to communicate with a Rack application.
90
-
91
- This is how Rails is built, this is how Sinatra is built and this is how every Rack application / framework is built. It's in [the current Rack specification](https://github.com/rack/rack/blob/master/SPEC).
92
-
93
- A simple Hello world using Rack would look like this (placed in a file called `config.ru`):
94
-
95
- ```ruby
96
- # normal HTTP response
97
- RESPONSE = [200, { 'Content-Type' => 'text/html',
98
- 'Content-Length' => '12' }, [ 'Hello World!' ] ]
99
- # note the `env` variable
100
- APP = Proc.new {|env| RESPONSE }
101
- # The Rack DSL used to run the application
102
- run APP
103
- ```
104
-
105
- This new proposal introduces the `env['rack.upgrade?']` variable.
106
-
107
- Normally, this variable is set to `nil` (or missing from the `env` Hash).
108
-
109
- However, for WebSocket connection, the `env['rack.upgrade?']` variable is set to `:websocket` and for EventSource (SSE) connections the variable is set to `:sse`.
110
-
111
- To set a callback object, the `env['rack.upgrade']` is introduced (notice the *missing* question mark).
112
-
113
- Now the design might look like this:
114
-
115
- ```ruby
116
- # Place in config.ru
117
- RESPONSE = [200, { 'Content-Type' => 'text/html',
118
- 'Content-Length' => '12' }, [ 'Hello World!' ] ]
119
- # an example Callback class
120
- class MyCallbacks
121
- def on_open client
122
- puts "* Push connection opened."
123
- end
124
- def on_message client, data
125
- puts "* Incoming data: #{data}"
126
- client.write "Roger that, \"#{data}\""
127
- end
128
- def on_close client
129
- puts "* Push connection closed."
130
- end
131
- end
132
- # note the `env` variable
133
- APP = Proc.new do |env|
134
- if(env['rack.upgrade?'])
135
- env['rack.upgrade'] = MyCallbacks.new
136
- [200, {}, []]
137
- else
138
- RESPONSE
139
- end
140
- end
141
- # The Rack DSL used to run the application
142
- run APP
143
- ```
144
-
145
- Run this application with the Agoo or Iodine servers and let the magic sparkle.
146
-
147
- For example, using Iodine:
148
-
149
- ```bash
150
- # install iodine, version 0.6.0 and up
151
- gem install iodine
152
- # start in single threaded mode
153
- iodine -t 1
154
- ```
155
-
156
- Now open the browser, visit [localhost:3000](http://localhost:3000) and open the browser console to test some JavaScript.
157
-
158
- First try an EventSource (SSE) connection (run in browser console):
159
-
160
- ```js
161
- // An SSE example
162
- var source = new EventSource("/");
163
- source.onmessage = function(msg) {
164
- console.log(msg.id);
165
- console.log(msg.data);
166
- };
167
- ```
168
-
169
- Sweet! nothing happened just yet (we aren't sending notifications), but we have an open SSE connection!
170
-
171
- What about WebSockets (run in browser console):
172
-
173
- ```js
174
- // A WebSocket example
175
- ws = new WebSocket("ws://localhost:3000/");
176
- ws.onmessage = function(e) { console.log(e.data); };
177
- ws.onclose = function(e) { console.log("closed"); };
178
- ws.onopen = function(e) { e.target.send("Hi!"); };
179
-
180
- ```
181
-
182
- Wow! Did you look at the Ruby console - we have working WebSockets, it's that easy.
183
-
184
- And this same example will run perfectly using the Agoo server as well (both Agoo and Iodine already support the Rack Push proposal).
185
-
186
- Try it:
187
-
188
- ```bash
189
- # install the agoo server, version 2.1.0 and up
190
- gem install agoo
191
- # start it up
192
- rackup -s agoo -p 3000
193
- ```
194
-
195
- Notice, no gems, no extra code, no huge memory consumption, just the Ruby server and raw Rack (I didn't even use a framework just yet).
196
-
197
- ### The amazing push
198
-
199
- So far, it's so simple, it's hard to notice how powerful this is.
200
-
201
- Consider implementing a stock ticker, or in this case, a timer:
202
-
203
- ```ruby
204
- # Place in config.ru
205
- RESPONSE = [200, { 'Content-Type' => 'text/html',
206
- 'Content-Length' => '12' }, [ 'Hello World!' ] ]
207
-
208
- # A global live connection storage
209
- module LiveList
210
- @list = []
211
- @lock = Mutex.new
212
- def <<(connection)
213
- @lock.synchronize { @list << connection }
214
- end
215
- def >>(connection)
216
- @lock.synchronize { @list.delete connection }
217
- end
218
- def any?
219
- # remove connection to the "live list"
220
- @lock.synchronize { @list.any? }
221
- end
222
- # this will send a message to all the connections that share the same process.
223
- # (in cluster mode we get partial broadcasting only and this doesn't scale)
224
- def broadcast(data)
225
- # copy the list so we don't perform long operations in the critical section
226
- tmp = nil # place tmp in this part of the scope
227
- @lock.synchronize do
228
- tmp = @list.dup # copy list into tmp
229
- end
230
- # iterate list outside of critical section
231
- tmp.each {|c| c.write data }
232
- end
233
- extend self
234
- end
235
-
236
- # Broadcast the time very second... but...
237
- # Threads will BREAK in cluster mode.
238
- @thread = Thread.new do
239
- while(LiveList.any?) do
240
- sleep(1)
241
- LiveList.broadcast "The time is: #{Time.now}"
242
- end
243
- end
244
-
245
- # an example static Callback module
246
- module MyCallbacks
247
- def on_open client
248
- # add connection to the "live list"
249
- LiveList << client
250
- end
251
- def on_message(client, data)
252
- # Just an example broadcast
253
- LiveList.broadcast "Special Announcement: #{data}"
254
- end
255
- def on_close client
256
- # remove connection to the "live list"
257
- LiveList >> client
258
- end
259
- extend self
260
- end
261
-
262
- # The Rack application
263
- APP = Proc.new do |env|
264
- if(env['rack.upgrade?'])
265
- env['rack.upgrade'] = MyCallbacks
266
- [200, {}, []]
267
- else
268
- RESPONSE
269
- end
270
- end
271
- # The Rack DSL used to run the application
272
- run APP
273
- ```
274
-
275
- Run the iodine server in single process mode: `iodine -w 1` and the little timer is ticking.
276
-
277
- Honestly, I don't love the code I just wrote for the previous example. It's a little long, it's slightly iffy and we can't use iodine's cluster mode.
278
-
279
- For my next example, I'll author a chat room in 32 lines (including comments).
280
-
281
- I will use Iodine's pub/sub extension API to avoid the LiveList module and the timer thread. I don't want a timer, so I'll skip the [`Iodine.run_every` method](https://www.rubydoc.info/github/boazsegev/iodine/master/Iodine#run_every-class_method).
282
-
283
- Also, I'll limit the interaction to WebSocket clients. Why? to show I can.
284
-
285
- This will better demonstrate the power offered by the new `env['rack.upgrade']` approach and it will also work in cluster mode.
286
-
287
- Sadly, this means that the example won't run on Agoo for now.
288
-
289
- ```ruby
290
- # Place in config.ru
291
- RESPONSE = [200, { 'Content-Type' => 'text/html',
292
- 'Content-Length' => '12' }, [ 'Hello World!' ] ]
293
- CHAT = "chat".freeze
294
- # a Callback class
295
- class MyCallbacks
296
- def initialize env
297
- @name = env["PATH_INFO"][1..-1]
298
- @name = "unknown" if(@name.length == 0)
299
- end
300
- def on_open client
301
- client.subscribe CHAT
302
- client.publish CHAT, "#{@name} joined the chat."
303
- end
304
- def on_message client, data
305
- client.publish CHAT, "#{@name}: #{data}"
306
- end
307
- def on_close client
308
- client.publish CHAT, "#{@name} left the chat."
309
- end
310
- end
311
- # The actual Rack application
312
- APP = Proc.new do |env|
313
- if(env['rack.upgrade?'] == :websocket)
314
- env['rack.upgrade'] = MyCallbacks.new(env)
315
- [200, {}, []]
316
- else
317
- RESPONSE
318
- end
319
- end
320
- # The Rack DSL used to run the application
321
- run APP
322
- ```
323
-
324
- Start the application from the command line (in terminal):
325
-
326
- ```bash
327
- iodine
328
- ```
329
-
330
- Now try (in the browser console):
331
-
332
- ```js
333
- ws = new WebSocket("ws://localhost:3000/Mitchel");
334
- ws.onmessage = function(e) { console.log(e.data); };
335
- ws.onclose = function(e) { console.log("Closed"); };
336
- ws.onopen = function(e) { e.target.send("Yo!"); };
337
- ```
338
-
339
- **EDIT**: Agoo 2.1.0 now implements pub/sub extensions, albeit, using slightly different semantics. I did my best so the same code would work on both servers.
340
-
341
- ### Why didn't anyone think of this sooner?
342
-
343
- Actually, this isn't a completely new idea.
344
-
345
- Evens as the `hijack` API itself was suggested, [an alternative approach was suggested](https://github.com/rack/rack/pull/481#issuecomment-11916942).
346
-
347
- Another proposal was attempted [a few years ago](https://github.com/rack/rack/issues/1093).
348
-
349
- But it seems things are finally going to change, as two high performance server, [agoo](https://github.com/ohler55/agoo) and [iodine](https://github.com/boazsegev/iodine) already support this new approach.
350
-
351
- Things look promising.
352
-
353
- **UPDATE**: code examples were updated to reflect changes in theRack specification's PR.