iodine 0.5.2 → 0.6.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/CHANGELOG.md +14 -0
- data/README.md +63 -100
- data/bin/raw-rbhttp +12 -7
- data/examples/config.ru +8 -7
- data/examples/echo.ru +8 -7
- data/examples/info.md +41 -35
- data/examples/pubsub_engine.ru +12 -12
- data/examples/redis.ru +10 -12
- data/examples/shootout.ru +19 -42
- data/exe/iodine +116 -1
- data/ext/iodine/defer.c +1 -1
- data/ext/iodine/facil.c +12 -8
- data/ext/iodine/facil.h +2 -2
- data/ext/iodine/iodine.c +177 -343
- data/ext/iodine/iodine.h +18 -72
- data/ext/iodine/iodine_caller.c +132 -0
- data/ext/iodine/iodine_caller.h +21 -0
- data/ext/iodine/iodine_connection.c +841 -0
- data/ext/iodine/iodine_connection.h +55 -0
- data/ext/iodine/iodine_defer.c +391 -0
- data/ext/iodine/iodine_defer.h +7 -0
- data/ext/iodine/{rb-fiobj2rb.h → iodine_fiobj2rb.h} +6 -6
- data/ext/iodine/iodine_helpers.c +51 -5
- data/ext/iodine/iodine_helpers.h +2 -3
- data/ext/iodine/iodine_http.c +284 -141
- data/ext/iodine/iodine_http.h +2 -2
- data/ext/iodine/iodine_json.c +13 -13
- data/ext/iodine/iodine_json.h +1 -1
- data/ext/iodine/iodine_pubsub.c +573 -823
- data/ext/iodine/iodine_pubsub.h +15 -27
- data/ext/iodine/{rb-rack-io.c → iodine_rack_io.c} +30 -8
- data/ext/iodine/{rb-rack-io.h → iodine_rack_io.h} +1 -0
- data/ext/iodine/iodine_store.c +136 -0
- data/ext/iodine/iodine_store.h +20 -0
- data/ext/iodine/iodine_tcp.c +385 -0
- data/ext/iodine/iodine_tcp.h +9 -0
- data/lib/iodine.rb +73 -171
- data/lib/iodine/connection.rb +34 -0
- data/lib/iodine/pubsub.rb +5 -18
- data/lib/iodine/rack_utils.rb +43 -0
- data/lib/iodine/version.rb +1 -1
- data/lib/rack/handler/iodine.rb +1 -182
- metadata +17 -18
- data/ext/iodine/iodine_protocol.c +0 -689
- data/ext/iodine/iodine_protocol.h +0 -13
- data/ext/iodine/iodine_websockets.c +0 -550
- data/ext/iodine/iodine_websockets.h +0 -17
- data/ext/iodine/rb-call.c +0 -156
- data/ext/iodine/rb-call.h +0 -70
- data/ext/iodine/rb-defer.c +0 -124
- data/ext/iodine/rb-registry.c +0 -150
- data/ext/iodine/rb-registry.h +0 -34
- data/lib/iodine/cli.rb +0 -89
- data/lib/iodine/monkeypatch.rb +0 -46
- data/lib/iodine/protocol.rb +0 -42
- data/lib/iodine/websocket.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c633ef1f40df47f2edefcd78b2bd3fc2a896ab7a704b298f42765d605b375947
|
4
|
+
data.tar.gz: e810cccda11b73c2c4881fd90e4e0132efbf62c11857edee457381cb700c6302
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e376b5ca69fb98da82062508687cd2bdaf7bb55b2071559d23c2f5eb330cd7fc9f7856865a86cb382357106835027e5296a8f0c67113a0522dd1a6ac9323bbf
|
7
|
+
data.tar.gz: ecd04e8c645961e8df5e4bfa7e2666a71a81dfda1647fe7e43c27894abb9431010663821d36a99bca23727cb0077f6f00144e0cd563a2a34ba9426ab80bb24cb
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,20 @@ 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.6.0
|
10
|
+
|
11
|
+
I apologize to all my amazing early adopters for the rapid changes in the API for connection objects (SSE / WebSockets) and Pub/Sub. This was a result of an attempt to create a de-facto standard with other server authors. Hopefully the API in the 0.6.0 release will see the last of the changes.
|
12
|
+
|
13
|
+
**API BREAKING CHANGE**: The API for persistent connections (SSE / WebSockets) was drastically changed in accordance with the Rack specification discussion that required each callback to accept a "client" object (replacing the `extend` approach). Please see the documentation.
|
14
|
+
|
15
|
+
**API BREAKING CHANGE**: `Iodine.attach` was removed due to instability and issues regarding TLS/SSL and file system IO. I hope to fix these issues in a future release. For now the `Iodine.attach_fd` can be used for clear-text sockets and pipes.
|
16
|
+
|
17
|
+
**API BREAKING CHANGE**: Pub/Sub API was changed, replacing the previously suggested pub/sub object with an updated `unsubscribe` method. This means there's no need for the client to map channel names to specific subscriptions (Iodine will perform this housekeeping task for the client).
|
18
|
+
|
19
|
+
**Fix**: Iodine should now build correctly on FreeBSD. Credit to @adam12 (Adam Daniels) for detecting the issue.
|
20
|
+
|
21
|
+
---
|
22
|
+
|
9
23
|
#### Change log v.0.5.2
|
10
24
|
|
11
25
|
**Fix**: fixed compilation issues on FreeBSD. Credit to @adam12 (Adam Daniels) for opening issue #35 and offering a patch.
|
data/README.md
CHANGED
@@ -21,33 +21,17 @@ Iodine is a fast concurrent web server for real-time Ruby applications, with nat
|
|
21
21
|
|
22
22
|
Iodine is an **evented** framework with a simple API that builds off the low level [C code library facil.io](https://github.com/boazsegev/facil.io) with support for **epoll** and **kqueue** - this means that:
|
23
23
|
|
24
|
-
* Iodine can handle **thousands of concurrent connections** (tested with more then 20K connections)!
|
24
|
+
* Iodine can handle **thousands of concurrent connections** (tested with more then 20K connections on Linux)!
|
25
25
|
|
26
|
-
* Iodine supports only **Linux/Unix** based systems (i.e. macOS, Ubuntu, FreeBSD etc'), which are ideal for evented IO (while Windows and Solaris are better at IO *completion* events, which are totally different).
|
26
|
+
* Iodine supports only **Linux/Unix** based systems (i.e. macOS, Ubuntu, FreeBSD etc'), which are ideal for evented IO (while Windows and Solaris are better at IO *completion* events, which are totally different).
|
27
27
|
|
28
28
|
Iodine is a C extension for Ruby, developed and optimized for Ruby MRI 2.2.2 and up... it should support the whole Ruby 2.0 MRI family, but Rack requires Ruby 2.2.2, and so iodine matches this requirement.
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
## Important Note about API Changes and Standardization
|
33
|
-
|
34
|
-
I am very thankful to the many early adopters of iodine using the WebSocket and Pub/Sub API.
|
35
|
-
|
36
|
-
Please note that Iodine 0.5.0's API is a temporary API that was part of an attempt to come up with a de facto Rack standard for WebSocket / SSE connectivity.
|
37
|
-
|
38
|
-
Sadly, this standard isn't progressing as well as I had hoped. More API changes were requested and it seems that the PR with the finalized API is still being considered ([please vote for the PR!](https://github.com/rack/rack/pull/1272)).
|
39
|
-
|
40
|
-
I'm working on Iodine 0.6.0 with the requested updates to the API, along with an updated Pub/Sub API that should fit better with existing pub/sub approaches such ActionCable.
|
41
|
-
|
42
|
-
It's my sincere hope that the upcoming API changes in 0.6.0 will be the last and that this will signal the API's maturity as well as general acceptance.
|
43
|
-
|
44
|
-
---
|
45
|
-
|
46
|
-
## Iodine::Rack == fast & powerful HTTP + Websockets server with native Pub/Sub
|
30
|
+
## Iodine - a fast & powerful HTTP + Websockets server with native Pub/Sub
|
47
31
|
|
48
32
|
Iodine includes a light and fast HTTP and Websocket server written in C that was written according to the [Rack interface specifications](http://www.rubydoc.info/github/rack/rack/master/file/SPEC) and the [Websocket draft extension](./SPEC-Websocket-Draft.md).
|
49
33
|
|
50
|
-
With `Iodine.listen2http` it's possible to run multiple HTTP applications
|
34
|
+
With `Iodine.listen2http` it's possible to run multiple HTTP applications (please remember not to set more than a single application on a single TCP/IP port).
|
51
35
|
|
52
36
|
Iodine also supports native process cluster Pub/Sub and a native RedisEngine to easily scale iodine's Pub/Sub horizontally.
|
53
37
|
|
@@ -56,7 +40,7 @@ Iodine also supports native process cluster Pub/Sub and a native RedisEngine to
|
|
56
40
|
Using the iodine server is easy, simply add iodine as a gem to your Rack application:
|
57
41
|
|
58
42
|
```ruby
|
59
|
-
gem 'iodine', '~>0.
|
43
|
+
gem 'iodine', '~>0.6'
|
60
44
|
```
|
61
45
|
|
62
46
|
Iodine will calculate, when possible, a good enough default concurrency model for lightweight applications... this might not fit your application if you use heavier database access or other blocking calls.
|
@@ -90,11 +74,10 @@ Or by adding a single line to the application. i.e. (a `config.ru` example):
|
|
90
74
|
```ruby
|
91
75
|
require 'iodine'
|
92
76
|
# static file service
|
93
|
-
Iodine
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
run app
|
77
|
+
Iodine.listen2http public: '/my/public/folder'
|
78
|
+
# for static file service, we only need a single thread per worker.
|
79
|
+
Iodine.threads = 1
|
80
|
+
Iodine.start
|
98
81
|
```
|
99
82
|
|
100
83
|
To enable logging from the command line, use the `-v` (verbose) option:
|
@@ -117,7 +100,7 @@ i.e. (example `config.ru` for iodine):
|
|
117
100
|
app = proc do |env|
|
118
101
|
request = Rack::Request.new(env)
|
119
102
|
if request.path_info == '/source'.freeze
|
120
|
-
[200, { 'X-Sendfile' => File.expand_path(__FILE__) }, []]
|
103
|
+
[200, { 'X-Sendfile' => File.expand_path(__FILE__), 'Content-Type' => 'text/plain'}, []]
|
121
104
|
elsif request.path_info == '/file'.freeze
|
122
105
|
[200, { 'X-Header' => 'This was a Rack::Sendfile response sent as text.' }, File.open(__FILE__)]
|
123
106
|
else
|
@@ -175,25 +158,26 @@ Here is a simple chat-room example we can run in the terminal (`irb`) or easily
|
|
175
158
|
|
176
159
|
```ruby
|
177
160
|
require 'iodine'
|
178
|
-
|
179
|
-
def on_open
|
161
|
+
module WebsocketChat
|
162
|
+
def on_open client
|
180
163
|
# Pub/Sub directly to the client (or use a block to process the messages)
|
181
|
-
subscribe :chat
|
164
|
+
client.subscribe :chat
|
182
165
|
# Writing directly to the socket
|
183
|
-
write "You're now in the chatroom."
|
166
|
+
client.write "You're now in the chatroom."
|
184
167
|
end
|
185
|
-
def on_message data
|
168
|
+
def on_message client, data
|
186
169
|
# Strings and symbol channel names are equivalent.
|
187
|
-
publish "chat", data
|
170
|
+
client.publish "chat", data
|
188
171
|
end
|
172
|
+
extend self
|
189
173
|
end
|
190
|
-
|
174
|
+
APP = Proc.new do |env|
|
191
175
|
if env['rack.upgrade?'.freeze] == :websocket
|
192
|
-
env['rack.upgrade'.freeze] = WebsocketChat
|
176
|
+
env['rack.upgrade'.freeze] = WebsocketChat
|
193
177
|
[0,{}, []] # It's possible to set cookies for the response.
|
194
178
|
elsif env['rack.upgrade?'.freeze] == :sse
|
195
179
|
puts "SSE connections can only receive data from the server, the can't write."
|
196
|
-
env['rack.upgrade'.freeze] = WebsocketChat
|
180
|
+
env['rack.upgrade'.freeze] = WebsocketChat
|
197
181
|
[0,{}, []] # It's possible to set cookies for the response.
|
198
182
|
else
|
199
183
|
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
|
@@ -203,11 +187,12 @@ end
|
|
203
187
|
root_pid = Process.pid
|
204
188
|
Iodine.subscribe(:chat) {|ch, msg| puts msg if Process.pid == root_pid }
|
205
189
|
# By default, Pub/Sub performs in process cluster mode.
|
206
|
-
Iodine.
|
207
|
-
#
|
208
|
-
Iodine
|
209
|
-
#
|
190
|
+
Iodine.workers = 4
|
191
|
+
# # in irb:
|
192
|
+
Iodine.listen2http public: "www/public", app: APP
|
210
193
|
Iodine.start
|
194
|
+
# # or in config.ru
|
195
|
+
run APP
|
211
196
|
```
|
212
197
|
|
213
198
|
#### Native Pub/Sub with *optional* Redis scaling
|
@@ -217,24 +202,21 @@ Iodine's core, `facil.io` offers a native Pub/Sub implementation. The implementa
|
|
217
202
|
Here's an example that adds horizontal scaling to the chat application in the previous example, so that Pub/Sub messages are published across many machines at once:
|
218
203
|
|
219
204
|
```ruby
|
220
|
-
require 'uri'
|
221
205
|
# initialize the Redis engine for each iodine process.
|
222
206
|
if ENV["REDIS_URL"]
|
223
|
-
|
224
|
-
Iodine.default_pubsub = Iodine::PubSub::RedisEngine.new(uri.host, uri.port, 0, uri.password)
|
207
|
+
Iodine::PubSub.default = Iodine::PubSub::Redis.new(ENV["REDIS_URL"])
|
225
208
|
else
|
226
209
|
puts "* No Redis, it's okay, pub/sub will still run on the whole process cluster."
|
227
210
|
end
|
228
|
-
|
229
|
-
# ... the rest of the application remain unchanged.
|
211
|
+
# ... the rest of the application remains unchanged.
|
230
212
|
```
|
231
213
|
|
232
214
|
The new Redis client can also be used for asynchronous Redis command execution. i.e.:
|
233
215
|
|
234
216
|
```ruby
|
235
|
-
if(Iodine.
|
217
|
+
if(Iodine.default.is_a? Iodine::PubSub::Redis)
|
236
218
|
# Ask Redis about all it's client connections and print out the reply.
|
237
|
-
Iodine.
|
219
|
+
Iodine.default.cmd("CLIENT LIST") { |reply| puts reply }
|
238
220
|
end
|
239
221
|
```
|
240
222
|
|
@@ -246,55 +228,62 @@ end
|
|
246
228
|
|
247
229
|
#### TCP/IP (raw) sockets
|
248
230
|
|
249
|
-
Upgrading to a custom protocol (i.e., in order to implement your own Websocket protocol with special extensions) is
|
231
|
+
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:
|
250
232
|
|
251
233
|
```ruby
|
252
234
|
require 'iodine'
|
253
235
|
class MyProtocol
|
254
|
-
def on_message data
|
255
|
-
# regular socket echo - NOT websockets
|
256
|
-
write data
|
236
|
+
def on_message client, data
|
237
|
+
# regular socket echo - NOT websockets
|
238
|
+
client.write data
|
257
239
|
end
|
258
240
|
end
|
259
|
-
|
260
|
-
if env[
|
261
|
-
env['upgrade.tcp'.freeze] = MyProtocol
|
262
|
-
#
|
263
|
-
|
264
|
-
[1000,{}, []]
|
241
|
+
APP = Proc.new do |env|
|
242
|
+
if env["HTTP_UPGRADE".freeze] =~ /echo/i.freeze
|
243
|
+
env['upgrade.tcp'.freeze] = MyProtocol.new
|
244
|
+
# an HTTP response will be sent before changing protocols.
|
245
|
+
[101, { "Upgrade" => "echo" }, []]
|
265
246
|
else
|
266
247
|
[200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Welcome Home"] ]
|
267
248
|
end
|
268
249
|
end
|
250
|
+
# # in irb:
|
251
|
+
Iodine.listen2http public: "www/public", app: APP
|
252
|
+
Iodine.threads = 1
|
269
253
|
Iodine.start
|
254
|
+
# # or in config.ru
|
255
|
+
run APP
|
270
256
|
```
|
271
257
|
|
272
258
|
#### A few notes
|
273
259
|
|
274
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.
|
275
261
|
|
276
|
-
Iodine
|
262
|
+
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.
|
277
263
|
|
278
|
-
|
264
|
+
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?).
|
279
265
|
|
280
266
|
### How does it compare to other servers?
|
281
267
|
|
282
|
-
|
268
|
+
The honest answer is "I don't know". I recommend that you perform your own tests.
|
283
269
|
|
284
|
-
|
270
|
+
In my tests, pitching Iodine against Puma, Iodine was anywhere between x1.5 and x7 faster than Puma (depending on use-case). such a big difference is suspect and I recommend that you test it yourself.
|
285
271
|
|
286
|
-
|
272
|
+
Also, performing benchmarks on a single machine isn't very reliable... but it's all I've got.
|
287
273
|
|
288
|
-
|
274
|
+
When benchmarking with `wrk`, on the same local machine with similar settings for both Puma and Iodine (4 workers, 16 threads each, 200 concurrent connections), I calculated Iodine to be x1.52 faster::
|
289
275
|
|
276
|
+
* Iodine performed at 74,786.27 req/sec, consuming ~68.4Mb of memory.
|
290
277
|
|
291
|
-
|
278
|
+
* Puma performed at 48,994.59 req/sec, consuming ~79.6Mb of memory.
|
279
|
+
|
280
|
+
When benchmarking using a VM (crossing machine boundaries, 16 threads, 4 workers, 200 concurrent connections), I calculated Iodine to be x2.3 faster:
|
292
281
|
|
293
|
-
* Iodine performed at
|
282
|
+
* Iodine performed at 23,559.56 req/sec, consuming ~88.8Mb of memory.
|
294
283
|
|
295
|
-
* Puma performed at
|
284
|
+
* Puma performed at 9,935.31 req/sec, consuming ~84.0Mb of memory.
|
296
285
|
|
297
|
-
When benchmarking using a VM (crossing machine boundaries, single thread, single worker, 200 concurrent connections),
|
286
|
+
When benchmarking using a VM (crossing machine boundaries, single thread, single worker, 200 concurrent connections), I calculated Iodine to be x7.3 faster:
|
298
287
|
|
299
288
|
* Iodine performed at 18,444.31 req/sec, consuming ~25.6Mb of memory.
|
300
289
|
|
@@ -331,6 +320,8 @@ $ RACK_ENV=production puma -p 3000 -t 16 -w 4
|
|
331
320
|
# Review the `iodine -?` help for more command line options.
|
332
321
|
```
|
333
322
|
|
323
|
+
It's recommended that the servers (Iodine/Puma) and the client (`wrk`/`ab`) run on separate machines.
|
324
|
+
|
334
325
|
### Performance oriented design - but safety first
|
335
326
|
|
336
327
|
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.
|
@@ -424,11 +415,11 @@ Iodine.start
|
|
424
415
|
|
425
416
|
## Why not EventMachine?
|
426
417
|
|
427
|
-
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
|
418
|
+
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.
|
428
419
|
|
429
|
-
|
420
|
+
EventMachine also offers some really great optimization features and it was vastly improved upon in the last few years (When I started Iodine, it was far more annoying to work with).
|
430
421
|
|
431
|
-
|
422
|
+
But there's a distinct approach difference for me. EventMachine attempts to give the developer access to the network layer while Iodine attempts to abstract the network layer away.
|
432
423
|
|
433
424
|
Besides, you're here - why not take iodine out for a spin and see for yourself?
|
434
425
|
|
@@ -438,42 +429,14 @@ Yes, please, here are some thoughts:
|
|
438
429
|
|
439
430
|
* 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).
|
440
431
|
|
441
|
-
* 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.
|
442
|
-
|
443
432
|
* 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).
|
444
433
|
|
445
434
|
* Bug reports and pull requests are welcome on GitHub at https://github.com/boazsegev/iodine.
|
446
435
|
|
436
|
+
* 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.
|
437
|
+
|
447
438
|
* 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.
|
448
439
|
|
449
440
|
## License
|
450
441
|
|
451
442
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
452
|
-
|
453
|
-
---
|
454
|
-
|
455
|
-
## "I'm also writing a Ruby extension in C"
|
456
|
-
|
457
|
-
Really?! That's great!
|
458
|
-
|
459
|
-
We could all use some more documentation around the subject and having an eco-system for extension tidbits would be nice.
|
460
|
-
|
461
|
-
Here's a few things you can use from this project and they seem to be handy to have (and easy to port):
|
462
|
-
|
463
|
-
* Iodine is using a [Registry](https://github.com/boazsegev/iodine/blob/0.2.0/ext/core/rb-registry.h) to keep dynamic Ruby objects that are owned by C-land from being collected by the garbage collector in Ruby-land...
|
464
|
-
|
465
|
-
Some people use global Ruby arrays, adding and removing Ruby objects to the array, but that sounds like a performance hog to me.
|
466
|
-
|
467
|
-
This one is a simple binary tree with a Ruby GC callback. Remember to initialize the Registry (`Registry.init(owner)`) so it's "owned" by some Ruby-land object, this allows it to bridge the two worlds for the GC's mark and sweep.
|
468
|
-
|
469
|
-
I'm attaching it to one of iodine's library classes, just in-case someone adopts my code and decides the registry should be owned by the global Object class.
|
470
|
-
|
471
|
-
* I was using a POSIX thread pool library ([`defer.h`](https://github.com/boazsegev/facil.io/blob/master/lib/facil/core/defer.c)) until I realized how many issues Ruby has with non-Ruby threads... So now there's a Ruby-thread patch for this library at ([`rb-defer.c`](https://github.com/boazsegev/iodine/blob/master/ext/iodine/rb-defer.c)).
|
472
|
-
|
473
|
-
Notice that all the new threads are free from the GVL - this allows true concurrency... but, you can't make Ruby API calls in that state.
|
474
|
-
|
475
|
-
To perform Ruby API calls you need to re-enter the global lock (GVL), albeit temporarily, using `rb_thread_call_with_gvl` and `rv_protect` (gotta watch out from Ruby `longjmp` exceptions).
|
476
|
-
|
477
|
-
* Since I needed to call Ruby methods while multi-threading and running outside the GVL, I wrote [`RubyCaller`](https://github.com/boazsegev/iodine/blob/0.2.0/ext/core/rb-call.h) which let's me call an object's method and wraps all the `rb_thread_call_with_gvl` and `rb_protect` details in a secret hidden place I never have to see again. It also keeps track of the thread's state, so if we're already within the GVL, we won't enter it "twice" (which could crash Ruby sporadically).
|
478
|
-
|
479
|
-
These are nice code snippets that can be easily used in other extensions. They're easy enough to write, I guess, but I already did the legwork, so enjoy.
|
data/bin/raw-rbhttp
CHANGED
@@ -14,20 +14,25 @@ $LOAD_PATH.unshift File.expand_path(File.join('..', '..', 'lib'), __FILE__)
|
|
14
14
|
require 'bundler/setup'
|
15
15
|
require 'iodine'
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
module HttpProtocol
|
18
|
+
def self.on_open client
|
19
|
+
client.timeout = 10
|
20
|
+
end
|
19
21
|
# `on_message` is an optional alternative to the `on_data` callback.
|
20
22
|
# `on_message` has a 1Kb buffer that recycles itself for memory optimization.
|
21
|
-
def on_message _
|
23
|
+
def self.on_message client, _
|
22
24
|
# writing will never block and will use a buffer written in C when needed.
|
23
|
-
write "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nKeep-Alive: timeout=1\r\nContent-Length: 12\r\n\r\nHello World!"
|
25
|
+
client.write "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nKeep-Alive: timeout=1\r\nContent-Length: 12\r\n\r\nHello World!"
|
26
|
+
end
|
27
|
+
def self.call
|
28
|
+
self
|
24
29
|
end
|
25
30
|
end
|
26
31
|
|
27
32
|
puts "thread #{Iodine.threads}"
|
28
33
|
# Listen
|
29
|
-
Iodine.listen '3000', HttpProtocol
|
30
|
-
Iodine.threads =
|
31
|
-
Iodine.
|
34
|
+
Iodine.listen port: '3000', handler: HttpProtocol
|
35
|
+
Iodine.threads = 4
|
36
|
+
Iodine.workers = 1
|
32
37
|
puts "now, thread #{Iodine.threads}"
|
33
38
|
Iodine.start
|
data/examples/config.ru
CHANGED
@@ -36,19 +36,20 @@ module MyHTTPRouter
|
|
36
36
|
end
|
37
37
|
|
38
38
|
# A simple Websocket Callback Object.
|
39
|
-
|
39
|
+
module BroadcastClient
|
40
40
|
# seng a message to new clients.
|
41
|
-
def on_open
|
42
|
-
subscribe :broadcast
|
41
|
+
def on_open client
|
42
|
+
client.subscribe :broadcast
|
43
43
|
end
|
44
44
|
# send a message, letting the client know the server is suggunt down.
|
45
|
-
def on_shutdown
|
46
|
-
write "Server shutting down. Goodbye."
|
45
|
+
def on_shutdown client
|
46
|
+
client.write "Server shutting down. Goodbye."
|
47
47
|
end
|
48
48
|
# perform the echo
|
49
|
-
def on_message data
|
50
|
-
publish :broadcast, data
|
49
|
+
def on_message client, data
|
50
|
+
client.publish :broadcast, data
|
51
51
|
end
|
52
|
+
extend self
|
52
53
|
end
|
53
54
|
|
54
55
|
# this function call links our HelloWorld application with Rack
|
data/examples/echo.ru
CHANGED
@@ -39,19 +39,20 @@ module MyHTTPRouter
|
|
39
39
|
end
|
40
40
|
|
41
41
|
# A simple Websocket Callback Object.
|
42
|
-
|
42
|
+
module WebsocketEcho
|
43
43
|
# seng a message to new clients.
|
44
|
-
def on_open
|
45
|
-
write "Welcome to our echo service!"
|
44
|
+
def on_open client
|
45
|
+
client.write "Welcome to our echo service!"
|
46
46
|
end
|
47
47
|
# send a message, letting the client know the server is suggunt down.
|
48
|
-
def on_shutdown
|
49
|
-
write "Server shutting down. Goodbye."
|
48
|
+
def on_shutdown client
|
49
|
+
client.write "Server shutting down. Goodbye."
|
50
50
|
end
|
51
51
|
# perform the echo
|
52
|
-
def on_message data
|
53
|
-
write data
|
52
|
+
def on_message client, data
|
53
|
+
client.write data
|
54
54
|
end
|
55
|
+
extend self
|
55
56
|
end
|
56
57
|
|
57
58
|
# this function call links our HelloWorld application with Rack
|
data/examples/info.md
CHANGED
@@ -82,7 +82,7 @@ By using a callback object, the application is notified of any events. Leaving t
|
|
82
82
|
|
83
83
|
The callback object doesn't even need to know anything about the server running the application or the underlying protocol.
|
84
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.
|
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
86
|
|
87
87
|
### How it works
|
88
88
|
|
@@ -108,7 +108,7 @@ Normally, this variable is set to `nil` (or missing from the `env` Hash).
|
|
108
108
|
|
109
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
110
|
|
111
|
-
To set a callback object, the `env['rack.upgrade']` is introduced (notice the missing question mark).
|
111
|
+
To set a callback object, the `env['rack.upgrade']` is introduced (notice the *missing* question mark).
|
112
112
|
|
113
113
|
Now the design might look like this:
|
114
114
|
|
@@ -116,15 +116,16 @@ Now the design might look like this:
|
|
116
116
|
# Place in config.ru
|
117
117
|
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
118
118
|
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
119
|
-
#
|
119
|
+
# an example Callback class
|
120
120
|
class MyCallbacks
|
121
|
-
def on_open
|
121
|
+
def on_open client
|
122
122
|
puts "* Push connection opened."
|
123
123
|
end
|
124
|
-
def on_message data
|
124
|
+
def on_message client, data
|
125
125
|
puts "* Incoming data: #{data}"
|
126
|
+
client.write "Roger that, \"#{data}\""
|
126
127
|
end
|
127
|
-
def on_close
|
128
|
+
def on_close client
|
128
129
|
puts "* Push connection closed."
|
129
130
|
end
|
130
131
|
end
|
@@ -146,7 +147,7 @@ Run this application with the Agoo or Iodine servers and let the magic sparkle.
|
|
146
147
|
For example, using Iodine:
|
147
148
|
|
148
149
|
```bash
|
149
|
-
# install iodine
|
150
|
+
# install iodine, version 0.6.0 and up
|
150
151
|
gem install iodine
|
151
152
|
# start in single threaded mode
|
152
153
|
iodine -t 1
|
@@ -185,7 +186,7 @@ And this same example will run perfectly using the Agoo server as well (both Ago
|
|
185
186
|
Try it:
|
186
187
|
|
187
188
|
```bash
|
188
|
-
# install the agoo server
|
189
|
+
# install the agoo server, version 2.1.0 and up
|
189
190
|
gem install agoo
|
190
191
|
# start it up
|
191
192
|
rackup -s agoo -p 3000
|
@@ -204,14 +205,14 @@ Consider implementing a stock ticker, or in this case, a timer:
|
|
204
205
|
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
205
206
|
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
206
207
|
|
207
|
-
# A live connection storage
|
208
|
+
# A global live connection storage
|
208
209
|
module LiveList
|
209
210
|
@list = []
|
210
211
|
@lock = Mutex.new
|
211
|
-
def
|
212
|
+
def <<(connection)
|
212
213
|
@lock.synchronize { @list << connection }
|
213
214
|
end
|
214
|
-
def
|
215
|
+
def >>(connection)
|
215
216
|
@lock.synchronize { @list.delete connection }
|
216
217
|
end
|
217
218
|
def any?
|
@@ -220,17 +221,16 @@ module LiveList
|
|
220
221
|
end
|
221
222
|
# this will send a message to all the connections that share the same process.
|
222
223
|
# (in cluster mode we get partial broadcasting only and this doesn't scale)
|
223
|
-
def
|
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
|
224
227
|
@lock.synchronize do
|
225
|
-
@list.
|
226
|
-
begin
|
227
|
-
c.write data
|
228
|
-
rescue IOError => _e
|
229
|
-
# An IOError can occur if the connection was closed during the loop.
|
230
|
-
end
|
231
|
-
end
|
228
|
+
tmp = @list.dup # copy list into tmp
|
232
229
|
end
|
230
|
+
# iterate list outside of critical section
|
231
|
+
tmp.each {|c| c.write data }
|
233
232
|
end
|
233
|
+
extend self
|
234
234
|
end
|
235
235
|
|
236
236
|
# Broadcast the time very second... but...
|
@@ -242,26 +242,27 @@ end
|
|
242
242
|
end
|
243
243
|
end
|
244
244
|
|
245
|
-
#
|
246
|
-
|
247
|
-
def on_open
|
245
|
+
# an example static Callback module
|
246
|
+
module MyCallbacks
|
247
|
+
def on_open client
|
248
248
|
# add connection to the "live list"
|
249
|
-
LiveList <<
|
249
|
+
LiveList << client
|
250
250
|
end
|
251
|
-
def on_message(data)
|
251
|
+
def on_message(client, data)
|
252
252
|
# Just an example broadcast
|
253
253
|
LiveList.broadcast "Special Announcement: #{data}"
|
254
254
|
end
|
255
|
-
def on_close
|
255
|
+
def on_close client
|
256
256
|
# remove connection to the "live list"
|
257
|
-
LiveList >>
|
257
|
+
LiveList >> client
|
258
258
|
end
|
259
|
+
extend self
|
259
260
|
end
|
260
261
|
|
261
262
|
# The Rack application
|
262
263
|
APP = Proc.new do |env|
|
263
264
|
if(env['rack.upgrade?'])
|
264
|
-
env['rack.upgrade'] = MyCallbacks
|
265
|
+
env['rack.upgrade'] = MyCallbacks
|
265
266
|
[200, {}, []]
|
266
267
|
else
|
267
268
|
RESPONSE
|
@@ -277,7 +278,7 @@ Honestly, I don't love the code I just wrote for the previous example. It's a li
|
|
277
278
|
|
278
279
|
For my next example, I'll author a chat room in 32 lines (including comments).
|
279
280
|
|
280
|
-
I will use Iodine's pub/sub extension API to avoid the LiveList module and the timer thread
|
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).
|
281
282
|
|
282
283
|
Also, I'll limit the interaction to WebSocket clients. Why? to show I can.
|
283
284
|
|
@@ -289,21 +290,22 @@ Sadly, this means that the example won't run on Agoo for now.
|
|
289
290
|
# Place in config.ru
|
290
291
|
RESPONSE = [200, { 'Content-Type' => 'text/html',
|
291
292
|
'Content-Length' => '12' }, [ 'Hello World!' ] ]
|
293
|
+
CHAT = "chat".freeze
|
292
294
|
# a Callback class
|
293
295
|
class MyCallbacks
|
294
296
|
def initialize env
|
295
297
|
@name = env["PATH_INFO"][1..-1]
|
296
298
|
@name = "unknown" if(@name.length == 0)
|
297
299
|
end
|
298
|
-
def on_open
|
299
|
-
subscribe
|
300
|
-
publish
|
300
|
+
def on_open client
|
301
|
+
client.subscribe CHAT
|
302
|
+
client.publish CHAT, "#{@name} joined the chat."
|
301
303
|
end
|
302
|
-
def on_message data
|
303
|
-
publish
|
304
|
+
def on_message client, data
|
305
|
+
client.publish CHAT, "#{@name}: #{data}"
|
304
306
|
end
|
305
|
-
def on_close
|
306
|
-
publish
|
307
|
+
def on_close client
|
308
|
+
client.publish CHAT, "#{@name} left the chat."
|
307
309
|
end
|
308
310
|
end
|
309
311
|
# The actual Rack application
|
@@ -334,6 +336,8 @@ ws.onclose = function(e) { console.log("Closed"); };
|
|
334
336
|
ws.onopen = function(e) { e.target.send("Yo!"); };
|
335
337
|
```
|
336
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
|
+
|
337
341
|
### Why didn't anyone think of this sooner?
|
338
342
|
|
339
343
|
Actually, this isn't a completely new idea.
|
@@ -345,3 +349,5 @@ Another proposal was attempted [a few years ago](https://github.com/rack/rack/is
|
|
345
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.
|
346
350
|
|
347
351
|
Things look promising.
|
352
|
+
|
353
|
+
**UPDATE**: code examples were updated to reflect changes in theRack specification's PR.
|