isomorfeus-iodine 0.7.44

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +32 -0
  6. data/.yardopts +8 -0
  7. data/CHANGELOG.md +1038 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE.txt +21 -0
  10. data/LIMITS.md +41 -0
  11. data/README.md +782 -0
  12. data/Rakefile +44 -0
  13. data/SPEC-PubSub-Draft.md +159 -0
  14. data/SPEC-WebSocket-Draft.md +239 -0
  15. data/bin/console +22 -0
  16. data/bin/info.md +353 -0
  17. data/bin/mustache_bench.rb +100 -0
  18. data/bin/poc/Gemfile.lock +23 -0
  19. data/bin/poc/README.md +37 -0
  20. data/bin/poc/config.ru +66 -0
  21. data/bin/poc/gemfile +1 -0
  22. data/bin/poc/www/index.html +57 -0
  23. data/examples/async_task.ru +92 -0
  24. data/examples/config.ru +56 -0
  25. data/examples/echo.ru +59 -0
  26. data/examples/hello.ru +29 -0
  27. data/examples/pubsub_engine.ru +81 -0
  28. data/examples/redis.ru +70 -0
  29. data/examples/shootout.ru +73 -0
  30. data/examples/sub-protocols.ru +90 -0
  31. data/examples/tcp_client.rb +66 -0
  32. data/examples/x-sendfile.ru +14 -0
  33. data/exe/iodine +277 -0
  34. data/ext/iodine/extconf.rb +109 -0
  35. data/ext/iodine/fio.c +11985 -0
  36. data/ext/iodine/fio.h +6373 -0
  37. data/ext/iodine/fio_cli.c +431 -0
  38. data/ext/iodine/fio_cli.h +189 -0
  39. data/ext/iodine/fio_json_parser.h +687 -0
  40. data/ext/iodine/fio_siphash.c +157 -0
  41. data/ext/iodine/fio_siphash.h +37 -0
  42. data/ext/iodine/fio_tls.h +129 -0
  43. data/ext/iodine/fio_tls_missing.c +649 -0
  44. data/ext/iodine/fio_tls_openssl.c +1056 -0
  45. data/ext/iodine/fio_tmpfile.h +50 -0
  46. data/ext/iodine/fiobj.h +44 -0
  47. data/ext/iodine/fiobj4fio.h +21 -0
  48. data/ext/iodine/fiobj_ary.c +333 -0
  49. data/ext/iodine/fiobj_ary.h +139 -0
  50. data/ext/iodine/fiobj_data.c +1185 -0
  51. data/ext/iodine/fiobj_data.h +167 -0
  52. data/ext/iodine/fiobj_hash.c +409 -0
  53. data/ext/iodine/fiobj_hash.h +176 -0
  54. data/ext/iodine/fiobj_json.c +622 -0
  55. data/ext/iodine/fiobj_json.h +68 -0
  56. data/ext/iodine/fiobj_mem.h +71 -0
  57. data/ext/iodine/fiobj_mustache.c +317 -0
  58. data/ext/iodine/fiobj_mustache.h +62 -0
  59. data/ext/iodine/fiobj_numbers.c +344 -0
  60. data/ext/iodine/fiobj_numbers.h +127 -0
  61. data/ext/iodine/fiobj_str.c +433 -0
  62. data/ext/iodine/fiobj_str.h +172 -0
  63. data/ext/iodine/fiobject.c +620 -0
  64. data/ext/iodine/fiobject.h +654 -0
  65. data/ext/iodine/hpack.h +1923 -0
  66. data/ext/iodine/http.c +2754 -0
  67. data/ext/iodine/http.h +1002 -0
  68. data/ext/iodine/http1.c +912 -0
  69. data/ext/iodine/http1.h +29 -0
  70. data/ext/iodine/http1_parser.h +873 -0
  71. data/ext/iodine/http_internal.c +1278 -0
  72. data/ext/iodine/http_internal.h +237 -0
  73. data/ext/iodine/http_mime_parser.h +350 -0
  74. data/ext/iodine/iodine.c +1430 -0
  75. data/ext/iodine/iodine.h +63 -0
  76. data/ext/iodine/iodine_caller.c +218 -0
  77. data/ext/iodine/iodine_caller.h +27 -0
  78. data/ext/iodine/iodine_connection.c +933 -0
  79. data/ext/iodine/iodine_connection.h +55 -0
  80. data/ext/iodine/iodine_defer.c +420 -0
  81. data/ext/iodine/iodine_defer.h +6 -0
  82. data/ext/iodine/iodine_fiobj2rb.h +120 -0
  83. data/ext/iodine/iodine_helpers.c +282 -0
  84. data/ext/iodine/iodine_helpers.h +12 -0
  85. data/ext/iodine/iodine_http.c +1171 -0
  86. data/ext/iodine/iodine_http.h +23 -0
  87. data/ext/iodine/iodine_json.c +302 -0
  88. data/ext/iodine/iodine_json.h +6 -0
  89. data/ext/iodine/iodine_mustache.c +567 -0
  90. data/ext/iodine/iodine_mustache.h +6 -0
  91. data/ext/iodine/iodine_pubsub.c +580 -0
  92. data/ext/iodine/iodine_pubsub.h +26 -0
  93. data/ext/iodine/iodine_rack_io.c +281 -0
  94. data/ext/iodine/iodine_rack_io.h +20 -0
  95. data/ext/iodine/iodine_store.c +142 -0
  96. data/ext/iodine/iodine_store.h +20 -0
  97. data/ext/iodine/iodine_tcp.c +346 -0
  98. data/ext/iodine/iodine_tcp.h +13 -0
  99. data/ext/iodine/iodine_tls.c +261 -0
  100. data/ext/iodine/iodine_tls.h +13 -0
  101. data/ext/iodine/mustache_parser.h +1546 -0
  102. data/ext/iodine/redis_engine.c +957 -0
  103. data/ext/iodine/redis_engine.h +79 -0
  104. data/ext/iodine/resp_parser.h +317 -0
  105. data/ext/iodine/websocket_parser.h +505 -0
  106. data/ext/iodine/websockets.c +735 -0
  107. data/ext/iodine/websockets.h +185 -0
  108. data/isomorfeus-iodine.gemspec +42 -0
  109. data/lib/iodine/connection.rb +61 -0
  110. data/lib/iodine/json.rb +42 -0
  111. data/lib/iodine/mustache.rb +113 -0
  112. data/lib/iodine/pubsub.rb +55 -0
  113. data/lib/iodine/rack_utils.rb +43 -0
  114. data/lib/iodine/tls.rb +16 -0
  115. data/lib/iodine/version.rb +3 -0
  116. data/lib/iodine.rb +274 -0
  117. data/lib/rack/handler/iodine.rb +33 -0
  118. data/logo.png +0 -0
  119. metadata +271 -0
data/bin/info.md ADDED
@@ -0,0 +1,353 @@
1
+ # Ruby's Rack Push: Decoupling the real-time web application from the web
2
+
3
+ ![](https://bowild.files.wordpress.com/2018/05/6865783407_84f470ec02_o.jpg)
4
+
5
+ Something exciting is coming.
6
+
7
+ Everyone is talking about WebSockets and their older cousin EventSource / Server Sent Events (SSE). Faye and ActionCable are all the rage and real-time updates are becoming easier than ever.
8
+
9
+ But it's all a mess. It's hard to set up, it's hard to maintain. The performance is meh. In short, the existing design is expensive - it's expensive in developer hours and it's expensive in hardware costs.
10
+
11
+ However, [a new PR in the Rack repository](https://github.com/rack/rack/pull/1272) promises to change all that in the near future.
12
+
13
+ This PR is a huge step towards simplifying our code base, improving real-time performance and lowering the overall cost of real-time web applications.
14
+
15
+ In a sentence, it's an important step towards [decoupling](https://softwareengineering.stackexchange.com/a/244478/224017) the web application from the web.
16
+
17
+ Remember, Rack is the interface Ruby frameworks (such and Rails and Sinatra) and web applications use to communicate with the Ruby application servers. It's everywhere. So this is a big deal.
18
+
19
+ ## The Problem in a Nutshell
20
+
21
+ The problem with the current standard approach, in a nutshell, is that each real-time application process has to run two servers in order to support real-time functionality.
22
+
23
+ The two servers might be listening on the same port, they might be hidden away in some gem, but at the end of the day, two different IO event handling units have to run side by side.
24
+
25
+ "Why?" you might ask. Well, since you asked, I'll tell you (if you didn't ask, skip to the solution).
26
+
27
+ ### The story of the temporary `hijack`
28
+
29
+ This is the story of a quick temporary solution coming up on it's 5th year as the only "standard" Rack solution available.
30
+
31
+ At some point in our history, the Rack specification needed a way to support long polling and other HTTP techniques. Specifically, Rails 4.0 needed something for their "live stream" feature.
32
+
33
+ For this purpose, [the Rack team came up with the `hijack` API approach](https://github.com/rack/rack/pull/481#issue-9702395).
34
+
35
+ This approach allowed for a quick fix to a pressing need. was meant to be temporary, something quick until Rack 2.0 was released (5 years later, the Rack protocol is still at version 1.3).
36
+
37
+ The `hijack` API offers applications complete control of the socket. Just hijack the socket away from the server and voilá, instant long polling / SSE support... sort of.
38
+
39
+ That's where things started to get messy.
40
+
41
+ To handle the (now "free") socket, a lot of network logic had to be copied from the server layer to the application layer (buffering `write` calls, handling incoming data, protocol management, timeout handling, etc').
42
+
43
+ This is an obvious violation of the "**S**" in S.O.L.I.D (single responsibility), as it adds IO handling responsibilities to the application / framework.
44
+
45
+ It also violates the DRY principle, since the IO handling logic is now duplicated (once within the server and once within the application / framework).
46
+
47
+ Additionally, this approach has issues with HTTP/2 connections, since the network protocol and the application are now entangled.
48
+
49
+ ### The obvious `hijack` price
50
+
51
+ The `hijack` approach has many costs, some hidden, some more obvious.
52
+
53
+ The most easily observed price is memory, performance and developer hours.
54
+
55
+ Due to code duplication and extra work, the memory consumption for `hijack` based solutions is higher and their performance is slower (more system calls, more context switches, etc').
56
+
57
+ Using `require 'faye'` will add WebSockets to your application, but it will take almost 9Mb just to load the gem (this is before any actual work was performed).
58
+
59
+ On the other hand, using the `agoo` or `iodine` HTTP servers will add both WebScokets and SSE to your application without any extra memory consumption.
60
+
61
+ To be more specific, using `iodine` will consume about 2Mb of memory, marginally less than Puma, while providing both HTTP and real-time capabilities.
62
+
63
+ ### The hidden `hijack` price
64
+
65
+ A more subtle price is higher hardware costs and a lower clients-per-machine ratio when using `hijack`.
66
+
67
+ Why?
68
+
69
+ Besides the degraded performance, the `hijack` approach allows some HTTP servers to lean on the `select` system call, (Puma used `select` last time I took a look).
70
+
71
+ This system call [breaks down at around the 1024 open file limit](http://man7.org/linux/man-pages/man2/select.2.html#BUGS), possibly limiting each process to 1024 open connections.
72
+
73
+ When a connection is hijacked, the sockets don't close as fast as the web server expects, eventually leading to breakage and possible crashes if the 1024 open file limit is exceeded.
74
+
75
+ ## The Solution - Callbacks and Events
76
+
77
+ [The new proposed Rack Push PR](https://github.com/rack/rack/pull/1272) offers a wonderful effective way to implement WebSockets and SSE while allowing an application to remain totally server agnostic.
78
+
79
+ This new proposal leaves the responsibility for the network / IO handling with the server, simplifying the application's code base and decoupling it from the network protocol.
80
+
81
+ By using a callback object, the application is notified of any events. Leaving the application free to focus on the data rather than the network layer.
82
+
83
+ The callback object doesn't even need to know anything about the server running the application or the underlying protocol.
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.~~ **EDIT**: the PR was updated, replacing the `extend` approach with an extra `client` object.
86
+
87
+ ### How it works
88
+
89
+ Every Rack server uses a Hash type object to communicate with a Rack application.
90
+
91
+ This is how Rails is built, this is how Sinatra is built and this is how every Rack application / framework is built. It's in [the current Rack specification](https://github.com/rack/rack/blob/master/SPEC).
92
+
93
+ A simple Hello world using Rack would look like this (placed in a file called `config.ru`):
94
+
95
+ ```ruby
96
+ # normal HTTP response
97
+ RESPONSE = [200, { 'Content-Type' => 'text/html',
98
+ 'Content-Length' => '12' }, [ 'Hello World!' ] ]
99
+ # note the `env` variable
100
+ APP = Proc.new {|env| RESPONSE }
101
+ # The Rack DSL used to run the application
102
+ run APP
103
+ ```
104
+
105
+ This new proposal introduces the `env['rack.upgrade?']` variable.
106
+
107
+ Normally, this variable is set to `nil` (or missing from the `env` Hash).
108
+
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
+
111
+ To set a callback object, the `env['rack.upgrade']` is introduced (notice the *missing* question mark).
112
+
113
+ Now the design might look like this:
114
+
115
+ ```ruby
116
+ # Place in config.ru
117
+ RESPONSE = [200, { 'Content-Type' => 'text/html',
118
+ 'Content-Length' => '12' }, [ 'Hello World!' ] ]
119
+ # an example Callback class
120
+ class MyCallbacks
121
+ def on_open client
122
+ puts "* Push connection opened."
123
+ end
124
+ def on_message client, data
125
+ puts "* Incoming data: #{data}"
126
+ client.write "Roger that, \"#{data}\""
127
+ end
128
+ def on_close client
129
+ puts "* Push connection closed."
130
+ end
131
+ end
132
+ # note the `env` variable
133
+ APP = Proc.new do |env|
134
+ if(env['rack.upgrade?'])
135
+ env['rack.upgrade'] = MyCallbacks.new
136
+ [200, {}, []]
137
+ else
138
+ RESPONSE
139
+ end
140
+ end
141
+ # The Rack DSL used to run the application
142
+ run APP
143
+ ```
144
+
145
+ Run this application with the Agoo or Iodine servers and let the magic sparkle.
146
+
147
+ For example, using Iodine:
148
+
149
+ ```bash
150
+ # install iodine, version 0.6.0 and up
151
+ gem install iodine
152
+ # start in single threaded mode
153
+ iodine -t 1
154
+ ```
155
+
156
+ Now open the browser, visit [localhost:3000](http://localhost:3000) and open the browser console to test some JavaScript.
157
+
158
+ First try an EventSource (SSE) connection (run in browser console):
159
+
160
+ ```js
161
+ // An SSE example
162
+ var source = new EventSource("/");
163
+ source.onmessage = function(msg) {
164
+ console.log(msg.id);
165
+ console.log(msg.data);
166
+ };
167
+ ```
168
+
169
+ Sweet! nothing happened just yet (we aren't sending notifications), but we have an open SSE connection!
170
+
171
+ What about WebSockets (run in browser console):
172
+
173
+ ```js
174
+ // A WebSocket example
175
+ ws = new WebSocket("ws://localhost:3000/");
176
+ ws.onmessage = function(e) { console.log(e.data); };
177
+ ws.onclose = function(e) { console.log("closed"); };
178
+ ws.onopen = function(e) { e.target.send("Hi!"); };
179
+
180
+ ```
181
+
182
+ Wow! Did you look at the Ruby console - we have working WebSockets, it's that easy.
183
+
184
+ And this same example will run perfectly using the Agoo server as well (both Agoo and Iodine already support the Rack Push proposal).
185
+
186
+ Try it:
187
+
188
+ ```bash
189
+ # install the agoo server, version 2.1.0 and up
190
+ gem install agoo
191
+ # start it up
192
+ rackup -s agoo -p 3000
193
+ ```
194
+
195
+ Notice, no gems, no extra code, no huge memory consumption, just the Ruby server and raw Rack (I didn't even use a framework just yet).
196
+
197
+ ### The amazing push
198
+
199
+ So far, it's so simple, it's hard to notice how powerful this is.
200
+
201
+ Consider implementing a stock ticker, or in this case, a timer:
202
+
203
+ ```ruby
204
+ # Place in config.ru
205
+ RESPONSE = [200, { 'Content-Type' => 'text/html',
206
+ 'Content-Length' => '12' }, [ 'Hello World!' ] ]
207
+
208
+ # A global live connection storage
209
+ module LiveList
210
+ @list = []
211
+ @lock = Mutex.new
212
+ def <<(connection)
213
+ @lock.synchronize { @list << connection }
214
+ end
215
+ def >>(connection)
216
+ @lock.synchronize { @list.delete connection }
217
+ end
218
+ def any?
219
+ # remove connection to the "live list"
220
+ @lock.synchronize { @list.any? }
221
+ end
222
+ # this will send a message to all the connections that share the same process.
223
+ # (in cluster mode we get partial broadcasting only and this doesn't scale)
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
227
+ @lock.synchronize do
228
+ tmp = @list.dup # copy list into tmp
229
+ end
230
+ # iterate list outside of critical section
231
+ tmp.each {|c| c.write data }
232
+ end
233
+ extend self
234
+ end
235
+
236
+ # Broadcast the time very second... but...
237
+ # Threads will BREAK in cluster mode.
238
+ @thread = Thread.new do
239
+ while(LiveList.any?) do
240
+ sleep(1)
241
+ LiveList.broadcast "The time is: #{Time.now}"
242
+ end
243
+ end
244
+
245
+ # an example static Callback module
246
+ module MyCallbacks
247
+ def on_open client
248
+ # add connection to the "live list"
249
+ LiveList << client
250
+ end
251
+ def on_message(client, data)
252
+ # Just an example broadcast
253
+ LiveList.broadcast "Special Announcement: #{data}"
254
+ end
255
+ def on_close client
256
+ # remove connection to the "live list"
257
+ LiveList >> client
258
+ end
259
+ extend self
260
+ end
261
+
262
+ # The Rack application
263
+ APP = Proc.new do |env|
264
+ if(env['rack.upgrade?'])
265
+ env['rack.upgrade'] = MyCallbacks
266
+ [200, {}, []]
267
+ else
268
+ RESPONSE
269
+ end
270
+ end
271
+ # The Rack DSL used to run the application
272
+ run APP
273
+ ```
274
+
275
+ Run the iodine server in single process mode: `iodine -w 1` and the little timer is ticking.
276
+
277
+ Honestly, I don't love the code I just wrote for the previous example. It's a little long, it's slightly iffy and we can't use iodine's cluster mode.
278
+
279
+ For my next example, I'll author a chat room in 32 lines (including comments).
280
+
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).
282
+
283
+ Also, I'll limit the interaction to WebSocket clients. Why? to show I can.
284
+
285
+ This will better demonstrate the power offered by the new `env['rack.upgrade']` approach and it will also work in cluster mode.
286
+
287
+ Sadly, this means that the example won't run on Agoo for now.
288
+
289
+ ```ruby
290
+ # Place in config.ru
291
+ RESPONSE = [200, { 'Content-Type' => 'text/html',
292
+ 'Content-Length' => '12' }, [ 'Hello World!' ] ]
293
+ CHAT = "chat".freeze
294
+ # a Callback class
295
+ class MyCallbacks
296
+ def initialize env
297
+ @name = env["PATH_INFO"][1..-1]
298
+ @name = "unknown" if(@name.length == 0)
299
+ end
300
+ def on_open client
301
+ client.subscribe CHAT
302
+ client.publish CHAT, "#{@name} joined the chat."
303
+ end
304
+ def on_message client, data
305
+ client.publish CHAT, "#{@name}: #{data}"
306
+ end
307
+ def on_close client
308
+ client.publish CHAT, "#{@name} left the chat."
309
+ end
310
+ end
311
+ # The actual Rack application
312
+ APP = Proc.new do |env|
313
+ if(env['rack.upgrade?'] == :websocket)
314
+ env['rack.upgrade'] = MyCallbacks.new(env)
315
+ [200, {}, []]
316
+ else
317
+ RESPONSE
318
+ end
319
+ end
320
+ # The Rack DSL used to run the application
321
+ run APP
322
+ ```
323
+
324
+ Start the application from the command line (in terminal):
325
+
326
+ ```bash
327
+ iodine
328
+ ```
329
+
330
+ Now try (in the browser console):
331
+
332
+ ```js
333
+ ws = new WebSocket("ws://localhost:3000/Mitchel");
334
+ ws.onmessage = function(e) { console.log(e.data); };
335
+ ws.onclose = function(e) { console.log("Closed"); };
336
+ ws.onopen = function(e) { e.target.send("Yo!"); };
337
+ ```
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
+
341
+ ### Why didn't anyone think of this sooner?
342
+
343
+ Actually, this isn't a completely new idea.
344
+
345
+ Evens as the `hijack` API itself was suggested, [an alternative approach was suggested](https://github.com/rack/rack/pull/481#issuecomment-11916942).
346
+
347
+ Another proposal was attempted [a few years ago](https://github.com/rack/rack/issues/1093).
348
+
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.
350
+
351
+ Things look promising.
352
+
353
+ **UPDATE**: code examples were updated to reflect changes in theRack specification's PR.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'benchmark/ips'
4
+ require 'mustache'
5
+ require 'iodine'
6
+
7
+ def benchmark_mustache
8
+ # benchmark code was copied, in part, from:
9
+ # https://github.com/mustache/mustache/blob/master/benchmarks/render_collection_benchmark.rb
10
+ template = """
11
+ {{#products}}
12
+ <div class='product_brick'>
13
+ <div class='container'>
14
+ <div class='element'>
15
+ <img src='images/{{image}}' class='product_miniature' />
16
+ </div>
17
+ <div class='element description'>
18
+ <a href={{url}} class='product_name block bold'>
19
+ {{external_index}}
20
+ </a>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ {{/products}}
25
+ """
26
+
27
+ IO.write "test_template.mustache", template
28
+ filename = "test_template.mustache"
29
+
30
+ data_1 = {
31
+ products: [ {
32
+ :external_index=>"This <product> should've been \"properly\" escaped.",
33
+ :url=>"/products/7",
34
+ :image=>"products/product.jpg"
35
+ } ]
36
+ }
37
+ data_1000 = {
38
+ products: []
39
+ }
40
+
41
+ 1000.times do
42
+ data_1000[:products] << {
43
+ :external_index=>"product",
44
+ :url=>"/products/7",
45
+ :image=>"products/product.jpg"
46
+ }
47
+ end
48
+
49
+ data_1000_escaped = {
50
+ products: []
51
+ }
52
+
53
+ 1000.times do
54
+ data_1000_escaped[:products] << {
55
+ :external_index=>"This <product> should've been \"properly\" escaped.",
56
+ :url=>"/products/7",
57
+ :image=>"products/product.jpg"
58
+ }
59
+ end
60
+
61
+ view = Mustache.new
62
+ view.template = template
63
+ view.render # Call render once so the template will be compiled
64
+ iodine_view = Iodine::Mustache.new(filename)
65
+
66
+ puts "Ruby Mustache rendering (and HTML escaping) results in:",
67
+ view.render(data_1), "",
68
+ "Notice that Iodine::Mustache rendering (and HTML escaping) results in agressive escaping:",
69
+ iodine_view.render(data_1), "", "----"
70
+
71
+ # return;
72
+
73
+ Benchmark.ips do |x|
74
+ x.report("Ruby Mustache render list of 1000") do |times|
75
+ view.render(data_1000)
76
+ end
77
+ x.report("Iodine::Mustache render list of 1000") do |times|
78
+ iodine_view.render(data_1000)
79
+ end
80
+
81
+ x.report("Ruby Mustache render list of 1000 with escaped data") do |times|
82
+ view.render(data_1000_escaped)
83
+ end
84
+ x.report("Iodine::Mustache render list of 1000 with escaped data") do |times|
85
+ iodine_view.render(data_1000_escaped)
86
+ end
87
+
88
+ x.report("Ruby Mustache - no chaching - render list of 1000") do |times|
89
+ tmp = Mustache.new
90
+ tmp.template = template
91
+ tmp.render(data_1000)
92
+ end
93
+ x.report("Iodine::Mustache - no chaching - render list of 1000") do |times|
94
+ Iodine::Mustache.render(nil, data_1000, template)
95
+ end
96
+
97
+ end
98
+ end
99
+
100
+ benchmark_mustache
@@ -0,0 +1,23 @@
1
+ GIT
2
+ remote: https://github.com/boazsegev/iodine.git
3
+ revision: 8b57a5f2008b9c35f2cf589be7ed121823a84582
4
+ specs:
5
+ iodine (0.2.0)
6
+ rack
7
+ rake-compiler
8
+
9
+ GEM
10
+ specs:
11
+ rack (2.0.1)
12
+ rake (11.2.2)
13
+ rake-compiler (0.9.5)
14
+ rake
15
+
16
+ PLATFORMS
17
+ ruby
18
+
19
+ DEPENDENCIES
20
+ iodine!
21
+
22
+ BUNDLED WITH
23
+ 1.12.5
data/bin/poc/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # A proof of concept for Rack's `env['rack.websocket']`
2
+
3
+ This is a proof of concept for Rack based Websocket connections, showing how the Rack API can be adjusted to support server native real-time connections.
4
+
5
+ The chosen proof of concept was the ugliest chatroom I could find.
6
+
7
+ Although my hope is that Rack will adopt the concept and make `env['rack.websocket?']` and `env['rack.websocket']` part of it's standard, at the moment it's an Iodine specific feature, implemented using `env['iodine.websocket']`.
8
+
9
+ ## Install
10
+
11
+ Install required gems using:
12
+
13
+ ```sh
14
+ bundler install
15
+ ```
16
+
17
+ ## Run
18
+
19
+ Run this application single threaded:
20
+
21
+ ```sh
22
+ bundler exec iodine -- -www ./www
23
+ ```
24
+
25
+ Or both multi-threaded and forked (you'll notice that memory barriers for forked processes prevent websocket broadcasting from reaching websockets connected to a different process).
26
+
27
+ ```sh
28
+ bundler exec iodine -- -www ./www -t 16 -w 4
29
+ ```
30
+
31
+ ## Further reading
32
+
33
+ * https://github.com/rack/rack/issues/1093
34
+
35
+ * https://bowild.wordpress.com/2016/07/31/the-dark-side-of-the-rack
36
+
37
+ * https://github.com/boazsegev/iodine
data/bin/poc/config.ru ADDED
@@ -0,0 +1,66 @@
1
+ # The Rack Application container
2
+ module MyRackApplication
3
+ # Rack applications use the `call` callback to handle HTTP requests.
4
+ def self.call(env)
5
+ # if upgrading...
6
+ if env['HTTP_UPGRADE'.freeze] =~ /websocket/i
7
+ # We can assign a class or an instance that implements callbacks.
8
+ # We will assign an object, passing it the request information (`env`)
9
+ env['iodine.websocket'.freeze] = MyWebsocket.new(env)
10
+ # Rack responses must be a 3 item array
11
+ # [status, {http: :headers}, ["response body"]]
12
+ return [0, {}, []]
13
+ end
14
+ # a semi-regualr HTTP response
15
+ out = File.open File.expand_path('../www/index.html', __FILE__)
16
+ [200, { 'X-Sendfile' => File.expand_path('../www/index.html', __FILE__),
17
+ 'Content-Length' => out.size }, out]
18
+ end
19
+ end
20
+
21
+ # The Websocket Callback Object
22
+ class MyWebsocket
23
+ # this is optional, but I wanted the object to have the nickname provided in
24
+ # the HTTP request
25
+ def initialize(env)
26
+ # we need to change the ASCI Rack encoding to UTF-8,
27
+ # otherwise everything with the nickname will be a binary "blob" in the
28
+ # Javascript layer
29
+ @nickname = env['PATH_INFO'][1..-1].force_encoding 'UTF-8'
30
+ end
31
+
32
+ # A classic websocket callback, called when the connection is opened and
33
+ # linked to this object
34
+ def on_open
35
+ puts 'We have a websocket connection'
36
+ end
37
+
38
+ # A classic websocket callback, called when the connection is closed
39
+ # (after disconnection).
40
+ def on_close
41
+ puts "Bye Bye... #{count} connections left..."
42
+ end
43
+
44
+ # A server-side niceness, called when the server if shutting down,
45
+ # to gracefully disconnect (before disconnection).
46
+ def on_shutdown
47
+ write 'The server is shutting down, goodbye.'
48
+ end
49
+
50
+ def on_message(data)
51
+ puts "got message: #{data} encoded as #{data.encoding}"
52
+ # data is a temporary string, it's buffer cleared as soon as we return.
53
+ # So we make a copy with the desired format.
54
+ tmp = "#{@nickname}: #{data}"
55
+ # The `write` method was added by the server and writes to the current
56
+ # connection
57
+ write tmp
58
+ puts "broadcasting #{tmp.bytesize} bytes with encoding #{tmp.encoding}"
59
+ # `each` was added by the server and excludes this connection
60
+ # (each except self).
61
+ each { |h| h.write tmp }
62
+ end
63
+ end
64
+
65
+ # `run` is a Rack API command, telling Rack where the `call(env)` callback is located.
66
+ run MyRackApplication
data/bin/poc/gemfile ADDED
@@ -0,0 +1 @@
1
+ gem 'iodine', git: 'https://github.com/boazsegev/iodine.git'
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
5
+ <script>
6
+ ws = NaN
7
+ handle = ''
8
+ function onsubmit(e) {
9
+ e.preventDefault();
10
+ if($('#text')[0].value == '') {return false}
11
+ if(ws && ws.readyState == 1) {
12
+ ws.send($('#text')[0].value);
13
+ $('#text')[0].value = '';
14
+ } else {
15
+ handle = $('#text')[0].value
16
+ var url = (window.location.protocol.match(/https/) ? 'wss' : 'ws') +
17
+ '://' + window.document.location.host +
18
+ '/' + $('#text')[0].value
19
+ ws = new WebSocket(url)
20
+ ws.onopen = function(e) {
21
+ output("<b>Connected :-)</b>");
22
+ $('#text')[0].value = '';
23
+ $('#text')[0].placeholder = 'your message';
24
+ }
25
+ ws.onclose = function(e) {
26
+ output("<b>Disonnected :-/</b>")
27
+ $('#text')[0].value = '';
28
+ $('#text')[0].placeholder = 'nickname';
29
+ $('#text')[0].value = handle
30
+ }
31
+ ws.onmessage = function(e) {
32
+ output(e.data);
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ function output(data) {
38
+ $('#output').append("<li>" + data + "</li>")
39
+ $('#output').animate({ scrollTop:
40
+ $('#output')[0].scrollHeight }, "slow");
41
+ }
42
+ </script>
43
+ <style>
44
+ html, body {width:100%; height: 100%; background-color: #ddd; color: #111;}
45
+ h3, form {text-align: center;}
46
+ input {background-color: #fff; color: #111; padding: 0.3em;}
47
+ </style>
48
+ </head><body>
49
+ <h3>The Ugly Chatroom POC</h3>
50
+ <form id='form'>
51
+ <input type='text' id='text' name='text' placeholder='nickname'></input>
52
+ <input type='submit' value='send'></input>
53
+ </form>
54
+ <script> $('#form')[0].onsubmit = onsubmit </script>
55
+ <ul id='output'></ul>
56
+ </body>
57
+ </html>