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.
- 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 -21
- 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 -148
- 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/README.md
CHANGED
|
@@ -1,782 +1,258 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://github.com/boazsegev/iodine)
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/iodine)
|
|
3
|
+
[](https://github.com/boazsegev/iodine/actions/workflows/posix.yml)
|
|
4
|
+
[](https://github.com/boazsegev/iodine/actions/workflows/Windows.yml)
|
|
6
5
|
[](http://www.rubydoc.info/github/boazsegev/iodine/master/frames)
|
|
7
|
-
[](https://github.com/boazsegev/iodine)
|
|
6
|
+
[](https://badge.fury.io/rb/iodine)
|
|
7
|
+
[](https://rubygems.org/gems/iodine)
|
|
10
8
|
|
|
11
|
-
Iodine
|
|
9
|
+
# Iodine — a fast, C-powered server and client for Ruby
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
**Try your existing Rack app on Iodine.** Iodine includes built-in WebSockets, SSE, Pub/Sub, static files, TLS, and hot application restarts.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
In a local three-trial benchmark, it delivered ~2.4× Puma's throughput at lower latency.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
* WebSocket connections (server / client);
|
|
19
|
-
* Pub/Sub (with optional Redis Pub/Sub scaling);
|
|
20
|
-
* Fast(!) builtin Mustache templating;
|
|
21
|
-
* Static file service (with automatic `gzip` support for pre-compressed assets);
|
|
22
|
-
* Optimized Logging to `stderr`;
|
|
23
|
-
* Asynchronous event scheduling and timers;
|
|
24
|
-
* HTTP/1.1 keep-alive and pipelining;
|
|
25
|
-
* Heap Fragmentation Protection;
|
|
26
|
-
* TLS 1.2 and above (Requiring OpenSSL >= 1.1.0);
|
|
27
|
-
* TCP/IP server and client connectivity;
|
|
28
|
-
* Unix Socket server and client connectivity;
|
|
29
|
-
* Hot Restarts (using the USR1 signal and without hot deployment);
|
|
30
|
-
* Custom protocol authoring;
|
|
31
|
-
* [Sequel](https://github.com/jeremyevans/sequel) and ActiveRecord forking protection;
|
|
32
|
-
* and more!
|
|
15
|
+
## Keep Client I/O Moving While Ruby Works
|
|
33
16
|
|
|
34
|
-
|
|
17
|
+
Built on [facil.io](https://facil.io), Iodine runs [Rack](https://github.com/rack/rack) and [NeoRack](https://github.com/boazsegev/neorack) applications and includes an evented client for TCP, HTTP, WebSockets, and SSE.
|
|
35
18
|
|
|
36
|
-
|
|
19
|
+
**Network I/O never waits on the Ruby GVL.** Each worker handles client I/O in a dedicated C reactor thread outside the GVL. A separate pool of Ruby threads runs application code.
|
|
37
20
|
|
|
38
|
-
|
|
21
|
+
This separation lets one worker keep thousands of connections alive while Ruby code executes.
|
|
39
22
|
|
|
40
|
-
Iodine
|
|
23
|
+
Iodine calls `accept` for new connections only when Ruby threads are available. This automatically routes new connections to the first available worker.
|
|
41
24
|
|
|
42
|
-
|
|
25
|
+
```mermaid
|
|
26
|
+
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '20px', 'clusterBkg': '#fafafa', 'clusterBorder': '#455a64', 'clusterTitleColor': '#263238', 'titleColor': '#263238'}, 'flowchart': { 'htmlLabels': true, 'curve': 'linear', 'padding': 24, 'nodeSpacing': 48, 'rankSpacing': 56 }}}%%
|
|
27
|
+
flowchart TB
|
|
28
|
+
subgraph Master["Master Process"]
|
|
29
|
+
MONITOR["Worker Monitoring"]
|
|
30
|
+
PUBSUB["Pub/Sub Postoffice"]
|
|
31
|
+
end
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
subgraph Worker["Worker(s) [0...N]"]
|
|
34
|
+
direction TB
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
subgraph ClientIO["IO/Reactor Thread"]
|
|
37
|
+
HTTP["HTTP"]
|
|
38
|
+
WS["WebSockets"]
|
|
39
|
+
SSE["SSE"]
|
|
40
|
+
MAILMAN["Pub/Sub Mailman"]
|
|
41
|
+
end
|
|
47
42
|
|
|
48
|
-
With `Iodine.listen service: :http` it's possible to run multiple HTTP applications (but please remember not to set more than a single application on a single TCP/IP port).
|
|
49
43
|
|
|
50
|
-
|
|
44
|
+
subgraph RubyBox["Ruby Threads / GVL"]
|
|
45
|
+
APP["Application Code"]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
ClientIO ~~~ RubyBox
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
Master <-->|IPC & Monitoring| Worker
|
|
52
|
+
ClientIO <-->|callbacks| RubyBox
|
|
53
|
+
ClientIO <-->|HTTP <code>accept</code> balancing| RubyBox
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
style Master fill:#f3e5f5,stroke:#6a1b9a,stroke-width:5px
|
|
56
|
+
style Worker fill:#e3f2fd,stroke:#1565c0,stroke-width:5px
|
|
57
|
+
style ClientIO fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
|
|
58
|
+
style RubyBox fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
classDef master fill:#ffffff,stroke:#6a1b9a,stroke-width:2px,color:#4a148c
|
|
61
|
+
classDef io fill:#ffffff,stroke:#1565c0,stroke-width:2px,color:#0d47a1
|
|
62
|
+
classDef ruby fill:#ffffff,stroke:#ef6c00,stroke-width:2px,color:#e65100
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
class PUBSUB,MONITOR master
|
|
65
|
+
class HTTP,WS,SSE,MAILMAN io
|
|
66
|
+
class APP ruby
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
## Install and Try Your App
|
|
70
|
+
|
|
71
|
+
**Bundler-managed apps:** add Iodine to your Gemfile. Keep your current server gem installed while you evaluate it:
|
|
65
72
|
|
|
66
73
|
```ruby
|
|
67
|
-
|
|
74
|
+
# Gemfile
|
|
75
|
+
gem "iodine", "~> 0.8"
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
Then start your application from the command-line / terminal using iodine:
|
|
71
|
-
|
|
72
78
|
```bash
|
|
73
|
-
|
|
79
|
+
bundle install
|
|
80
|
+
bundle exec iodine
|
|
74
81
|
```
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
**Note**: iodine has known issues with the TLS/SSL support. TLS/SSL should **NOT** be used in production (see issues #95 and #94).
|
|
83
|
+
Iodine loads `config.ru` (or `config.nru`) from the current directory by default. It accepts `THREADS` and `WORKERS` environment variables.
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
**Rails apps:** if you use Puma, remove `config/puma.rb` or make its content conditional when switching servers. Iodine honors `RAILS_MAX_THREADS` and `WEB_CONCURRENCY` when not otherwise configured, so most Rails apps need no initializer.
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
Prefer command-line options and environment variables for code-free configuration updates.
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
**Standalone Rack apps:** install and run the CLI directly:
|
|
85
90
|
|
|
86
91
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
checking for -lcrypto... yes
|
|
90
|
-
checking for -lssl... yes
|
|
91
|
-
Detected OpenSSL library, testing for version.
|
|
92
|
-
Confirmed OpenSSL to be version 1.1.0 or above (OpenSSL 1.1.0j 20 Nov 2018)...
|
|
93
|
-
* Compiling with HAVE_OPENSSL.
|
|
94
|
-
...
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
The installation script tests for OpenSSL 1.1.0 and above. However, this testing approach sometimes provides false positives. **If TLS isn't required, install with `NO_SSL=1`**. i.e.:
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
NO_SSL=1 bundler exec iodine
|
|
92
|
+
gem install iodine
|
|
93
|
+
iodine
|
|
101
94
|
```
|
|
102
95
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
On Rails:
|
|
106
|
-
|
|
107
|
-
1. Replace the `puma` gem with the `iodine` gem.
|
|
108
|
-
|
|
109
|
-
1. Remove the `config/puma.rb` file (or comment out the code).
|
|
110
|
-
|
|
111
|
-
1. Optionally, it's possible to add a `config/initializers/iodine.rb` file. For example:
|
|
112
|
-
|
|
113
|
-
```ruby
|
|
114
|
-
# Iodine setup - use conditional setup to allow command-line arguments to override these:
|
|
115
|
-
if(defined?(Iodine))
|
|
116
|
-
Iodine.threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i if Iodine.threads.zero?
|
|
117
|
-
Iodine.workers = ENV.fetch("WEB_CONCURRENCY", 2).to_i if Iodine.workers.zero?
|
|
118
|
-
Iodine::DEFAULT_SETTINGS[:port] ||= ENV.fetch("PORT") if ENV.fetch("PORT")
|
|
119
|
-
end
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
When using native WebSockets with Rails, middle-ware is probably the best approach. A guide for this approach will, hopefully, get published in the future.
|
|
123
|
-
|
|
124
|
-
**Note**: command-line instructions (CLI) should be the preferred way for configuring iodine, allowing for code-less configuration updates.
|
|
96
|
+
## Add Real-Time Features Without Extra Infrastructure
|
|
125
97
|
|
|
126
|
-
|
|
98
|
+
**Start without a message broker.** Iodine supports WebSockets and Server-Sent Events in both the server and client. Its built-in Pub/Sub engine routes messages across local worker processes.
|
|
127
99
|
|
|
128
|
-
|
|
100
|
+
A single-host deployment can therefore broadcast without Redis, Valkey, or another service to deploy, monitor, secure, or pay for.
|
|
129
101
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Command line arguments allow easy access to different options, including concurrency levels. i.e., to set up 16 threads and 4 processes:
|
|
102
|
+
**Add Redis or Valkey when your deployment needs it.** Configure a RESP3-compatible service for external integration or independent broker operations:
|
|
133
103
|
|
|
134
104
|
```bash
|
|
135
|
-
|
|
105
|
+
iodine -w -1 -t 8 -r redis://localhost:6379
|
|
136
106
|
```
|
|
137
107
|
|
|
138
|
-
The
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
export THREADS=16
|
|
142
|
-
export WORKERS=-1 # negative values are fractions of CPU cores.
|
|
143
|
-
bundler exec iodine -p $PORT
|
|
144
|
-
```
|
|
108
|
+
The [API reference](http://www.rubydoc.info/github/boazsegev/iodine) covers programmatic RESP3 setup and direct commands.
|
|
145
109
|
|
|
146
|
-
|
|
110
|
+
**Optional LAN mesh:** connect machines on the same local network through automatic UDP peer discovery. All participating machines must share the broadcast port and secret:
|
|
147
111
|
|
|
148
112
|
```bash
|
|
149
|
-
|
|
113
|
+
iodine -w -1 -t 8 -bp 7555 -scrt "my-cluster-secret"
|
|
150
114
|
```
|
|
151
115
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Iodine includes a fast, network oriented, custom memory allocator, optimizing away some of the work usually placed on the Ruby Garbage Collector (GC).
|
|
155
|
-
|
|
156
|
-
This approach helps to minimize heap fragmentation for long running processes, by grouping many short-lived objects into a common memory space.
|
|
116
|
+
Mesh traffic uses ChaCha20/Poly1305 with the shared secret. The `SECRET` and `PUBSUB_PORT` environment variables provide the same settings. The secret may be readable by other users on the same machine, and this feature is lightly tested; please report dropped messages or isolated machines.
|
|
157
117
|
|
|
158
|
-
|
|
118
|
+
## Use Built-In Static Files, Compression, and TLS
|
|
159
119
|
|
|
160
|
-
|
|
120
|
+
**Static files:** serve a public directory without Rack middleware. The C layer answers `GET` and `HEAD` requests directly, bypassing Ruby. Other methods and misses fall through to your app.
|
|
161
121
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
This means that iodine won't lock Ruby's GVL when sending static files. The files will be sent directly, allowing for true native concurrency.
|
|
165
|
-
|
|
166
|
-
Since the Ruby layer is unaware of these requests, logging can be performed by turning iodine's logger on.
|
|
167
|
-
|
|
168
|
-
To use native static file service, setup the public folder's address **before** starting the server.
|
|
169
|
-
|
|
170
|
-
This can be done when starting the server from the command line:
|
|
122
|
+
When the client supports compression, Iodine serves a neighboring pre-compressed `.br`, `.zstd`, `.gz`, or `.zip` file. It also probes missing extensions (`.html`, `.htm`, `.txt`, `.md`) and directory `index` files.
|
|
171
123
|
|
|
172
124
|
```bash
|
|
173
|
-
|
|
125
|
+
iodine -www /my/public/folder
|
|
174
126
|
```
|
|
175
127
|
|
|
176
|
-
|
|
128
|
+
**Compression:** dynamic response deflation and WebSocket permessage-deflate are enabled by default; disable them with `-no-dynd` and `-no-wsd`.
|
|
177
129
|
|
|
178
|
-
|
|
179
|
-
require 'iodine'
|
|
180
|
-
# static file service
|
|
181
|
-
Iodine.listen, service: :http, public: '/my/public/folder'
|
|
182
|
-
# for static file service, we only need a single thread and a single worker.
|
|
183
|
-
Iodine.threads = 1
|
|
184
|
-
Iodine.start
|
|
185
|
-
```
|
|
130
|
+
**Request logging:** enable request logging with `-v`. Date and time strings are cached to reduce formatting work.
|
|
186
131
|
|
|
187
|
-
|
|
132
|
+
**TLS:** use your own certificate with either OpenSSL or the embedded TLS 1.3.
|
|
188
133
|
|
|
189
134
|
```bash
|
|
190
|
-
|
|
135
|
+
iodine -cert cert.pem -key key.pem # OpenSSL backend
|
|
136
|
+
iodine -mtls # embedded TLS 1.3
|
|
191
137
|
```
|
|
192
138
|
|
|
193
|
-
|
|
139
|
+
The OpenSSL backend is auto-detected at install time. Switch backends at runtime with `Iodine::TLS.default = :iodine` (or `:openssl`). The embedded TLS implementation is unaudited; review its suitability before production use.
|
|
194
140
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
This allows Ruby to send very large files using a very small memory footprint and usually leverages the `sendfile` system call.
|
|
198
|
-
|
|
199
|
-
i.e. (example `config.ru` for iodine):
|
|
200
|
-
|
|
201
|
-
```ruby
|
|
202
|
-
app = proc do |env|
|
|
203
|
-
request = Rack::Request.new(env)
|
|
204
|
-
if request.path_info == '/source'.freeze
|
|
205
|
-
[200, { 'X-Sendfile' => File.expand_path(__FILE__), 'Content-Type' => 'text/plain'}, []]
|
|
206
|
-
elsif request.path_info == '/file'.freeze
|
|
207
|
-
[200, { 'X-Header' => 'This was a Rack::Sendfile response sent as text.' }, File.open(__FILE__)]
|
|
208
|
-
else
|
|
209
|
-
[200, { 'Content-Type' => 'text/html',
|
|
210
|
-
'Content-Length' => request.path_info.length.to_s },
|
|
211
|
-
[request.path_info]]
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
# # optional:
|
|
215
|
-
# use Rack::Sendfile
|
|
216
|
-
run app
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Benchmark [localhost:3000/source](http://localhost:3000/source) to experience the `X-Sendfile` extension at work.
|
|
220
|
-
|
|
221
|
-
#### Pre-Compressed assets / files
|
|
222
|
-
|
|
223
|
-
Rails does this automatically when compiling assets, which is: `gzip` your static files.
|
|
224
|
-
|
|
225
|
-
Iodine will automatically recognize and send the `gz` version if the client (browser) supports the `gzip` transfer-encoding.
|
|
226
|
-
|
|
227
|
-
For example, to offer a compressed version of `style.css`, run (in the terminal):
|
|
141
|
+
Configure client-certificate trust in the binding URL:
|
|
228
142
|
|
|
229
143
|
```bash
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
This results in both files, `style.css` (the original) and `style.css.gz` (the compressed).
|
|
234
|
-
|
|
235
|
-
When a browser that supports compressed encoding (which is most browsers) requests the file, iodine will recognize that a pre-compressed option exists and will prefer the `gzip` compressed version.
|
|
236
|
-
|
|
237
|
-
It's as easy as that. No extra code required.
|
|
238
|
-
|
|
239
|
-
### Special HTTP `Upgrade` and SSE support
|
|
240
|
-
|
|
241
|
-
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.
|
|
242
|
-
|
|
243
|
-
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.
|
|
244
|
-
|
|
245
|
-
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']`.
|
|
246
|
-
|
|
247
|
-
#### EventSource / SSE
|
|
248
|
-
|
|
249
|
-
Iodine treats EventSource / SSE connections as if they were a half-duplex WebSocket connection, using the exact same API and callbacks as WebSockets.
|
|
250
|
-
|
|
251
|
-
When an EventSource / SSE request is received, iodine will set the Rack Hash's upgrade property to `:sse`, so that: `env['rack.upgrade?'] == :sse`.
|
|
252
|
-
|
|
253
|
-
The rest is detailed in the WebSocket support section.
|
|
254
|
-
|
|
255
|
-
#### WebSockets
|
|
256
|
-
|
|
257
|
-
When a WebSocket connection request is received, iodine will set the Rack Hash's upgrade property to `:websocket`, so that: `env['rack.upgrade?'] == :websocket`
|
|
258
|
-
|
|
259
|
-
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)`
|
|
260
|
-
|
|
261
|
-
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.
|
|
262
|
-
|
|
263
|
-
Here is a simple chat-room example we can run in the terminal (`irb`) or easily paste into a `config.ru` file:
|
|
264
|
-
|
|
265
|
-
```ruby
|
|
266
|
-
require 'iodine'
|
|
267
|
-
module WebsocketChat
|
|
268
|
-
def on_open client
|
|
269
|
-
# Pub/Sub directly to the client (or use a block to process the messages)
|
|
270
|
-
client.subscribe :chat
|
|
271
|
-
# Writing directly to the socket
|
|
272
|
-
client.write "You're now in the chatroom."
|
|
273
|
-
end
|
|
274
|
-
def on_message client, data
|
|
275
|
-
# Strings and symbol channel names are equivalent.
|
|
276
|
-
client.publish "chat", data
|
|
277
|
-
end
|
|
278
|
-
extend self
|
|
279
|
-
end
|
|
280
|
-
APP = Proc.new do |env|
|
|
281
|
-
if env['rack.upgrade?'.freeze] == :websocket
|
|
282
|
-
env['rack.upgrade'.freeze] = WebsocketChat
|
|
283
|
-
[0,{}, []] # It's possible to set cookies for the response.
|
|
284
|
-
elsif env['rack.upgrade?'.freeze] == :sse
|
|
285
|
-
puts "SSE connections can only receive data from the server, the can't write."
|
|
286
|
-
env['rack.upgrade'.freeze] = WebsocketChat
|
|
287
|
-
[0,{}, []] # It's possible to set cookies for the response.
|
|
288
|
-
else
|
|
289
|
-
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
# Pus/Sub can be server oriented as well as connection bound
|
|
293
|
-
Iodine.subscribe(:chat) {|ch, msg| puts msg if Iodine.master? }
|
|
294
|
-
# By default, Pub/Sub performs in process cluster mode.
|
|
295
|
-
Iodine.workers = 4
|
|
296
|
-
# # in irb:
|
|
297
|
-
Iodine.listen service: :http, public: "www/public", handler: APP
|
|
298
|
-
Iodine.start
|
|
299
|
-
# # or in config.ru
|
|
300
|
-
run APP
|
|
144
|
+
iodine -b https://0.0.0.0/?trust=ca.pem
|
|
301
145
|
```
|
|
302
146
|
|
|
303
|
-
|
|
147
|
+
The [API reference](http://www.rubydoc.info/github/boazsegev/iodine) covers per-listener `Iodine::TLS#trust` and peer certificate-chain inspection.
|
|
304
148
|
|
|
305
|
-
Iodine
|
|
149
|
+
**Security limits:** Iodine sets defaults for HTTP header and body sizes and incoming WebSocket message size. Configure them with `-maxln`, `-maxhd`, `-maxbd`, and `-maxms`. Large HTTP payloads can be diverted to temporary files. See `iodine -h` and load-test your chosen limits and timeouts.
|
|
306
150
|
|
|
307
|
-
|
|
151
|
+
## Tune Workers and Threads for Your Workload
|
|
308
152
|
|
|
309
|
-
|
|
153
|
+
Set explicit worker and thread counts, then measure your workload:
|
|
310
154
|
|
|
311
155
|
```bash
|
|
312
|
-
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
It's also possible to initialize the iodine<=>Redis link using Ruby, directly from the application's code:
|
|
156
|
+
iodine -t 16 -w 4
|
|
316
157
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if ENV["REDIS_URL"]
|
|
320
|
-
Iodine::PubSub.default = Iodine::PubSub::Redis.new(ENV["REDIS_URL"])
|
|
321
|
-
else
|
|
322
|
-
puts "* No Redis, it's okay, pub/sub will still run on the whole process cluster."
|
|
323
|
-
end
|
|
324
|
-
# ... the rest of the application remains unchanged.
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
Iodine's Redis client can also be used for asynchronous Redis command execution. i.e.:
|
|
328
|
-
|
|
329
|
-
```ruby
|
|
330
|
-
if(Iodine::PubSub.default.is_a? Iodine::PubSub::Redis)
|
|
331
|
-
# Ask Redis about all it's client connections and print out the reply.
|
|
332
|
-
Iodine::PubSub.default.cmd("CLIENT LIST") { |reply| puts reply }
|
|
333
|
-
end
|
|
158
|
+
# equivalent environment configuration
|
|
159
|
+
THREADS=16 WORKERS=4 iodine
|
|
334
160
|
```
|
|
335
161
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
* 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 and performance hit.
|
|
162
|
+
Without explicit settings, Iodine defaults to `-t -4` and `-w -2`, so cluster mode is enabled. The defaults printed by `iodine -h` reflect any `THREADS` or `WORKERS` variables in your current environment.
|
|
339
163
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
* Connections will be automatically re-established if timeouts or errors occur.
|
|
343
|
-
|
|
344
|
-
### Hot Restart
|
|
345
|
-
|
|
346
|
-
Iodine will "hot-restart" the application by shutting down and re-spawning the worker processes.
|
|
347
|
-
|
|
348
|
-
This will clear away any memory fragmentation concerns and other issues that might plague a long running worker process or ruby application.
|
|
349
|
-
|
|
350
|
-
To hot-restart iodine, send the `SIGUSR1` signal to the root process.
|
|
351
|
-
|
|
352
|
-
The following code will hot-restart iodine every 4 hours when iodine is running in cluster mode:
|
|
353
|
-
|
|
354
|
-
```ruby
|
|
355
|
-
Iodine.run_every(4 * 60 * 60 * 1000) do
|
|
356
|
-
Process.kill("SIGUSR1", Process.pid) unless Iodine.worker?
|
|
357
|
-
end
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
Since the master / root process doesn't handle any requests (it only handles pub/sub and house-keeping), it's memory map and process data shouldn't be as affected and the new worker processes should be healthier and more performant.
|
|
361
|
-
|
|
362
|
-
**Note**: This will **not** re-load the application (any changes to the Ruby code require an actual restart).
|
|
363
|
-
|
|
364
|
-
### Optimized HTTP logging
|
|
365
|
-
|
|
366
|
-
By default, iodine is pretty quiet. Some messages are logged to `stderr`, but not many.
|
|
367
|
-
|
|
368
|
-
However, HTTP requests can be logged using iodine's optimized logger to `stderr`. Iodine will optimize the log output by caching the output time string which updates every second rather than every request.
|
|
369
|
-
|
|
370
|
-
This can be performed by setting the `-v` flag during startup, i.e.:
|
|
371
|
-
|
|
372
|
-
```bash
|
|
373
|
-
bundler exec iodine -p $PORT -t 16 -w 4 -v -www /my/public/folder
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
The log output can be redirected to a file:
|
|
377
|
-
|
|
378
|
-
```bash
|
|
379
|
-
bundler exec iodine -p $PORT -v 2>my_log.log
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
The log output can also be redirected to a `stdout`:
|
|
164
|
+
Negative values represent fractions of the available CPU cores. On an 8-core machine, `-w -2` starts 4 worker processes:
|
|
383
165
|
|
|
384
166
|
```bash
|
|
385
|
-
|
|
167
|
+
iodine -t 1 -w -2 # fast applications: one thread per process, cores/2 processes
|
|
168
|
+
iodine -t 4 -w -4 # slower, CPU-heavy applications
|
|
169
|
+
iodine -t 5 -w -1 # slower, IO-bound applications
|
|
386
170
|
```
|
|
387
171
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
It's a well known fact that [Database connections require special attention when using `fork`-ing servers (multi-process servers)](https://devcenter.heroku.com/articles/concurrency-and-database-connections#multi-process-servers) such as Puma, Passenger (Pro) and iodine.
|
|
172
|
+
Because client I/O runs in the C reactor thread, a single process has been tested with over 20,000 concurrent connections on Linux. Real limits depend on machine resources and application design, not on the server.
|
|
391
173
|
|
|
392
|
-
|
|
174
|
+
On Linux (epoll), macOS and the BSDs (kqueue), Iodine can use worker processes; Windows runs in single-process mode because clustering requires `fork`.
|
|
393
175
|
|
|
394
|
-
|
|
176
|
+
## Hot-Restart Application Code
|
|
395
177
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
### Client Support
|
|
399
|
-
|
|
400
|
-
Iodine supports raw (TCP/IP and Unix Sockets) client connections as well as WebSocket connections.
|
|
401
|
-
|
|
402
|
-
This can be utilized for communicating across micro services or taking advantage of persistent connection APIs such as ActionCable APIs, socket.io APIs etc'.
|
|
403
|
-
|
|
404
|
-
Here is an example WebSocket client that will connect to the [WebSocket.org echo test service](https://www.websocket.org/echo.html) and send a number of pre-programmed messages.
|
|
405
|
-
|
|
406
|
-
```ruby
|
|
407
|
-
require 'iodine'
|
|
408
|
-
|
|
409
|
-
# The client class
|
|
410
|
-
class EchoClient
|
|
411
|
-
|
|
412
|
-
def on_open(connection)
|
|
413
|
-
@messages = [ "Hello World!",
|
|
414
|
-
"I'm alive and sending messages",
|
|
415
|
-
"I also receive messages",
|
|
416
|
-
"now that we all know this...",
|
|
417
|
-
"I can stop.",
|
|
418
|
-
"Goodbye." ]
|
|
419
|
-
send_one_message(connection)
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
def on_message(connection, message)
|
|
423
|
-
puts "Received: #{message}"
|
|
424
|
-
send_one_message(connection)
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def on_close(connection)
|
|
428
|
-
# in this example, we stop iodine once the client is closed
|
|
429
|
-
puts "* Client closed."
|
|
430
|
-
Iodine.stop
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
# We use this method to pop messages from the queue and send them
|
|
434
|
-
#
|
|
435
|
-
# When the queue is empty, we disconnect the client.
|
|
436
|
-
def send_one_message(connection)
|
|
437
|
-
msg = @messages.shift
|
|
438
|
-
if(msg)
|
|
439
|
-
connection.write msg
|
|
440
|
-
else
|
|
441
|
-
connection.close
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
Iodine.threads = 1
|
|
447
|
-
Iodine.connect url: "wss://echo.websocket.org", handler: EchoClient.new, ping: 40
|
|
448
|
-
Iodine.start
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### TLS >= 1.2 support
|
|
452
|
-
|
|
453
|
-
> Requires OpenSSL >= `1.1.0`. On Heroku, requires `heroku-18`.
|
|
454
|
-
|
|
455
|
-
Iodine supports secure connections fore TLS version 1.2 **and up** (depending on the OpenSSL version).
|
|
456
|
-
|
|
457
|
-
A self signed certificate is available using the `-tls` flag from the command-line.
|
|
458
|
-
|
|
459
|
-
PEM encoded certificates (which is probably the most common format) can be loaded from the command-line (`-tls-cert` and `-tls-key`) or dynamically (using `Iodine::TLS`).
|
|
460
|
-
|
|
461
|
-
The TLS API is simplified but powerful, supporting the ALPN extension and peer verification (which client connections really should leverage).
|
|
462
|
-
|
|
463
|
-
When enabling peer verification for server connections (using `Iodine::TLS#trust`), clients will be required to submit a trusted certificate in order to connect to the server.
|
|
464
|
-
|
|
465
|
-
### TCP/IP (raw) sockets
|
|
466
|
-
|
|
467
|
-
Upgrading to a custom protocol (i.e., in order to implement your own WebSocket protocol with special extensions) is available when neither WebSockets nor SSE connection upgrades were requested. In the following (terminal) example, we'll use an echo server without direct socket echo:
|
|
468
|
-
|
|
469
|
-
```ruby
|
|
470
|
-
require 'iodine'
|
|
471
|
-
class MyProtocol
|
|
472
|
-
def on_message client, data
|
|
473
|
-
# regular socket echo - NOT websockets
|
|
474
|
-
client.write data
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
APP = Proc.new do |env|
|
|
478
|
-
if env["HTTP_UPGRADE".freeze] =~ /echo/i.freeze
|
|
479
|
-
env['upgrade.tcp'.freeze] = MyProtocol.new
|
|
480
|
-
# an HTTP response will be sent before changing protocols.
|
|
481
|
-
[101, { "Upgrade" => "echo" }, []]
|
|
482
|
-
else
|
|
483
|
-
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
|
|
484
|
-
end
|
|
485
|
-
end
|
|
486
|
-
# # in irb:
|
|
487
|
-
Iodine.listen service: :http, public: "www/public", handler: APP
|
|
488
|
-
Iodine.threads = 1
|
|
489
|
-
Iodine.start
|
|
490
|
-
# # or in config.ru
|
|
491
|
-
run APP
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### How does it compare to other servers?
|
|
495
|
-
|
|
496
|
-
In my tests, pitching Iodine against Puma, Iodine was anywhere between x1.5 and more than x10 faster than Puma (depending on use-case and settings).
|
|
497
|
-
|
|
498
|
-
Such a big difference is suspect and I recommend that you test it yourself - even better if you test performance using your own application and a number of possible different settings (how many threads per CPU core? how many worker processes? middleware vs. server request logging, etc').
|
|
499
|
-
|
|
500
|
-
I recommend benchmarking the performance for yourself using `wrk` or `ab`:
|
|
178
|
+
**Reload application code without restarting the master.** In cluster mode, send `SIGUSR1` to request a hot restart, or use `-hr <seconds>` to schedule worker restarts:
|
|
501
179
|
|
|
502
180
|
```bash
|
|
503
|
-
|
|
181
|
+
kill -USR1 <root-process-pid>
|
|
504
182
|
# or
|
|
505
|
-
|
|
183
|
+
iodine -hr 3600
|
|
506
184
|
```
|
|
507
185
|
|
|
508
|
-
|
|
186
|
+
A hot restart respawns workers and reloads application code and gems other than Iodine itself. Old workers stop accepting new connections but finish in-flight requests, so both code versions coexist briefly. Use `--preload` instead for copy-on-write memory savings; it disables code swapping.
|
|
509
187
|
|
|
510
|
-
|
|
511
|
-
App = Proc.new do |env|
|
|
512
|
-
[200,
|
|
513
|
-
{ "Content-Type" => "text/html".freeze,
|
|
514
|
-
"Content-Length" => "16".freeze },
|
|
515
|
-
['Hello from Rack!'.freeze] ]
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
run App
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
Then start comparing servers. Here are the settings I used to compare iodine and Puma (4 processes, 4 threads):
|
|
522
|
-
|
|
523
|
-
```bash
|
|
524
|
-
$ RACK_ENV=production iodine -p 3000 -t 4 -w 4
|
|
525
|
-
# vs.
|
|
526
|
-
$ RACK_ENV=production puma -p 3000 -t 4 -w 4
|
|
527
|
-
# Review the `iodine -?` help for more command line options.
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
It's recommended that the servers (Iodine/Puma) and the client (`wrk`/`ab`) run on separate machines.
|
|
531
|
-
|
|
532
|
-
It is worth noting that iodine can also speed up logging by replacing the logging middleware with `iodine -v`. This approach uses less memory and improves performance at the expense of fuzzy timing and some string caching.
|
|
533
|
-
|
|
534
|
-
On my machine, testing with the logging functionality enabled, iodine was more then 10 times faster than puma (60.9K req/sec vs. 5.3K req/sec)
|
|
535
|
-
|
|
536
|
-
### A few notes
|
|
537
|
-
|
|
538
|
-
Iodine's upgrade / callback 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.
|
|
539
|
-
|
|
540
|
-
Iodine's HTTP server imposes a few restrictions for performance and security reasons, such as limiting each header line to 8Kb. These restrictions shouldn't be an issue and are similar to limitations imposed by Apache or Nginx.
|
|
188
|
+
Two drain limits protect against maliciously slow clients. Clients that have not finished sending their request are disconnected. A worker that cannot finish sending its response within 15 seconds is terminated mid-response (compile-time `FIO_IO_SHUTDOWN_TIMEOUT`, default `15000` ms). Applications with SSE streams or other long-running responses should validate restart and drain behavior explicitly.
|
|
541
189
|
|
|
542
|
-
|
|
190
|
+
## Compare Architectures, Then Benchmark Your App
|
|
543
191
|
|
|
544
|
-
|
|
192
|
+
### Architecture Differences
|
|
545
193
|
|
|
546
|
-
|
|
194
|
+
Puma and Iodine are both multi-process Ruby application servers with per-worker thread pools, worker supervision, and hot restarts. They differ mainly in where the I/O layer lives. Puma uses Ruby threads with `nio4r`; Iodine runs a native C reactor thread outside the GVL.
|
|
547
195
|
|
|
548
|
-
|
|
549
|
-
$ gem install iodine
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
Iodine is written in C and allows some compile-time customizations, such as:
|
|
196
|
+
Most differences below follow from that trade-off.
|
|
553
197
|
|
|
554
|
-
|
|
198
|
+
**Legend:** ✅ supported · 🟡 partial / it depends · 🧩 available via external gem, middleware, or service · ❌ not supported
|
|
555
199
|
|
|
556
|
-
|
|
200
|
+
| | Puma | Iodine |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| GVL-free I/O | ❌ | ✅ |
|
|
203
|
+
| GVL-free streaming responses | ❌ | ✅² |
|
|
204
|
+
| WebSocket server (+ permessage-deflate) | 🧩¹ | ✅ |
|
|
205
|
+
| SSE server | 🟡² | ✅ |
|
|
206
|
+
| Static file server | 🧩¹ | ✅ |
|
|
207
|
+
| Response compression (brotli / gzip) | 🧩¹ | ✅ |
|
|
208
|
+
| Cluster-wide Pub/Sub (+ RESP3 scaling) | 🧩¹ | ✅ |
|
|
209
|
+
| NeoRack apps (`.nru`) | ❌ | ✅ |
|
|
210
|
+
| Resilience to slow-client / connection-flood DoS | 🟡² | ✅ |
|
|
211
|
+
| Embedded TLS 1.3 backend (OpenSSL alternative) | ❌ | ✅³ |
|
|
212
|
+
| Mutual TLS (client-certificate verification) | 🟡³ | ✅ |
|
|
213
|
+
| Battle-tested protocol parsers | ✅ | 🟡³ |
|
|
214
|
+
| Built-in stats / control endpoint | ✅ | 🧩¹ |
|
|
215
|
+
| Standard Ruby profiling & debugging | ✅ | ❌ |
|
|
216
|
+
| JRuby / TruffleRuby support | ✅ | ❌ |
|
|
217
|
+
| Windows support | 🟡⁴ | 🟡⁴ |
|
|
218
|
+
| HTTP/2 or HTTP/3 | ❌⁴ | ❌⁴ |
|
|
219
|
+
| Efficient CPU-bound Ruby work (MRI) | 🟡² | 🟡² |
|
|
557
220
|
|
|
558
|
-
|
|
221
|
+
¹ **External dependencies:** 🧩 features are available via external gems, middleware, or services — at the cost of additional dependencies or infrastructure to operate.
|
|
559
222
|
|
|
560
|
-
|
|
223
|
+
² **Threads & the GVL:** Puma dedicates a Ruby thread to each active connection or `each`-style stream, while Iodine's C reactor and NeoRack's callback streaming avoid consuming Ruby threads. CPU-bound work remains GVL-bound in both.
|
|
561
224
|
|
|
562
|
-
|
|
225
|
+
³ **Security:** both servers ship hand-written C components — Puma's are far more battle-tested, while Iodine's newer security features (embedded TLS, mTLS) are tested in-house but not third-party audited.
|
|
563
226
|
|
|
564
|
-
|
|
227
|
+
⁴ **Platforms:** both servers are POSIX-first — Puma's cluster mode requires `fork`, Iodine runs on Windows in single-process mode only (its test suite passes in CI, but it has no production track record yet), and HTTP/2 is typically handled by a front proxy.
|
|
565
228
|
|
|
566
|
-
|
|
229
|
+
### Local Benchmark
|
|
567
230
|
|
|
568
|
-
|
|
231
|
+
A local hello-world Rack benchmark used 4 workers × 4 threads and `wrk`. It ran on macOS arm64 (16 cores) with Ruby 4.0.1, Iodine 0.8.0.rc.02, and Puma 8.0.2. After a 3-second warmup, each server completed three 10-second measurements. Iodine received `-R` and the explicit `./examples/config-hello.ru` path; Puma received the same Rack file.
|
|
569
232
|
|
|
570
|
-
|
|
233
|
+
| Connections | Server | Requests/sec (median) | Avg latency (median) |
|
|
234
|
+
|------------|--------|-----------------------|----------------------|
|
|
235
|
+
| 200 | **Iodine** | **199,177** | **0.97 ms** |
|
|
236
|
+
| 200 | Puma | 83,345 | 2.38 ms |
|
|
237
|
+
| 1000 | **Iodine** | **197,685** | **4.99 ms** |
|
|
238
|
+
| 1000 | Puma | 81,878 | 12.12 ms |
|
|
571
239
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
* `FIO_TLS_PRINT_SECRET` - if true, the OpenSSL master key will be printed as debug message level log. Use only for testing (with WireShark etc'), never in production! Default: false.
|
|
575
|
-
|
|
576
|
-
These options can be used, for example, like so:
|
|
240
|
+
These are local measurements, not a promise about your application. The servers and client shared one machine; the 12-byte response did not exercise slow-reader or large-response backpressure. Run the same check against your app:
|
|
577
241
|
|
|
578
242
|
```bash
|
|
579
|
-
|
|
580
|
-
--with-cflags=\"-DHTTP_MAX_HEADER_LENGTH=48000 -DFIO_FORCE_MALLOC=1 -DHTTP_MAX_HEADER_COUNT=64\"
|
|
243
|
+
wrk -t4 -c200 -d10s http://localhost:3000/
|
|
581
244
|
```
|
|
582
245
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
## Evented oriented design with extra safety
|
|
246
|
+
## Use Evented Clients and Streaming
|
|
586
247
|
|
|
587
|
-
Iodine
|
|
248
|
+
Iodine can make TCP, HTTP, WebSocket, and SSE connections from the same evented runtime. Classic Rack streaming keeps a worker thread occupied. For serious streaming workloads, use [NeoRack](https://github.com/boazsegev/neorack)'s evented approach.
|
|
588
249
|
|
|
589
|
-
|
|
250
|
+
## Documentation and Reporting Issues
|
|
590
251
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
```ruby
|
|
596
|
-
QUEUE = Queue.new
|
|
597
|
-
|
|
598
|
-
def server_cycle
|
|
599
|
-
if(QUEUE.empty?)
|
|
600
|
-
QUEUE << get_next_32_socket_events # these events schedule the proper user code to run
|
|
601
|
-
end
|
|
602
|
-
QUEUE << server_cycle
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
def run_server
|
|
606
|
-
while ((event = QUEUE.pop))
|
|
607
|
-
event.shift.call(*event)
|
|
608
|
-
end
|
|
609
|
-
end
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
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 could work well for lighter loads.
|
|
613
|
-
|
|
614
|
-
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...
|
|
615
|
-
|
|
616
|
-
...but single threaded mode should probably be avoided.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
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 short tasks on the same thread.
|
|
620
|
-
|
|
621
|
-
The thread pool is there to help slow user code.
|
|
622
|
-
|
|
623
|
-
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).
|
|
624
|
-
|
|
625
|
-
To make a thread pool easier and safer to use, iodine makes sure that no connection task / callback is called concurrently for the same connection.
|
|
626
|
-
|
|
627
|
-
For example, a is a WebSocket connection is already busy in it's `on_message` callback, no other messages will be forwarded to the callback until the current callback returns.
|
|
628
|
-
|
|
629
|
-
## Free, as in freedom (BYO beer)
|
|
630
|
-
|
|
631
|
-
Iodine is **free** and **open source**, so why not take it out for a spin?
|
|
632
|
-
|
|
633
|
-
It's installable just like any other gem on Ruby MRI, run:
|
|
634
|
-
|
|
635
|
-
```
|
|
636
|
-
$ gem install iodine
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
If building the native C extension fails, please note that some Ruby installations, such as on Ubuntu, require that you separately install the development headers (`ruby.h` and friends). I have no idea why they do that, as you will need the development headers for any native gems you want to install - so hurry up and get them.
|
|
640
|
-
|
|
641
|
-
If you have the development headers but still can't compile the iodine extension, [open an issue](https://github.com/boazsegev/iodine/issues) with any messages you're getting and I'll be happy to look into it.
|
|
642
|
-
|
|
643
|
-
## Mr. Sandman, write me a server
|
|
644
|
-
|
|
645
|
-
Iodine allows custom TCP/IP server authoring, for those cases where we need raw TCP/IP (UDP isn't supported just yet).
|
|
646
|
-
|
|
647
|
-
Here's a short and sweet echo server - No HTTP, just use `telnet`:
|
|
648
|
-
|
|
649
|
-
```ruby
|
|
650
|
-
USE_TLS = false
|
|
651
|
-
|
|
652
|
-
require 'iodine'
|
|
653
|
-
|
|
654
|
-
# an echo protocol with asynchronous notifications.
|
|
655
|
-
class EchoProtocol
|
|
656
|
-
# `on_message` is called when data is available.
|
|
657
|
-
def on_message client, buffer
|
|
658
|
-
# writing will never block and will use a buffer written in C when needed.
|
|
659
|
-
client.write buffer
|
|
660
|
-
# close will be performed only once all the data in the write buffer
|
|
661
|
-
# was sent. use `force_close` to close early.
|
|
662
|
-
client.close if buffer =~ /^bye[\r\n]/i
|
|
663
|
-
# run asynchronous tasks... after a set number of milliseconds
|
|
664
|
-
Iodine.run_after(1000) do
|
|
665
|
-
# or schedule the task immediately
|
|
666
|
-
Iodine.run do
|
|
667
|
-
puts "Echoed data: #{buffer}"
|
|
668
|
-
end
|
|
669
|
-
end
|
|
670
|
-
end
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
tls = USE_TLS ? Iodine::TLS.new("localhost") : nil
|
|
674
|
-
|
|
675
|
-
# listen on port 3000 for the echo protocol.
|
|
676
|
-
Iodine.listen(port: "3000", tls: tls) { EchoProtocol.new }
|
|
677
|
-
Iodine.threads = 1
|
|
678
|
-
Iodine.workers = 1
|
|
679
|
-
Iodine.start
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
Or a nice plain text chat room (connect using `telnet` or `nc` ):
|
|
683
|
-
|
|
684
|
-
```ruby
|
|
685
|
-
require 'iodine'
|
|
686
|
-
|
|
687
|
-
# a chat protocol with asynchronous notifications.
|
|
688
|
-
class ChatProtocol
|
|
689
|
-
def initialize nickname = "guest"
|
|
690
|
-
@nickname = nickname
|
|
691
|
-
end
|
|
692
|
-
def on_open client
|
|
693
|
-
client.subscribe :chat
|
|
694
|
-
client.publish :chat, "#{@nickname} joined chat.\n"
|
|
695
|
-
client.timeout = 40
|
|
696
|
-
end
|
|
697
|
-
def on_close client
|
|
698
|
-
client.publish :chat, "#{@nickname} left chat.\n"
|
|
699
|
-
end
|
|
700
|
-
def on_shutdown client
|
|
701
|
-
client.write "Server is shutting down... try reconnecting later.\n"
|
|
702
|
-
end
|
|
703
|
-
def on_message client, buffer
|
|
704
|
-
if(buffer[-1] == "\n")
|
|
705
|
-
client.publish :chat, "#{@nickname}: #{buffer}"
|
|
706
|
-
else
|
|
707
|
-
client.publish :chat, "#{@nickname}: #{buffer}\n"
|
|
708
|
-
end
|
|
709
|
-
# close will be performed only once all the data in the outgoing buffer
|
|
710
|
-
client.close if buffer =~ /^bye[\r\n]/i
|
|
711
|
-
end
|
|
712
|
-
def ping client
|
|
713
|
-
client.write "(ping) Are you there, #{@nickname}...?\n"
|
|
714
|
-
end
|
|
715
|
-
end
|
|
716
|
-
|
|
717
|
-
# an initial login protocol
|
|
718
|
-
class LoginProtocol
|
|
719
|
-
def on_open client
|
|
720
|
-
client.write "Enter nickname to log in to chat room:\n"
|
|
721
|
-
client.timeout = 10
|
|
722
|
-
end
|
|
723
|
-
def ping client
|
|
724
|
-
client.write "Time's up... goodbye.\n"
|
|
725
|
-
client.close
|
|
726
|
-
end
|
|
727
|
-
def on_message client, buffer
|
|
728
|
-
# validate nickname and switch connection callback to ChatProtocol
|
|
729
|
-
nickname = buffer.split("\n")[0]
|
|
730
|
-
while (nickname && nickname.length() > 0 && (nickname[-1] == '\n' || nickname[-1] == '\r'))
|
|
731
|
-
nickname = nickname.slice(0, nickname.length() -1)
|
|
732
|
-
end
|
|
733
|
-
if(nickname && nickname.length() > 0 && buffer.split("\n").length() == 1)
|
|
734
|
-
chat = ChatProtocol.new(nickname)
|
|
735
|
-
client.handler = chat
|
|
736
|
-
else
|
|
737
|
-
client.write "Nickname error, try again.\n"
|
|
738
|
-
on_open client
|
|
739
|
-
end
|
|
740
|
-
end
|
|
741
|
-
end
|
|
742
|
-
|
|
743
|
-
# listen on port 3000
|
|
744
|
-
Iodine.listen(port: 3000) { LoginProtocol.new }
|
|
745
|
-
Iodine.threads = 1
|
|
746
|
-
Iodine.workers = 1
|
|
747
|
-
Iodine.start
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
### Why not EventMachine?
|
|
751
|
-
|
|
752
|
-
EventMachine attempts to give the developer access to the network layer while Iodine attempts to abstract the network layer away and offer the developer a distraction free platform.
|
|
753
|
-
|
|
754
|
-
You can go ahead and use EventMachine if you like. They're doing amazing work on that one and it's been used a lot in Ruby-land... really, tons of good developers and people on that project.
|
|
755
|
-
|
|
756
|
-
But why not take iodine out for a spin and see for yourself?
|
|
757
|
-
|
|
758
|
-
## Can I contribute?
|
|
759
|
-
|
|
760
|
-
Yes, please, here are some thoughts:
|
|
761
|
-
|
|
762
|
-
* I'm really not good at writing automated tests and benchmarks, any help would be appreciated. I keep testing manually and that's less then ideal (and it's mistake prone).
|
|
763
|
-
|
|
764
|
-
* PRs or issues related to [the `facil.io` C framework](https://github.com/boazsegev/facil.io) should be placed in [the `facil.io` repository](https://github.com/boazsegev/facil.io).
|
|
765
|
-
|
|
766
|
-
* Bug reports and pull requests are welcome on GitHub at https://github.com/boazsegev/iodine.
|
|
767
|
-
|
|
768
|
-
* If we can write a Java wrapper for [the `facil.io` C framework](https://github.com/boazsegev/facil.io), it would be nice... but it could be as big a project as the whole gem, as a lot of minor details are implemented within the bridge between these two languages.
|
|
769
|
-
|
|
770
|
-
* If you love the project or thought the code was nice, maybe helped you in your own project, drop me a line. I'd love to know.
|
|
771
|
-
|
|
772
|
-
### Running the Tests
|
|
773
|
-
|
|
774
|
-
Running this task will compile the C extensions then run RSpec tests:
|
|
775
|
-
|
|
776
|
-
```sh
|
|
777
|
-
bundle exec rake spec
|
|
778
|
-
```
|
|
252
|
+
- **API reference:** [rubydoc.info](http://www.rubydoc.info/github/boazsegev/iodine) — full details for the RESP3 engine commands, the TLS backend selection API, Pub/Sub delivery options, and protocol limits.
|
|
253
|
+
- **Iodine:** [GitHub issues](https://github.com/boazsegev/iodine/issues)
|
|
254
|
+
- **facil.io C core:** [facil-io/cstl](https://github.com/facil-io/cstl)
|
|
779
255
|
|
|
780
|
-
|
|
256
|
+
---
|
|
781
257
|
|
|
782
|
-
|
|
258
|
+
Try it on your own app: install Iodine, switch the server command, and run a benchmark.
|