iodine 0.4.19 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/CHANGELOG.md +22 -0
- data/LIMITS.md +19 -9
- data/README.md +92 -77
- data/SPEC-PubSub-Draft.md +113 -0
- data/SPEC-Websocket-Draft.md +127 -143
- data/bin/http-hello +0 -1
- data/bin/raw-rbhttp +1 -1
- data/bin/raw_broadcast +8 -10
- data/bin/updated api +2 -2
- data/bin/ws-broadcast +2 -4
- data/bin/ws-echo +2 -2
- data/examples/config.ru +13 -13
- data/examples/echo.ru +5 -6
- data/examples/hello.ru +2 -3
- data/examples/info.md +316 -0
- data/examples/pubsub_engine.ru +81 -0
- data/examples/redis.ru +9 -9
- data/examples/shootout.ru +45 -11
- data/ext/iodine/defer.c +194 -297
- data/ext/iodine/defer.h +61 -53
- data/ext/iodine/evio.c +0 -260
- data/ext/iodine/evio.h +50 -22
- data/ext/iodine/evio_callbacks.c +26 -0
- data/ext/iodine/evio_epoll.c +251 -0
- data/ext/iodine/evio_kqueue.c +193 -0
- data/ext/iodine/extconf.rb +1 -1
- data/ext/iodine/facil.c +1420 -542
- data/ext/iodine/facil.h +151 -64
- data/ext/iodine/fio_ary.h +418 -0
- data/ext/iodine/{base64.c → fio_base64.c} +33 -24
- data/ext/iodine/{base64.h → fio_base64.h} +6 -7
- data/ext/iodine/{fio_cli_helper.c → fio_cli.c} +77 -58
- data/ext/iodine/{fio_cli_helper.h → fio_cli.h} +9 -4
- data/ext/iodine/fio_hashmap.h +759 -0
- data/ext/iodine/fio_json_parser.h +651 -0
- data/ext/iodine/fio_llist.h +257 -0
- data/ext/iodine/fio_mem.c +672 -0
- data/ext/iodine/fio_mem.h +140 -0
- data/ext/iodine/fio_random.c +248 -0
- data/ext/iodine/{random.h → fio_random.h} +11 -14
- data/ext/iodine/{sha1.c → fio_sha1.c} +28 -24
- data/ext/iodine/{sha1.h → fio_sha1.h} +38 -16
- data/ext/iodine/{sha2.c → fio_sha2.c} +66 -49
- data/ext/iodine/{sha2.h → fio_sha2.h} +57 -26
- data/ext/iodine/{fiobj_internal.c → fio_siphash.c} +9 -90
- data/ext/iodine/fio_siphash.h +18 -0
- data/ext/iodine/fio_tmpfile.h +38 -0
- data/ext/iodine/fiobj.h +24 -7
- data/ext/iodine/fiobj4sock.h +23 -0
- data/ext/iodine/fiobj_ary.c +143 -226
- data/ext/iodine/fiobj_ary.h +17 -16
- data/ext/iodine/fiobj_data.c +1160 -0
- data/ext/iodine/fiobj_data.h +164 -0
- data/ext/iodine/fiobj_hash.c +298 -406
- data/ext/iodine/fiobj_hash.h +101 -54
- data/ext/iodine/fiobj_json.c +478 -601
- data/ext/iodine/fiobj_json.h +34 -9
- data/ext/iodine/fiobj_numbers.c +383 -51
- data/ext/iodine/fiobj_numbers.h +87 -11
- data/ext/iodine/fiobj_str.c +423 -184
- data/ext/iodine/fiobj_str.h +81 -32
- data/ext/iodine/fiobject.c +273 -522
- data/ext/iodine/fiobject.h +477 -112
- data/ext/iodine/http.c +2243 -83
- data/ext/iodine/http.h +842 -121
- data/ext/iodine/http1.c +810 -385
- data/ext/iodine/http1.h +16 -39
- data/ext/iodine/http1_parser.c +146 -74
- data/ext/iodine/http1_parser.h +15 -4
- data/ext/iodine/http_internal.c +1258 -0
- data/ext/iodine/http_internal.h +226 -0
- data/ext/iodine/http_mime_parser.h +341 -0
- data/ext/iodine/iodine.c +86 -68
- data/ext/iodine/iodine.h +26 -11
- data/ext/iodine/iodine_helpers.c +8 -7
- data/ext/iodine/iodine_http.c +487 -324
- data/ext/iodine/iodine_json.c +304 -0
- data/ext/iodine/iodine_json.h +6 -0
- data/ext/iodine/iodine_protocol.c +107 -45
- data/ext/iodine/iodine_pubsub.c +526 -225
- data/ext/iodine/iodine_pubsub.h +10 -0
- data/ext/iodine/iodine_websockets.c +268 -510
- data/ext/iodine/iodine_websockets.h +2 -4
- data/ext/iodine/pubsub.c +726 -432
- data/ext/iodine/pubsub.h +85 -103
- data/ext/iodine/rb-call.c +4 -4
- data/ext/iodine/rb-defer.c +46 -22
- data/ext/iodine/rb-fiobj2rb.h +117 -0
- data/ext/iodine/rb-rack-io.c +73 -238
- data/ext/iodine/rb-rack-io.h +2 -2
- data/ext/iodine/rb-registry.c +35 -93
- data/ext/iodine/rb-registry.h +1 -0
- data/ext/iodine/redis_engine.c +742 -304
- data/ext/iodine/redis_engine.h +42 -39
- data/ext/iodine/resp_parser.h +311 -0
- data/ext/iodine/sock.c +627 -490
- data/ext/iodine/sock.h +345 -297
- data/ext/iodine/spnlock.inc +15 -4
- data/ext/iodine/websocket_parser.h +16 -20
- data/ext/iodine/websockets.c +188 -257
- data/ext/iodine/websockets.h +24 -133
- data/lib/iodine.rb +52 -7
- data/lib/iodine/cli.rb +6 -24
- data/lib/iodine/json.rb +40 -0
- data/lib/iodine/version.rb +1 -1
- data/lib/iodine/websocket.rb +5 -3
- data/lib/rack/handler/iodine.rb +58 -13
- metadata +38 -48
- data/bin/ws-shootout +0 -107
- data/examples/broadcast.ru +0 -56
- data/ext/iodine/bscrypt-common.h +0 -116
- data/ext/iodine/bscrypt.h +0 -49
- data/ext/iodine/fio2resp.c +0 -60
- data/ext/iodine/fio2resp.h +0 -51
- data/ext/iodine/fio_dict.c +0 -446
- data/ext/iodine/fio_dict.h +0 -99
- data/ext/iodine/fio_hash_table.h +0 -370
- data/ext/iodine/fio_list.h +0 -111
- data/ext/iodine/fiobj_internal.h +0 -280
- data/ext/iodine/fiobj_primitives.c +0 -131
- data/ext/iodine/fiobj_primitives.h +0 -55
- data/ext/iodine/fiobj_sym.c +0 -135
- data/ext/iodine/fiobj_sym.h +0 -60
- data/ext/iodine/hex.c +0 -124
- data/ext/iodine/hex.h +0 -70
- data/ext/iodine/http1_request.c +0 -81
- data/ext/iodine/http1_request.h +0 -58
- data/ext/iodine/http1_response.c +0 -417
- data/ext/iodine/http1_response.h +0 -95
- data/ext/iodine/http_request.c +0 -111
- data/ext/iodine/http_request.h +0 -102
- data/ext/iodine/http_response.c +0 -1703
- data/ext/iodine/http_response.h +0 -250
- data/ext/iodine/misc.c +0 -182
- data/ext/iodine/misc.h +0 -74
- data/ext/iodine/random.c +0 -208
- data/ext/iodine/redis_connection.c +0 -278
- data/ext/iodine/redis_connection.h +0 -86
- data/ext/iodine/resp.c +0 -842
- data/ext/iodine/resp.h +0 -261
- data/ext/iodine/siphash.c +0 -154
- data/ext/iodine/siphash.h +0 -22
- data/ext/iodine/xor-crypt.c +0 -193
- data/ext/iodine/xor-crypt.h +0 -107
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddc0f34881258b83f214f4809227c826c6bc8f1e259a29fecc0d583c2d6c9a69
|
4
|
+
data.tar.gz: 58686f504218966ed558172cca8176c05fde580c122d8d2dc8196253f31c8d14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 608097ab4f29ed39435098c4b2930379d164b4e6d76634032ea056efc8b7f55dad2092cc28ab4bdf4172630eb3c707f68eecfd31f0a43c5e3b6511d9268994f6
|
7
|
+
data.tar.gz: d36b60bc3a67815afa9b2be2c1a2a00a773264146d4ecb5f796e12ab7c143664e2dd3e66f535272ba1c74a6e381e27ff57752d7f88dc3ba4f0d40a7032a6bc19
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,28 @@ Please notice that this change log contains changes for upcoming releases as wel
|
|
6
6
|
|
7
7
|
## Changes:
|
8
8
|
|
9
|
+
#### Change log v.0.5.0
|
10
|
+
|
11
|
+
Changed... everything. At least all the internal bits and some of the API.
|
12
|
+
|
13
|
+
Iodine 0.5.0 is a stability oriented release. It also supports the updated Rack specification draft for WebSocket and SSE connections (yes, iodine 0.5.0 brings about SSE support).
|
14
|
+
|
15
|
+
Deprecated the `each` function family in favor of the more scalable pub/sub approach.
|
16
|
+
|
17
|
+
Moved the HTTP network layer outside of the GIL, more robust pub/sub (using Unix Sockets instead of pipes), hot restart (in cluster mode) and more.
|
18
|
+
|
19
|
+
Larger header support. The total headers length now defaults to 32Kb, but can be adjusted. A hard coded limit of 8Kb per header line is still enforced (to minimize network buffer).
|
20
|
+
|
21
|
+
Improved concurrency and energy consumption (idling CPU cycles reduced).
|
22
|
+
|
23
|
+
Higher overall memory consumption might be observed (some security and network features now perform data copying rather than allowing for direct data access).
|
24
|
+
|
25
|
+
Improved automatic header completion for missing `Content-Length`, `Date` and `Last-Modified`.
|
26
|
+
|
27
|
+
Support for the Unicorn style `before_fork` and `after_fork` DSL as well as the Puma style `on_worker_boot` DSL.
|
28
|
+
|
29
|
+
Credit to Anatoly Nosov (@jomei) for fixing some typos in the documentation.
|
30
|
+
|
9
31
|
---
|
10
32
|
|
11
33
|
#### Change log v.0.4.19
|
data/LIMITS.md
CHANGED
@@ -1,25 +1,35 @@
|
|
1
1
|
# Iodine limits and settings
|
2
2
|
|
3
|
-
I will, at some point, document these... here's
|
3
|
+
I will, at some point, document these... here's the key points:
|
4
4
|
|
5
|
-
## HTTP
|
6
|
-
|
7
|
-
* HTTP Headers have a ~16Kb total size limit.
|
5
|
+
## HTTP limits
|
8
6
|
|
9
7
|
* Uploads are adjustable and limited to ~50Mib by default.
|
10
8
|
|
11
|
-
* HTTP keep-alive is adjustable and set to ~
|
9
|
+
* HTTP connection timeout (keep-alive) is adjustable and set to ~60 seconds by default.
|
10
|
+
|
11
|
+
* HTTP total header size is adjustable and limited to ~32Kib by default.
|
12
|
+
|
13
|
+
* HTTP header line length size is limited to a hard-coded limit of 8Kb.
|
14
|
+
|
15
|
+
* HTTP headers count is limited to a hard-coded limit of 128 headers.
|
12
16
|
|
13
17
|
## Websocket
|
14
18
|
|
15
19
|
* Incoming Websocket message size limits are adjustable and limited to ~250Kib by default.
|
16
20
|
|
17
|
-
##
|
21
|
+
## EventSource / SSE
|
18
22
|
|
19
|
-
*
|
23
|
+
* Iodine will automatically attempt to send a `ping` event instead of disconnecting the connection. The ping interval is the same as the HTTP connection timeout interval.
|
20
24
|
|
21
|
-
|
25
|
+
## Pub/Sub
|
26
|
+
|
27
|
+
* Channel names are binary safe and unlimited in length. However, name lengths effect performance.
|
22
28
|
|
23
|
-
|
29
|
+
**Do NOT allow clients to dictate the channel name**, as they might use extremely long names and cause resource starvation.
|
24
30
|
|
25
31
|
* Pub/sub is limited to the process cluster. To use pub/sub with an external service (such as Redis) an "Engine" is required (see YARD documentation).
|
32
|
+
|
33
|
+
* Pub/sub pattern matching supports only the Redis pattern matching approach. This makes patterns significantly more expensive and exact matches simpler and faster.
|
34
|
+
|
35
|
+
It's recommended to prefer exact channel/stream name matching when possible.
|
data/README.md
CHANGED
@@ -1,22 +1,22 @@
|
|
1
|
-
# iodine - HTTP / Websocket Server with Pub/Sub support
|
2
|
-
[![
|
3
|
-
|
1
|
+
# iodine - a fast HTTP / Websocket Server with native Pub/Sub support for the new web
|
2
|
+
[![Gem](https://img.shields.io/gem/dt/iodine.svg)](https://rubygems.org/gems/iodine)
|
4
3
|
[![Build Status](https://travis-ci.org/boazsegev/iodine.svg?branch=master)](https://travis-ci.org/boazsegev/iodine)
|
5
4
|
[![Gem Version](https://badge.fury.io/rb/iodine.svg)](https://badge.fury.io/rb/iodine)
|
6
5
|
[![Inline docs](http://inch-ci.org/github/boazsegev/iodine.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/iodine/master/frames)
|
7
6
|
[![GitHub](https://img.shields.io/badge/GitHub-Open%20Source-blue.svg)](https://github.com/boazsegev/iodine)
|
8
7
|
|
9
|
-
|
8
|
+
[![Logo](https://github.com/boazsegev/iodine/raw/master/logo.png)](https://github.com/boazsegev/iodine)
|
10
9
|
|
11
|
-
**
|
10
|
+
**Notice: *iodine's core library, [facil.io](https://github.com/boazsegev/facil.io) is being re-vamped with many updates and changes. This is a time to ask - what features are important for you? [let me know here](https://github.com/boazsegev/facil.io/issues/24)***.
|
12
11
|
|
13
12
|
Iodine is a fast concurrent web server for real-time Ruby applications, with native support for:
|
14
13
|
|
15
|
-
* Websockets;
|
14
|
+
* Websockets and EventSource (SSE);
|
16
15
|
* Pub/Sub (with optional Redis Pub/Sub scaling);
|
17
16
|
* Static file service (with automatic `gzip` support for pre-compressed versions);
|
18
17
|
* HTTP/1.1 keep-alive and pipelining;
|
19
18
|
* Asynchronous event scheduling and timers;
|
19
|
+
* Hot Restart (using the USR1 signal);
|
20
20
|
* Client connectivity (attach client sockets to make them evented);
|
21
21
|
* Custom protocol authoring;
|
22
22
|
* and more!
|
@@ -93,6 +93,8 @@ bundler exec iodine -p $PORT -t 16 -w 4 -www /my/public/folder -v
|
|
93
93
|
|
94
94
|
Ruby can leverage static file support (if enabled) by using the `X-Sendfile` header in the Ruby application response.
|
95
95
|
|
96
|
+
To enable iodine's native X-Sendfile support, a static file service (a public folder) needs to be assigned (this informs iodine that static files aren't sent using a different layer, such as nginx).
|
97
|
+
|
96
98
|
This allows Ruby to send very large files using a very small memory footprint, as well as (when possible) leveraging the `sendfile` system call.
|
97
99
|
|
98
100
|
i.e. (example `config.ru` for iodine):
|
@@ -115,7 +117,7 @@ end
|
|
115
117
|
run app
|
116
118
|
```
|
117
119
|
|
118
|
-
Go to [localhost:3000/source](http://localhost:3000/source) to
|
120
|
+
Go to [localhost:3000/source](http://localhost:3000/source) to experience the `X-Sendfile` extension at work.
|
119
121
|
|
120
122
|
#### Pre-Compressed assets / files
|
121
123
|
|
@@ -131,47 +133,61 @@ When a browser that supports compressed encoding (which is most browsers) reques
|
|
131
133
|
|
132
134
|
It's as easy as that. No extra code required.
|
133
135
|
|
134
|
-
### Special HTTP `Upgrade` support
|
136
|
+
### Special HTTP `Upgrade` and SSE support
|
137
|
+
|
138
|
+
Iodine's HTTP server implements the [WebSocket/SSE Rack Specification Draft](SPEC-Websocket-Draft.md), supporting native WebSocket/SSE connections using Rack's `env` Hash.
|
139
|
+
|
140
|
+
This promotes separation of concerns, where iodine handles all the Network related logic and the application can focus on the API and data it provides.
|
135
141
|
|
136
|
-
|
142
|
+
Upgrading an HTTP connection can be performed either using iodine's native WebSocket / EventSource (SSE) support with `env['rack.upgrade?']` or by implementing your own protocol directly over the TCP/IP layer - be it a WebSocket flavor or something completely different - using `env['upgrade.tcp']`.
|
137
143
|
|
138
|
-
|
144
|
+
#### EventSource / SSE
|
139
145
|
|
140
|
-
|
146
|
+
Iodine treats EventSource / SSE connections as if they were a half-duplex WebSocket connection, using the exact same API and callbacks as WebSockets.
|
141
147
|
|
142
|
-
When an
|
148
|
+
When an EventSource / SSE request is received, iodine will set the Rack Hash's upgrade property to `:sse`, so that: `env[rack.upgrade?] == :sse`.
|
143
149
|
|
144
|
-
|
150
|
+
The rest is detailed in the WebSocket support section.
|
145
151
|
|
146
|
-
|
152
|
+
#### WebSockets
|
147
153
|
|
148
|
-
|
154
|
+
When a WebSocket connection request is received, iodine will set the Rack Hash's upgrade property to `:websocket`, so that: `env[rack.upgrade?] == :websocket`
|
155
|
+
|
156
|
+
To "upgrade" the HTTP request to the WebSockets protocol (or SSE), simply provide iodine with a WebSocket Callback Object instance or class: `env['rack.upgrade'] = MyWebsocketClass` or `env['rack.upgrade'] = MyWebsocketClass.new(args)`
|
157
|
+
|
158
|
+
Iodine will adopt the object, providing it with network functionality (methods such as `write`, `defer` and `close` will become available) and invoke it's callbacks on network events.
|
159
|
+
|
160
|
+
Here is a simple chat-room example we can run in the terminal (`irb`) or easily paste into a `config.ru` file:
|
149
161
|
|
150
162
|
```ruby
|
151
163
|
require 'iodine'
|
152
164
|
class WebsocketChat
|
153
165
|
def on_open
|
154
166
|
# Pub/Sub directly to the client (or use a block to process the messages)
|
155
|
-
subscribe
|
167
|
+
subscribe :chat
|
156
168
|
# Writing directly to the socket
|
157
169
|
write "You're now in the chatroom."
|
158
170
|
end
|
159
171
|
def on_message data
|
160
172
|
# Strings and symbol channel names are equivalent.
|
161
|
-
publish
|
173
|
+
publish "chat", data
|
162
174
|
end
|
163
175
|
end
|
164
176
|
Iodine::Rack.app= Proc.new do |env|
|
165
|
-
if env['upgrade
|
166
|
-
env['upgrade
|
177
|
+
if env['rack.upgrade?'.freeze] == :websocket
|
178
|
+
env['rack.upgrade'.freeze] = WebsocketChat # or: WebsocketChat.new
|
179
|
+
[0,{}, []] # It's possible to set cookies for the response.
|
180
|
+
elsif env['rack.upgrade?'.freeze] == :sse
|
181
|
+
puts "SSE connections can only receive data from the server, the can't write."
|
182
|
+
env['rack.upgrade'.freeze] = WebsocketChat # or: WebsocketChat.new
|
167
183
|
[0,{}, []] # It's possible to set cookies for the response.
|
168
184
|
else
|
169
|
-
[200, {"Content-Length" => "12"}, ["Welcome Home"] ]
|
185
|
+
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
|
170
186
|
end
|
171
187
|
end
|
172
188
|
# Pus/Sub can be server oriented as well as connection bound
|
173
189
|
root_pid = Process.pid
|
174
|
-
Iodine.subscribe(
|
190
|
+
Iodine.subscribe(:chat) {|ch, msg| puts msg if Process.pid == root_pid }
|
175
191
|
# By default, Pub/Sub performs in process cluster mode.
|
176
192
|
Iodine.processes = 4
|
177
193
|
# static file serving can be set manually as well as using the command line:
|
@@ -208,19 +224,11 @@ if(Iodine.default_pubsub.is_a? Iodine::PubSub::RedisEngine)
|
|
208
224
|
end
|
209
225
|
```
|
210
226
|
|
211
|
-
**Details and Limitations:**
|
212
|
-
|
213
|
-
* Iodine does not use a Hash table for the Pub/Sub channels, it uses a [4 bit trie](https://en.wikipedia.org/wiki/Trie).
|
214
|
-
|
215
|
-
The cost is higher memory consumption per channel and a limitation of 1024 bytes per channel name (shorter names are better).
|
216
|
-
|
217
|
-
The bonus is high lookup times, zero chance of channel conflicts and an optimized preference for shared prefix channels (i.e. "user:1", "user:2"...).
|
218
|
-
|
219
|
-
Another added bonus is pattern publishing (is addition to pattern subscriptions) which isn't available when using Redis (since Redis doesn't support this feature).
|
227
|
+
**Pub/Sub Details and Limitations:**
|
220
228
|
|
221
229
|
* Iodine's Redis client does *not* support multiple databases. This is both because [database scoping is ignored by Redis during pub/sub](https://redis.io/topics/pubsub#database-amp-scoping) and because [Redis Cluster doesn't support multiple databases](https://redis.io/topics/cluster-spec). This indicated that multiple database support just isn't worth the extra effort.
|
222
230
|
|
223
|
-
* The iodine Redis client will use
|
231
|
+
* The iodine Redis client will use a single Redis connection per process (for publishing data) and an extra Redis connection for subscriptions (owned by the master process). Connections will be automatically re-established if timeouts or errors occur.
|
224
232
|
|
225
233
|
#### TCP/IP (raw) sockets
|
226
234
|
|
@@ -241,7 +249,7 @@ Iodine::Rack.app = Proc.new do |env|
|
|
241
249
|
# to upgrade AFTER a response, set a valid response status code.
|
242
250
|
[1000,{}, []]
|
243
251
|
else
|
244
|
-
[200, {"Content-Length" => "12"}, ["Welcome Home"] ]
|
252
|
+
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
|
245
253
|
end
|
246
254
|
end
|
247
255
|
Iodine.start
|
@@ -251,63 +259,38 @@ Iodine.start
|
|
251
259
|
|
252
260
|
This design has a number of benefits, some of them related to better IO handling, resource optimization (no need for two IO polling systems), etc. This also allows us to use middleware without interfering with connection upgrades and provides backwards compatibility.
|
253
261
|
|
254
|
-
Iodine::Rack imposes a few restrictions for performance and security reasons, such as
|
262
|
+
Iodine::Rack imposes a few restrictions for performance and security reasons, such as limitimg each header line to 4Kb. These restrictions shouldn't be an issue and are similar to limitations imposed by Apache or Nginx.
|
255
263
|
|
256
264
|
Of course, if you still want to use Rack's `hijack` API, iodine will support you - but be aware that you will need to implement your own reactor and thread pool for any sockets you hijack, as well as a socket buffer for non-blocking `write` operations (why do that when you can write a protocol object and have the main reactor manage the socket?).
|
257
265
|
|
258
|
-
###
|
259
|
-
|
260
|
-
Iodine is an evened server, similar in it's architecture to `nginx` and `puma`. It's different than the simple "thread-per-client" design that is often taught when we begin to learn about network programming.
|
261
|
-
|
262
|
-
By leveraging `epoll` (on Linux) and `kqueue` (on BSD), iodine can listen to multiple network events on multiple sockets using a single thread.
|
263
|
-
|
264
|
-
All these events go into a task queue, together with the application events and any user generated tasks, such as ones scheduled by [`Iodine.run`](http://www.rubydoc.info/github/boazsegev/iodine/Iodine#run-class_method).
|
265
|
-
|
266
|
-
In pseudo-code, this might look like this
|
267
|
-
|
268
|
-
```ruby
|
269
|
-
QUEUE = Queue.new
|
270
|
-
|
271
|
-
def server_cycle
|
272
|
-
if(QUEUE.empty?)
|
273
|
-
QUEUE << get_next_32_socket_events # these events schedule the proper user code to run
|
274
|
-
end
|
275
|
-
QUEUE << server_cycle
|
276
|
-
end
|
266
|
+
### How does it compare to other servers?
|
277
267
|
|
278
|
-
|
279
|
-
while ((event = QUEUE.pop))
|
280
|
-
event.shift.call(*event)
|
281
|
-
end
|
282
|
-
end
|
283
|
-
```
|
268
|
+
Personally, after looking around, the only comparable servers are Puma and Passenger (both offer multi-threaded and multi-process concurrency), which iodine significantly outperformed on my tests (I didn't test Passenger's enterprise version). Another upcoming server is the Agoo server (which has a very high performance).
|
284
269
|
|
285
|
-
|
270
|
+
When benchmarking with `wrk`, on the same local machine with similar settings (4 workers, 16 threads each, 200 concurrent connections), iodine performed better than Puma (I don't have Passenger enterprise, so I couldn't compare against it).
|
286
271
|
|
287
|
-
|
272
|
+
* Iodine performed at 69,885.30 req/sec, consuming ~77.8Mb of memory.
|
288
273
|
|
289
|
-
|
274
|
+
* Puma performed at 48,994.59 req/sec, consuming ~79.6Mb of memory.
|
290
275
|
|
291
|
-
The thread pool is there to help slow user code.
|
292
276
|
|
293
|
-
|
277
|
+
When benchmarking with `wrk` and using striped down settings (single worker, single thread, 200 concurrent connections), iodine was faster than Puma.
|
294
278
|
|
295
|
-
|
279
|
+
* Iodine performed at 56,648.86 req/sec, consuming ~27.4Mb of memory.
|
296
280
|
|
297
|
-
|
281
|
+
* Puma performed at 16,547.31 req/sec, consuming ~23.4Mb of memory.
|
298
282
|
|
299
|
-
|
283
|
+
When benchmarking using a VM (crossing machine boundaries, single thread, single worker, 200 concurrent connections), iodine significantly outperformed Puma.
|
300
284
|
|
301
|
-
|
285
|
+
* Iodine performed at 18,444.31 req/sec, consuming ~25.6Mb of memory.
|
302
286
|
|
303
|
-
|
287
|
+
* Puma performed at 2,521.56 req/sec, consuming ~27.5Mb of memory.
|
304
288
|
|
305
|
-
Another assumption iodine makes is that it is behind a load balancer / proxy (which is the normal way Ruby applications are deployed) - this allows iodine to disregard some header validity checks (we're not checking for invalid characters) and focus it's resources on other security and performance concerns.
|
306
289
|
|
307
|
-
I recommend benchmarking the performance for yourself using `wrk` or `ab`:
|
290
|
+
I have doubts about my own benchmarks and I recommend benchmarking the performance for yourself using `wrk` or `ab`:
|
308
291
|
|
309
292
|
```bash
|
310
|
-
$ wrk -c200 -d4 -
|
293
|
+
$ wrk -c200 -d4 -t2 http://localhost:3000/
|
311
294
|
# or
|
312
295
|
$ ab -n 100000 -c 200 -k http://127.0.0.1:3000/
|
313
296
|
```
|
@@ -334,18 +317,50 @@ $ RACK_ENV=production puma -p 3000 -t 16 -w 4
|
|
334
317
|
# Review the `iodine -?` help for more command line options.
|
335
318
|
```
|
336
319
|
|
320
|
+
### Performance oriented design - but safety first
|
337
321
|
|
338
|
-
|
322
|
+
Iodine is an evened server, similar in it's architecture to `nginx` and `puma`. It's different than the simple "thread-per-client" design that is often taught when we begin to learn about network programming.
|
339
323
|
|
340
|
-
|
324
|
+
By leveraging `epoll` (on Linux) and `kqueue` (on BSD), iodine can listen to multiple network events on multiple sockets using a single thread.
|
325
|
+
|
326
|
+
All these events go into a task queue, together with the application events and any user generated tasks, such as ones scheduled by [`Iodine.run`](http://www.rubydoc.info/github/boazsegev/iodine/Iodine#run-class_method).
|
341
327
|
|
342
|
-
|
328
|
+
In pseudo-code, this might look like this
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
QUEUE = Queue.new
|
332
|
+
|
333
|
+
def server_cycle
|
334
|
+
if(QUEUE.empty?)
|
335
|
+
QUEUE << get_next_32_socket_events # these events schedule the proper user code to run
|
336
|
+
end
|
337
|
+
QUEUE << server_cycle
|
338
|
+
end
|
339
|
+
|
340
|
+
def run_server
|
341
|
+
while ((event = QUEUE.pop))
|
342
|
+
event.shift.call(*event)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
```
|
346
|
+
|
347
|
+
In pure Ruby (without using C extensions or Java), it's possible to do the same by using `select`... and although `select` has some issues, it works well for lighter loads.
|
348
|
+
|
349
|
+
The server events are fairly fast and fragmented (longer code is fragmented across multiple events), so one thread is enough to run the server including it's static file service and everything...
|
350
|
+
|
351
|
+
...but single threaded mode should probably be avoided.
|
352
|
+
|
353
|
+
The thread pool is there to help slow user code.
|
354
|
+
|
355
|
+
It's very common that the application's code will run slower and require external resources (i.e., databases, a custom pub/sub service, etc'). This slow code could "starve" the server, which is patiently waiting to run it's tasks on the same thread.
|
356
|
+
|
357
|
+
The slower your application code, the more threads you will need to keep the server running in a responsive manner (note that responsiveness and speed aren't always the same).
|
343
358
|
|
344
|
-
##
|
359
|
+
## Free, as in freedom (BYO beer)
|
345
360
|
|
346
|
-
|
361
|
+
Iodine is **free** and **open source**, so why not take it out for a spin?
|
347
362
|
|
348
|
-
It's installable just like any other gem on MRI, run:
|
363
|
+
It's installable just like any other gem on Ruby MRI, run:
|
349
364
|
|
350
365
|
```
|
351
366
|
$ gem install iodine
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# Ruby Pub/Sub API Specification Draft
|
2
|
+
|
3
|
+
## Purpose
|
4
|
+
|
5
|
+
The pub/sub design is idiomatic to WebSocket and EventSource approaches as well as other reactive programming techniques.
|
6
|
+
|
7
|
+
The purpose of this specification is to offer a recommendation for pub/sub design that will allow applications to be implementation agnostic (not care which pub/sub extension is used)\*.
|
8
|
+
|
9
|
+
Simply put, applications will not have to worry about the chosen pub/sub implementation or about inter-process communication.
|
10
|
+
|
11
|
+
This should simplify the idiomatic `subscribe` / `publish` approach to real-time data pushing.
|
12
|
+
|
13
|
+
\* The pub/sub extension could be implemented by any external library as long as the API conforms to the specification. The extension will have to manage the fact that some servers `fork` and manage inter-process communication for pub/sub propagation (or limit it's support to specific servers). Also, servers that opt to implement the pub/sub layer, could perform optimizations related to connection handling and pub/sub lifetimes.
|
14
|
+
|
15
|
+
## Pub/Sub handling
|
16
|
+
|
17
|
+
Conforming Pub/Sub implementations **MUST** extend the WebSocket and SSE callback objects to implement the following pub/sub related methods (this requires that either the Pub/Sub implementation has knowledge about the Server OR that the Server has knowledge about the Pub/Sub implementation):
|
18
|
+
|
19
|
+
* `subscribe(to, opt = {}) { |from, message| optional_block }` where `opt` is a Hash object that supports the following possible keys (undefined keys *SHOULD* be ignored):
|
20
|
+
|
21
|
+
* `:match` indicates a matching algorithm should be applied. Possible values should include [`:redis`](https://github.com/antirez/redis/blob/398b2084af067ae4d669e0ce5a63d3bc89c639d3/src/util.c#L46-L167), [`:nats`](https://nats.io/documentation/faq/#wildcards) or [`:rabbitmq`](https://www.rabbitmq.com/tutorials/tutorial-five-ruby.html). Pub/Sub implementations should support some or all of these common pattern resolution schemes.
|
22
|
+
|
23
|
+
* `:handler` is an alternative to the optional block. It should accept Proc like opbjects (objects that answer to `call(from, msg)`).
|
24
|
+
|
25
|
+
* If an optional `block` (or `:handler`) is provided, if will be called when a publication was received. Otherwise, the message alone (**not** the channel data) should be sent directly to the WebSocket / EventSource client.
|
26
|
+
|
27
|
+
* `:as` accepts either `:text` or `:binary` Symbol objects.
|
28
|
+
|
29
|
+
This option is only valid if the optional `block` is missing and the connection is a WebSocket connection. Note that SSE connections are limited to text data by design.
|
30
|
+
|
31
|
+
This will dictate the encoding for outgoing WebSocket message when publications are directly sent to the client (as a text message or a binary blob). `:text` will be the default value for a missing `:as` option.
|
32
|
+
|
33
|
+
If the `subscribe` method is called within a WebSocket / SSE Callback object, the subscription must be associated with the Callback object and closed automatically when the connection is closed.
|
34
|
+
|
35
|
+
If the `subscribe` method isn't called from within a connection, it should be considered a global (non connection related) subscription and a `block` or `:handler` **MUST** be provided.
|
36
|
+
|
37
|
+
The `subscribe` method must return a subscription object if a subscription was scheduled (not necessarily performed). If it's already known that the subscription would fail, the method should return `nil`.
|
38
|
+
|
39
|
+
The subscription object **MUST** support the method `close` (that will close the subscription).
|
40
|
+
|
41
|
+
The subscription object **MAY** support the method `to_s` (that will return a String representing the stream / channel / pattern).
|
42
|
+
|
43
|
+
The subscription object **MUST** support the method `==(str)` where `str` is a String object (that will return true if the subscription matches the String.
|
44
|
+
|
45
|
+
A global variation for this method (allowing global subscriptions to be created) MUST be defined as `Rack::PubSub.subscribe`.
|
46
|
+
|
47
|
+
* `publish(to, message, engine = nil)` (preferably supporting named arguments) where:
|
48
|
+
|
49
|
+
* `to` a String that identifies the channel / stream / subject for the publication ("channel" is the semantic used by Redis, it is similar to "subject" or "stream" in other pub/sub systems).
|
50
|
+
|
51
|
+
* `message` a String with containing the data to be published.
|
52
|
+
|
53
|
+
* `engine` (optional) routed the publish method to the specified Pub/Sub Engine (see later on). If none is specified, the default engine should be used.
|
54
|
+
|
55
|
+
The `publish` method must return `true` if a publication was scheduled (not necessarily performed). If it's already known that the publication would fail, the method should return `false`.
|
56
|
+
|
57
|
+
An implementation **MUST** call the relevant PubSubEngine's `publish` method after performing any internal book keeping logic. If `engine` is `nil`, the default PubSubEngine should be called. If `engine` is `false`, the implementation **MUST** forward the published message to the actual clients (if any).
|
58
|
+
|
59
|
+
A global alias for this method (allowing it to be accessed from outside active connections) should be defined as `Rack::PubSub.publish`.
|
60
|
+
|
61
|
+
Implementations **MUST** implement the following methods:
|
62
|
+
|
63
|
+
* `Rack::PubSub.register(engine)` where `engine` is a PubSubEngine object as described in this specification.
|
64
|
+
|
65
|
+
When a pub/sub engine is registered, the implementation **MUST** inform the engine of any existing or future subscriptions.
|
66
|
+
|
67
|
+
The implementation **MUST** call the engine's `subscribe` callback for each existing (and future) subscription.
|
68
|
+
|
69
|
+
* `Rack::PubSub.deregister(engine)` where `engine` is a PubSubEngine object as described in this specification.
|
70
|
+
|
71
|
+
Removes an engine from the pub/sub registration. The opposit of
|
72
|
+
|
73
|
+
* `Rack::PubSub.default_engine = engine` sets a default pub/sub engine, where `engine` is a PubSubEngine object as described in this specification.
|
74
|
+
|
75
|
+
Implementations **MUST** forward any `publish` method calls to the default pub/sub engine.
|
76
|
+
|
77
|
+
* `Rack::PubSub.default_engine` returns the current default pub/sub engine, where the engine is a PubSubEngine object as described in this specification.
|
78
|
+
|
79
|
+
* `Rack::PubSub.reset(engine)` where `engine` is a PubSubEngine object as described in this specification.
|
80
|
+
|
81
|
+
Implementations **MUST** behave as if the engine was newly registered and (re)inform the engine of any existing subscriptions by calling engine's `subscribe` callback for each existing subscription.
|
82
|
+
|
83
|
+
Implementations **MAY** implement pub/sub internally (in which case the `pubsub_default` engine is the server itself or a server's module).
|
84
|
+
|
85
|
+
However, servers **MUST** support external pub/sub "engines" as described above, using PubSubEngine objects.
|
86
|
+
|
87
|
+
PubSubEngine objects **MUST** implement the following methods:
|
88
|
+
|
89
|
+
* `subscribe(channel, as=nil)` this method performs the subscription to the specified channel.
|
90
|
+
|
91
|
+
If `as` is a Symbol that the engine recognizes (i.e., `:redis`, `:nats`, etc'), the engine should behave accordingly. i.e., the value `:redis` on a Redis engine will invoke the PSUBSCRIBE Redis command.
|
92
|
+
|
93
|
+
The method must return `true` if a subscription was scheduled (or performed) or `false` if the subscription is known to fail.
|
94
|
+
|
95
|
+
This method will be called by the server (for each registered engine). The engine may assume that the method would never be called directly by an application.
|
96
|
+
|
97
|
+
* `unsubscribe(channel, as=nil)` this method performs closes the subscription to the specified channel.
|
98
|
+
|
99
|
+
The method's semantics are similar to `subscribe`.
|
100
|
+
|
101
|
+
This method will be called by the server (for each registered engine). The engine may assume that the method would never be called directly by an application.
|
102
|
+
|
103
|
+
* `publish(channel, message)` where both `channel` and `message` are String object.
|
104
|
+
|
105
|
+
This method will be called by the server when a message is published using the engine.
|
106
|
+
|
107
|
+
The engine **MUST** assume that the method might called directly by an application.
|
108
|
+
|
109
|
+
When a PubSubEngine object receives a published message, it should call:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
Rack::PubSub.publish channel: channel, message: message, engine: false
|
113
|
+
```
|