iodine 0.4.19 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

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