iodine 0.7.59 → 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.
- checksums.yaml +4 -4
- data/.clang-format +15 -0
- data/.rspec +1 -0
- data/.yardopts +1 -3
- data/CHANGELOG.md +132 -12
- data/Gemfile +5 -7
- data/LICENSE.txt +1 -1
- data/README.md +147 -671
- data/Rakefile +9 -16
- data/examples/chatroom.nru +9 -0
- data/examples/client +74 -0
- data/examples/config-hello.nru +9 -0
- data/examples/config-hello.ru +11 -0
- data/examples/config-hijack.ru +13 -0
- data/examples/config-shootout.ru +43 -0
- data/examples/config.nru +120 -0
- data/examples/config.rb +37 -0
- data/examples/config.ru +42 -53
- data/examples/fake +24 -0
- data/examples/hello_neorack +137 -0
- data/examples/routes.nru +29 -0
- data/examples/routes.ru +42 -0
- data/exe/iodine +218 -258
- data/ext/iodine/extconf.rb +27 -100
- data/ext/iodine/fio-stl.h +128336 -0
- data/ext/iodine/fio-stl.md +26396 -0
- data/ext/iodine/iodine.c +213 -1393
- data/ext/iodine/iodine.h +250 -55
- data/ext/iodine/iodine_arg_helper.h +329 -0
- data/ext/iodine/iodine_caller.h +234 -25
- data/ext/iodine/iodine_cli.h +434 -0
- data/ext/iodine/iodine_compression.h +312 -0
- data/ext/iodine/iodine_connection.h +3484 -34
- data/ext/iodine/iodine_core.h +363 -0
- data/ext/iodine/iodine_crypto.h +1441 -0
- data/ext/iodine/iodine_defer.h +225 -4
- data/ext/iodine/iodine_json.h +738 -4
- data/ext/iodine/iodine_listener.h +263 -0
- data/ext/iodine/iodine_log.h +297 -0
- data/ext/iodine/iodine_minimap.h +543 -0
- data/ext/iodine/iodine_mustache.h +461 -4
- data/ext/iodine/iodine_pubsub_eng.h +729 -0
- data/ext/iodine/iodine_pubsub_history.h +547 -0
- data/ext/iodine/iodine_pubsub_msg.h +228 -0
- data/ext/iodine/iodine_redis.h +322 -0
- data/ext/iodine/iodine_store.h +418 -16
- data/ext/iodine/iodine_threads.h +279 -0
- data/ext/iodine/iodine_tls.h +515 -6
- data/ext/iodine/iodine_utils.h +923 -0
- data/iodine.gemspec +37 -42
- data/lib/iodine/benchmark.rb +1690 -0
- data/lib/iodine/documentation.rb +3085 -0
- data/lib/iodine/version.rb +17 -1
- data/lib/iodine.rb +54 -255
- data/logo.svg +36 -0
- data/mustache.md +55 -0
- data/sig/iodine.rbs +4 -0
- metadata +58 -184
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- data/.github/workflows/ruby.yml +0 -42
- data/.gitignore +0 -20
- data/LIMITS.md +0 -41
- data/SPEC-PubSub-Draft.md +0 -159
- data/SPEC-WebSocket-Draft.md +0 -239
- data/bin/console +0 -22
- data/bin/info.md +0 -353
- data/bin/mustache_bench.rb +0 -100
- data/bin/poc/Gemfile.lock +0 -23
- data/bin/poc/README.md +0 -37
- data/bin/poc/config.ru +0 -66
- data/bin/poc/gemfile +0 -1
- data/bin/poc/www/index.html +0 -57
- data/examples/async_task.ru +0 -92
- data/examples/bates/README.md +0 -3
- data/examples/bates/config.ru +0 -342
- data/examples/bates/david+bold.pdf +0 -0
- data/examples/bates/public/drop-pdf.png +0 -0
- data/examples/bates/public/index.html +0 -600
- data/examples/echo.ru +0 -59
- data/examples/etag.ru +0 -16
- data/examples/hello.ru +0 -29
- data/examples/pubsub_engine.ru +0 -81
- data/examples/rack3.ru +0 -12
- data/examples/redis.ru +0 -70
- data/examples/shootout.ru +0 -73
- data/examples/sub-protocols.ru +0 -90
- data/examples/tcp_client.rb +0 -66
- data/examples/x-sendfile.ru +0 -14
- data/ext/iodine/fio.c +0 -11995
- data/ext/iodine/fio.h +0 -6371
- data/ext/iodine/fio_cli.c +0 -431
- data/ext/iodine/fio_cli.h +0 -189
- data/ext/iodine/fio_json_parser.h +0 -688
- data/ext/iodine/fio_siphash.c +0 -157
- data/ext/iodine/fio_siphash.h +0 -37
- data/ext/iodine/fio_tls.h +0 -129
- data/ext/iodine/fio_tls_missing.c +0 -649
- data/ext/iodine/fio_tls_openssl.c +0 -1106
- data/ext/iodine/fio_tmpfile.h +0 -50
- data/ext/iodine/fiobj.h +0 -44
- data/ext/iodine/fiobj4fio.h +0 -21
- data/ext/iodine/fiobj_ary.c +0 -333
- data/ext/iodine/fiobj_ary.h +0 -139
- data/ext/iodine/fiobj_data.c +0 -1187
- data/ext/iodine/fiobj_data.h +0 -167
- data/ext/iodine/fiobj_hash.c +0 -409
- data/ext/iodine/fiobj_hash.h +0 -176
- data/ext/iodine/fiobj_json.c +0 -622
- data/ext/iodine/fiobj_json.h +0 -68
- data/ext/iodine/fiobj_mem.h +0 -71
- data/ext/iodine/fiobj_mustache.c +0 -317
- data/ext/iodine/fiobj_mustache.h +0 -62
- data/ext/iodine/fiobj_numbers.c +0 -344
- data/ext/iodine/fiobj_numbers.h +0 -127
- data/ext/iodine/fiobj_str.c +0 -433
- data/ext/iodine/fiobj_str.h +0 -172
- data/ext/iodine/fiobject.c +0 -620
- data/ext/iodine/fiobject.h +0 -655
- data/ext/iodine/hpack.h +0 -1923
- data/ext/iodine/http.c +0 -2743
- data/ext/iodine/http.h +0 -1002
- data/ext/iodine/http1.c +0 -910
- data/ext/iodine/http1.h +0 -29
- data/ext/iodine/http1_parser.h +0 -878
- data/ext/iodine/http_internal.c +0 -1279
- data/ext/iodine/http_internal.h +0 -237
- data/ext/iodine/http_mime_parser.h +0 -350
- data/ext/iodine/iodine_caller.c +0 -230
- data/ext/iodine/iodine_connection.c +0 -941
- data/ext/iodine/iodine_defer.c +0 -420
- data/ext/iodine/iodine_fiobj2rb.h +0 -120
- data/ext/iodine/iodine_helpers.c +0 -282
- data/ext/iodine/iodine_helpers.h +0 -12
- data/ext/iodine/iodine_http.c +0 -1181
- data/ext/iodine/iodine_http.h +0 -23
- data/ext/iodine/iodine_json.c +0 -302
- data/ext/iodine/iodine_mustache.c +0 -567
- data/ext/iodine/iodine_pubsub.c +0 -580
- data/ext/iodine/iodine_pubsub.h +0 -26
- data/ext/iodine/iodine_rack_io.c +0 -273
- data/ext/iodine/iodine_rack_io.h +0 -20
- data/ext/iodine/iodine_store.c +0 -146
- data/ext/iodine/iodine_tcp.c +0 -346
- data/ext/iodine/iodine_tcp.h +0 -13
- data/ext/iodine/iodine_tls.c +0 -261
- data/ext/iodine/mustache_parser.h +0 -1546
- data/ext/iodine/redis_engine.c +0 -957
- data/ext/iodine/redis_engine.h +0 -79
- data/ext/iodine/resp_parser.h +0 -317
- data/ext/iodine/websocket_parser.h +0 -506
- data/ext/iodine/websockets.c +0 -743
- data/ext/iodine/websockets.h +0 -185
- data/lib/iodine/connection.rb +0 -61
- data/lib/iodine/json.rb +0 -42
- data/lib/iodine/mustache.rb +0 -113
- data/lib/iodine/pubsub.rb +0 -55
- data/lib/iodine/rack_utils.rb +0 -43
- data/lib/iodine/tls.rb +0 -16
- data/lib/rack/handler/iodine.rb +0 -33
- data/logo.png +0 -0
data/SPEC-WebSocket-Draft.md
DELETED
|
@@ -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
|
-

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