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.

Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +63 -100
  4. data/bin/raw-rbhttp +12 -7
  5. data/examples/config.ru +8 -7
  6. data/examples/echo.ru +8 -7
  7. data/examples/info.md +41 -35
  8. data/examples/pubsub_engine.ru +12 -12
  9. data/examples/redis.ru +10 -12
  10. data/examples/shootout.ru +19 -42
  11. data/exe/iodine +116 -1
  12. data/ext/iodine/defer.c +1 -1
  13. data/ext/iodine/facil.c +12 -8
  14. data/ext/iodine/facil.h +2 -2
  15. data/ext/iodine/iodine.c +177 -343
  16. data/ext/iodine/iodine.h +18 -72
  17. data/ext/iodine/iodine_caller.c +132 -0
  18. data/ext/iodine/iodine_caller.h +21 -0
  19. data/ext/iodine/iodine_connection.c +841 -0
  20. data/ext/iodine/iodine_connection.h +55 -0
  21. data/ext/iodine/iodine_defer.c +391 -0
  22. data/ext/iodine/iodine_defer.h +7 -0
  23. data/ext/iodine/{rb-fiobj2rb.h → iodine_fiobj2rb.h} +6 -6
  24. data/ext/iodine/iodine_helpers.c +51 -5
  25. data/ext/iodine/iodine_helpers.h +2 -3
  26. data/ext/iodine/iodine_http.c +284 -141
  27. data/ext/iodine/iodine_http.h +2 -2
  28. data/ext/iodine/iodine_json.c +13 -13
  29. data/ext/iodine/iodine_json.h +1 -1
  30. data/ext/iodine/iodine_pubsub.c +573 -823
  31. data/ext/iodine/iodine_pubsub.h +15 -27
  32. data/ext/iodine/{rb-rack-io.c → iodine_rack_io.c} +30 -8
  33. data/ext/iodine/{rb-rack-io.h → iodine_rack_io.h} +1 -0
  34. data/ext/iodine/iodine_store.c +136 -0
  35. data/ext/iodine/iodine_store.h +20 -0
  36. data/ext/iodine/iodine_tcp.c +385 -0
  37. data/ext/iodine/iodine_tcp.h +9 -0
  38. data/lib/iodine.rb +73 -171
  39. data/lib/iodine/connection.rb +34 -0
  40. data/lib/iodine/pubsub.rb +5 -18
  41. data/lib/iodine/rack_utils.rb +43 -0
  42. data/lib/iodine/version.rb +1 -1
  43. data/lib/rack/handler/iodine.rb +1 -182
  44. metadata +17 -18
  45. data/ext/iodine/iodine_protocol.c +0 -689
  46. data/ext/iodine/iodine_protocol.h +0 -13
  47. data/ext/iodine/iodine_websockets.c +0 -550
  48. data/ext/iodine/iodine_websockets.h +0 -17
  49. data/ext/iodine/rb-call.c +0 -156
  50. data/ext/iodine/rb-call.h +0 -70
  51. data/ext/iodine/rb-defer.c +0 -124
  52. data/ext/iodine/rb-registry.c +0 -150
  53. data/ext/iodine/rb-registry.h +0 -34
  54. data/lib/iodine/cli.rb +0 -89
  55. data/lib/iodine/monkeypatch.rb +0 -46
  56. data/lib/iodine/protocol.rb +0 -42
  57. data/lib/iodine/websocket.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 360dcbeabc106918078a92d3902fc8ef1a94323c933e028c99224a2ef3b5409a
4
- data.tar.gz: 75425229c8fefd9429d0f8d0936186d13668eb7b3f7c8905e16e3d617cdd3390
3
+ metadata.gz: c633ef1f40df47f2edefcd78b2bd3fc2a896ab7a704b298f42765d605b375947
4
+ data.tar.gz: e810cccda11b73c2c4881fd90e4e0132efbf62c11857edee457381cb700c6302
5
5
  SHA512:
6
- metadata.gz: 1f437aad47df8fc727bf360692597e31ad41e297d1ff7dfe46786b9d3d7a9c40ff334e3eeca8cec1927b2143d4509b2ce509eaf192d9d521f4113b0b68a1cec8
7
- data.tar.gz: ca852958d4f264f907911f95fbd44288b8e6b525afdfc5708b51d2ea13b3d8e236c166544b19686151fdf2c73741b7353e2c2a90b28702275158f16bdf311eeb
6
+ metadata.gz: 1e376b5ca69fb98da82062508687cd2bdaf7bb55b2071559d23c2f5eb330cd7fc9f7856865a86cb382357106835027e5296a8f0c67113a0522dd1a6ac9323bbf
7
+ data.tar.gz: ecd04e8c645961e8df5e4bfa7e2666a71a81dfda1647fe7e43c27894abb9431010663821d36a99bca23727cb0077f6f00144e0cd563a2a34ba9426ab80bb24cb
@@ -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). Currently, macOS support is limited to 10.12 or higher (see [issue #32](https://github.com/boazsegev/iodine/issues/32))
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 in addition to (or instead of) the default `Iodine::Rack` HTTP service.
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.4'
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::Rack.public = '/my/public/folder'
94
- # application
95
- out = [404, {"Content-Length" => "10"}, ["Not Found."]].freeze
96
- app = Proc.new { out }
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
- class WebsocketChat
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
- Iodine::Rack.app= Proc.new do |env|
174
+ APP = Proc.new do |env|
191
175
  if env['rack.upgrade?'.freeze] == :websocket
192
- env['rack.upgrade'.freeze] = WebsocketChat # or: WebsocketChat.new
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 # or: WebsocketChat.new
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.processes = 4
207
- # static file serving can be set manually as well as using the command line:
208
- Iodine::Rack.public = "www/public"
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
- uri = URI(ENV["REDIS_URL"])
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.default_pubsub.is_a? Iodine::PubSub::RedisEngine)
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.default_pubsub.send("CLIENT LIST") { |reply| puts reply }
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 performed almost the same way, using `env['upgrade.tcp']`. In the following (terminal) example, we'll use an echo server without direct socket echo:
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 - notice the upgrade code
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
- Iodine::Rack.app = Proc.new do |env|
260
- if env['upgrade.tcp?'.freeze] && env["HTTP_UPGRADE".freeze] =~ /echo/i.freeze
261
- env['upgrade.tcp'.freeze] = MyProtocol
262
- # no HTTP response will be sent when the status code is 0 (or less).
263
- # to upgrade AFTER a response, set a valid response status code.
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::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.
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
- 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?).
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
- 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).
268
+ The honest answer is "I don't know". I recommend that you perform your own tests.
283
269
 
284
- 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).
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
- * Iodine performed at 69,885.30 req/sec, consuming ~77.8Mb of memory.
272
+ Also, performing benchmarks on a single machine isn't very reliable... but it's all I've got.
287
273
 
288
- * Puma performed at 48,994.59 req/sec, consuming ~79.6Mb of memory.
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
- When benchmarking with `wrk` and using striped down settings (single worker, single thread, 200 concurrent connections), iodine was faster than Puma.
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 56,648.86 req/sec, consuming ~27.4Mb of memory.
282
+ * Iodine performed at 23,559.56 req/sec, consuming ~88.8Mb of memory.
294
283
 
295
- * Puma performed at 16,547.31 req/sec, consuming ~23.4Mb of memory.
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), iodine significantly outperformed Puma.
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, I'm sure...
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
- But me, I prefer to make sure my development software runs the exact same code as my production software. So here we are.
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
- Also, I don't really understand all the minute details of EventMachine's API, it kept crashing my system every time I reached 1K-2K active connections... I'm sure I just don't know how to use EventMachine, but that's just that.
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.
@@ -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
- class HttpProtocol
18
- @timeout = 10
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 = 8
31
- Iodine.processes = 1
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
@@ -36,19 +36,20 @@ module MyHTTPRouter
36
36
  end
37
37
 
38
38
  # A simple Websocket Callback Object.
39
- class BroadcastClient
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
@@ -39,19 +39,20 @@ module MyHTTPRouter
39
39
  end
40
40
 
41
41
  # A simple Websocket Callback Object.
42
- class WebsocketEcho
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
@@ -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
- # a Callback class
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 self.<<(connection)
212
+ def <<(connection)
212
213
  @lock.synchronize { @list << connection }
213
214
  end
214
- def self.>>(connection)
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 self.broadcast(data)
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.each do |c|
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
- # a Callback class
246
- class MyCallbacks
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 << self
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 >> self
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.new
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 (I don't need 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
+ 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 :chat
300
- publish :chat, "#{@name} joined the chat."
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 :chat, "#{@name}: #{data}"
304
+ def on_message client, data
305
+ client.publish CHAT, "#{@name}: #{data}"
304
306
  end
305
- def on_close
306
- publish :chat, "#{@name} left the chat."
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.