agoo 2.15.10 → 2.15.11

Sign up to get free protection for your applications and to get access to all the features.
data/misc/push.md ADDED
@@ -0,0 +1,581 @@
1
+ # A New Rack Push
2
+
3
+ Realtime websites are essential for some domains. A web site displaying stock
4
+ prices or race progress would not be very useful if the displays were not kept
5
+ up to date. It is of course possible to use constant refreshes or polling but
6
+ that puts a heavy demand on the server and it is not real time.
7
+
8
+ Fortunately there is technology to push data to a web page. WebSocket and SSE
9
+ provide a means to push data to a web page. Coupled with Javascript pages can
10
+ be dynamic and display data as it changes.
11
+
12
+ Up until now, Ruby support for WebSockets and SSE was very cumbersome,
13
+ resulting in poor performance and higher memory bloat despite well authored
14
+ gems such as Faye and ActionCable.
15
+
16
+ Using Rack as an example it was possible to hijack a connection and manage all
17
+ the interactions using raw IO calls but that is a task that is not for the
18
+ faint at heart. It is a non trivial exercise and it requires duplication of
19
+ the network handling logic. Basically it's almost like running two servers
20
+ instead of one.
21
+
22
+ Won't it be nice to have a simple way of supporting push with Ruby? Something
23
+ that works with the server rather than against it? Something that avoids the
24
+ need to run another "server" on the same process?
25
+
26
+ There is a proposal to extend the Rack spec to include an option for WebSocket
27
+ and SSE. Currently two servers support that proposed
28
+ extension. [Agoo](https://https://github.com/ohler55/agoo) and
29
+ [Iodine](https://github.com/boazsegev/iodine) both high performance web server
30
+ gems that support push and implement the [proposed Rack
31
+ extension](https://github.com/rack/rack/pull/1272). A copy of the proposed
32
+ spec in HTML format is [here](http://www.ohler.com/agoo/rack/file.SPEC.html).
33
+
34
+ ## Simple Callback Design
35
+
36
+ The Rack extension proposal uses a simple callback design. This design
37
+ replaces the need for socket hijacking, removes the hassle of socket
38
+ management and decouples the application from the network protocol layer.
39
+
40
+ All Rack handlers already have a `#call(env)` method that processes HTTP
41
+ requests. If the `env` argument to that call includes a non `nil` value for
42
+ `env['rack.upgrade?']` then the handle is expected to upgrade to a push
43
+ connection that is either WebSocket or SSE.
44
+
45
+ Acceptance of the connection upgrade is implemented by setting
46
+ `env['rack.upgrade']` to a new push handler. The push handler can implement
47
+ methods `#on_open`, `#on_close`, `#on_message`, `#on_drained`, and
48
+ `#on_shutdown`. These are all optional. A `client` object is passed as the
49
+ first argument to each method. This `client` can be used to check connection
50
+ status as well as writing messages to the connection.
51
+
52
+ The `client` has methods `#write(msg)`, `#pending`, `#open?`, and `#close`
53
+ method. The `#write(msg)` method is used to push data to web pages. The
54
+ details are handled by the server. Just call write and data appears at
55
+ browser.
56
+
57
+ ## Examples
58
+
59
+ The example is a bit more than a hello world but only enough to make it
60
+ interesting. A browser is used to connect to a Rack server that runs a clock,
61
+ On each tick of the clock the time is sent to the browser. Either an SSE and a
62
+ WebSocket page can be used. That means you can connect with your mobile device
63
+ using SSE. Try it to see how easy it is.
64
+
65
+ First some web pages will be needed. Lets call them `websocket.html` and
66
+ `sse.html`. Notice how similar they look.
67
+
68
+ ```html
69
+ <!-- websocket.html -->
70
+ <html>
71
+ <body>
72
+ <p id="status"> ... </p>
73
+ <p id="message"> ... waiting ... </p>
74
+
75
+ <script type="text/javascript">
76
+ var sock;
77
+ var url = "ws://" + document.URL.split('/')[2] + '/upgrade'
78
+ if (typeof MozWebSocket != "undefined") {
79
+ sock = new MozWebSocket(url);
80
+ } else {
81
+ sock = new WebSocket(url);
82
+ }
83
+ sock.onopen = function() {
84
+ document.getElementById("status").textContent = "connected";
85
+ }
86
+ sock.onmessage = function(msg) {
87
+ document.getElementById("message").textContent = msg.data;
88
+ }
89
+ </script>
90
+ </body>
91
+ </html>
92
+ ```
93
+
94
+ ```html
95
+ <!-- sse.html -->
96
+ <html>
97
+ <body>
98
+ <p id="status"> ... </p>
99
+ <p id="message"> ... waiting ... </p>
100
+
101
+ <script type="text/javascript">
102
+ var src = new EventSource('upgrade');
103
+ src.onopen = function() {
104
+ document.getElementById('status').textContent = 'connected';
105
+ }
106
+ src.onerror = function() {
107
+ document.getElementById('status').textContent = 'not connected';
108
+ }
109
+ function doSet(e) {
110
+ document.getElementById("message").textContent = e.data;
111
+ }
112
+ src.addEventListener('msg', doSet, false);
113
+ </script>
114
+ </body>
115
+ </html>
116
+ ```
117
+
118
+ With those two files there are a couple ways to implement the Ruby side.
119
+
120
+ A pure, `rackup` example works with any of the web server gems the support the
121
+ proposed additions to the Rack spec. Currently there are two gems that support
122
+ the additions. They are [Agoo](https://github.com/ohler55/agoo) and
123
+ [Iodine](https://github.com/boazsegev/iodine). Both are high performance
124
+ servers with some features unique to both.
125
+
126
+ One example is for Agoo only to demonstrate the use of the Agoo demultiplexing
127
+ but is otherwise identical to the pure Rack example.
128
+
129
+ The third, pub-sub example is also Agoo specific but with a few minor changes
130
+ it would be compatible with Iodine as well. The pub-sub example is a minimal
131
+ example of how publish and subscribe can be used with Rack.
132
+
133
+ ### Pure Rack Example
134
+
135
+ Lets start with the server agnostic example that is just Rack with the
136
+ proposed extensions to the spec. Of course the file is named `config.ru`. The
137
+ file includes more than is necessary just work but some options methods are
138
+ added to make it clear when they are called. Since it is a 'hello world'
139
+ example there is the obligatory handled for returning that result. Comments
140
+ have been stripped out of the code in favor of explaining the code separately.
141
+
142
+ First the file then an explanation.
143
+
144
+ ```ruby
145
+ # config.ru
146
+ require 'rack'
147
+ #require 'agoo'
148
+
149
+ class Clock
150
+ def initialize()
151
+ @clients = []
152
+ @mutex = Mutex.new
153
+ end
154
+
155
+ def on_open(client)
156
+ puts "--- on_open"
157
+ @mutex.synchronize {
158
+ @clients << client
159
+ }
160
+ end
161
+
162
+ def on_close(client)
163
+ puts "--- on_close"
164
+ @mutex.synchronize {
165
+ @clients.delete(client)
166
+ }
167
+ end
168
+
169
+ def on_drained(client)
170
+ puts "--- on_drained"
171
+ end
172
+
173
+ def on_message(client, data)
174
+ puts "--- on_message #{data}"
175
+ client.write("echo: #{data}")
176
+ end
177
+
178
+ def start
179
+ loop do
180
+ now = Time.now
181
+ msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
182
+ @mutex.synchronize {
183
+ @clients.each { |c|
184
+ puts "--- write failed" unless c.write(msg)
185
+ }
186
+ }
187
+ sleep(1)
188
+ end
189
+ end
190
+ end
191
+
192
+ $clock = Clock.new
193
+
194
+ class Listen
195
+ def call(env)
196
+ path = env['SCRIPT_NAME'] + env['PATH_INFO']
197
+ case path
198
+ when '/'
199
+ return [ 200, { }, [ "hello world" ] ]
200
+ when '/websocket.html'
201
+ return [ 200, { }, [ File.read('websocket.html') ] ]
202
+ when '/sse.html'
203
+ return [ 200, { }, [ File.read('sse.html') ] ]
204
+ when '/upgrade'
205
+ unless env['rack.upgrade?'].nil?
206
+ env['rack.upgrade'] = $clock
207
+ return [ 200, { }, [ ] ]
208
+ end
209
+ end
210
+ [ 404, { }, [ ] ]
211
+ end
212
+ end
213
+
214
+ Thread.new { $clock.start }
215
+ run Listen.new
216
+ ```
217
+
218
+ #### Running
219
+
220
+ The example is run with this command.
221
+
222
+ ```
223
+ $ bundle exec rackup -r agoo -s agoo
224
+ ```
225
+
226
+ The server to use must be specified to use something other than the
227
+ default. Alternatively the commented out `require` for Agoo or an alternative
228
+ can be used by uncommenting the line.
229
+
230
+ ```ruby
231
+ require 'agoo'
232
+ ```
233
+
234
+ Once started open a browser and go to `http://localhost:9292/websocket.html`
235
+ or `http://localhost:9292/sse.html` and watch the page show 'connected' and
236
+ then showing the time as it changes every second.
237
+
238
+ #### Clock
239
+
240
+ The `Clock` class is the handler for upgraded calls. The `Listener` and
241
+ `Clock` could have been combined into a single class but for clarity they are
242
+ kept separate in this example.
243
+
244
+ The `Clock` object, `$clock` maintains a list of open connections. The default
245
+ configuration uses one thread but to be safe a mutex is used so that the same
246
+ example can be used with multiple threads configured. It's a good practice
247
+ when writing asynchronous callback code to assume there will be multiple threads
248
+ invoking the callbacks.
249
+
250
+ ```ruby
251
+ def initialize()
252
+ @clients = []
253
+ @mutex = Mutex.new
254
+ end
255
+ ```
256
+
257
+ The `#on_open` callback is called when a connection has been upgraded to a
258
+ WebSocket or SSE connection. That is the time to add the connection to the
259
+ client `client` to the `@clients` list is used to implement a broadcast write
260
+ or publish to all open connections. There are other options that will be
261
+ explored later.
262
+
263
+ ```ruby
264
+ def on_open(client)
265
+ puts "--- on_open"
266
+ @mutex.synchronize {
267
+ @clients << client
268
+ }
269
+ end
270
+ ```
271
+
272
+ When a connection is closed it should be removed the the `@clients` list. The
273
+ `#on_close` callback handles that. The other callbacks merely print out a
274
+ statement that they have been invoked so that it is easier to trace what is
275
+ going on.
276
+
277
+ ```ruby
278
+ def on_close(client)
279
+ puts "--- on_close"
280
+ @mutex.synchronize {
281
+ @clients.delete(client)
282
+ }
283
+ end
284
+ ```
285
+
286
+ Finally, the clock is going to publish the time to all connections. The
287
+ `#start` method starts the clock and then every second the time is published
288
+ by writing to each connection.
289
+
290
+ ```ruby
291
+ def start
292
+ loop do
293
+ now = Time.now
294
+ msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
295
+ @mutex.synchronize {
296
+ @clients.each { |c|
297
+ puts "--- write failed" unless c.write(msg)
298
+ }
299
+ }
300
+ sleep(1)
301
+ end
302
+ end
303
+ ```
304
+
305
+ #### Listening
306
+
307
+ In a rackup started application, all request go to a single handler that has a
308
+ `#call(env)` method. Demultiplexing of the incoming request is done in the
309
+ `#call(env)` itself. Requests for static assets such as web pages have to be
310
+ handled.
311
+
312
+ ```ruby
313
+ def call(env)
314
+ path = env['SCRIPT_NAME'] + env['PATH_INFO']
315
+ case path
316
+ when '/'
317
+ return [ 200, { }, [ "hello world" ] ]
318
+ when '/websocket.html'
319
+ return [ 200, { }, [ File.read('websocket.html') ] ]
320
+ when '/sse.html'
321
+ return [ 200, { }, [ File.read('sse.html') ] ]
322
+ when '/upgrade'
323
+ unless env['rack.upgrade?'].nil?
324
+ env['rack.upgrade'] = $clock
325
+ return [ 200, { }, [ ] ]
326
+ end
327
+ end
328
+ [ 404, { }, [ ] ]
329
+ end
330
+ ```
331
+
332
+ The proposed feature is the detection of a connection that wants to be
333
+ upgraded by checking the `env['rack.upgrade?']` variable. If that variables is
334
+ `:websocket` of `sse` then a handle should be provided by setting
335
+ `env['rack.upgrade']` to the upgrade handler. A return code of less than 300
336
+ will indicate to the server that the connection can be upgraded.
337
+
338
+ ```ruby
339
+ when '/upgrade'
340
+ unless env['rack.upgrade?'].nil?
341
+ env['rack.upgrade'] = $clock
342
+ return [ 200, { }, [ ] ]
343
+ end
344
+ end
345
+ ```
346
+
347
+ #### Starting
348
+
349
+ The last lines start the clock is a separate thread so that it runs in the
350
+ background and then rackup starts the server with a new listener.
351
+
352
+ ```ruby
353
+ Thread.new { $clock.start }
354
+ run Listen.new
355
+ ```
356
+
357
+ ### Demultiplex
358
+
359
+ The `push.rb` is an example of using the Agoo demultiplexing feature. Allowing
360
+ the server to handle the demultiplexing simplifies `#call(env)` method and
361
+ allows each URL path to be handled by a separate object. This approach is
362
+ common among most web server in almost every language. But that is not the
363
+ only advantage. By setting a root directory for static resources Agoo can
364
+ serve up those resources without getting Ruby involved at all. This allows
365
+ those resources to be server many times faster all without creating additional
366
+ Ruby objects. Pages with significant static resources become snappier and the
367
+ whole users experience is improved.
368
+
369
+ Note: It is possible to set up the demultiplexing using arguments to
370
+ `rackup`. As an example, to set up the handler for `/myhandler` to an instance
371
+ of the `MyHandler` class start `rackup` like this.
372
+
373
+ ```
374
+ $ rackup -r agoo -s agoo -O "/myhandler=MyHandler"
375
+ ```
376
+
377
+ Now the `push.rb` file.
378
+
379
+ ```ruby
380
+ # push.rb
381
+ require 'agoo'
382
+ Agoo::Log.configure(dir: '',
383
+ console: true,
384
+ classic: true,
385
+ colorize: true,
386
+ states: {
387
+ INFO: true,
388
+ DEBUG: false,
389
+ connect: true,
390
+ request: true,
391
+ response: true,
392
+ eval: true,
393
+ push: true,
394
+ })
395
+ Agoo::Server.init(6464, '.', thread_count: 1)
396
+
397
+ class HelloHandler
398
+ def call(env)
399
+ [ 200, { }, [ "hello world" ] ]
400
+ end
401
+ end
402
+
403
+ class Clock
404
+ def initialize()
405
+ @clients = []
406
+ @mutex = Mutex.new
407
+ end
408
+
409
+ def on_open(client)
410
+ puts "--- on_open"
411
+ @mutex.synchronize {
412
+ @clients << client
413
+ }
414
+ end
415
+
416
+ def on_close(client)
417
+ puts "--- on_close"
418
+ @mutex.synchronize {
419
+ @clients.delete(client)
420
+ }
421
+ end
422
+
423
+ def on_drained(client)
424
+ puts "--- on_drained"
425
+ end
426
+
427
+ def on_message(client, data)
428
+ puts "--- on_message #{data}"
429
+ client.write("echo: #{data}")
430
+ end
431
+
432
+ def start
433
+ loop do
434
+ now = Time.now
435
+ msg = "%02d:%02d:%02d" % [now.hour, now.min, now.sec]
436
+ @mutex.synchronize {
437
+ @clients.each { |c|
438
+ puts "--- write failed" unless c.write(msg)
439
+ }
440
+ }
441
+ sleep(1)
442
+ end
443
+ end
444
+ end
445
+
446
+ $clock = Clock.new
447
+
448
+ class Listen
449
+ # Only used for WebSocket or SSE upgrades.
450
+ def call(env)
451
+ unless env['rack.upgrade?'].nil?
452
+ env['rack.upgrade'] = $clock
453
+ [ 200, { }, [ ] ]
454
+ else
455
+ [ 404, { }, [ ] ]
456
+ end
457
+ end
458
+ end
459
+
460
+ Agoo::Server.handle(:GET, "/hello", HelloHandler.new)
461
+ Agoo::Server.handle(:GET, "/upgrade", Listen.new)
462
+
463
+ Agoo::Server.start()
464
+ $clock.start
465
+ ```
466
+
467
+ The `push.rb` file is similar to the `config.ru` file with the same `Clock`
468
+ class. Running is different but still simple.
469
+
470
+ ```
471
+ $ ruby push.rb
472
+ ```
473
+
474
+ #### Logging
475
+
476
+ Agoo allows logging to be configured. The log is an asynchronous log so the
477
+ impact of logging on performance is minimal. The log is a feature based
478
+ logger. Most of the features are turned on so that it is clear what is
479
+ happening when running the example. Switch the `true` values to `false` to
480
+ turn off logging of any of the features listed.
481
+
482
+ ```ruby
483
+ Agoo::Log.configure(dir: '',
484
+ console: true,
485
+ classic: true,
486
+ colorize: true,
487
+ states: {
488
+ INFO: true,
489
+ DEBUG: false,
490
+ connect: true,
491
+ request: true,
492
+ response: true,
493
+ eval: true,
494
+ push: true,
495
+ })
496
+ ```
497
+
498
+ #### Static Assets
499
+
500
+ The second argument to the Aggo server initializer sets the root directory for
501
+ static assets.
502
+
503
+ ```ruby
504
+ Agoo::Server.init(6464, '.', thread_count: 1)
505
+ ```
506
+
507
+ #### Listening
508
+
509
+ Listening becomes simpler just handling the upgrade.
510
+
511
+ ```ruby
512
+ def call(env)
513
+ unless env['rack.upgrade?'].nil?
514
+ env['rack.upgrade'] = $clock
515
+ [ 200, { }, [ ] ]
516
+ else
517
+ [ 404, { }, [ ] ]
518
+ end
519
+ end
520
+ ```
521
+
522
+ #### Demultiplexing
523
+
524
+ It is rare to have the behavior on a URL path to change after starting so why
525
+ not let the server handle the switching. The option to let the application
526
+ handle the demultiplexing in the `#call(env)` invocation but defining the
527
+ switching in one place is much easier to follow and manage especially a large
528
+ team of developers are working on a project.
529
+
530
+ ```ruby
531
+ Agoo::Server.handle(:GET, "/hello", HelloHandler.new)
532
+ Agoo::Server.handle(:GET, "/upgrade", Listen.new)
533
+ ```
534
+
535
+ ### Simplified Pub-Sub
536
+
537
+ No reason to stop simplifying. With Agoo 2.1.0, publish and subscribe is
538
+ possible using string subjects. Instead of setting up the `@clients` array
539
+ attribute the Agoo server can take care of those details. This example is
540
+ slimmed down from the earlier example by making use of the Ruby feature that
541
+ classes are also object so the `Clock` class can act as the handler. Methods
542
+ not needed have been removed to leave just what is needed to publish time out
543
+ to all the listeners. Open a few pages with either or both the
544
+ `websocket.html` or `sse.html` files.
545
+
546
+ Note the use of `Agoo.publish` and `client.subscribe` in the `pubsub.rb`
547
+ example. Those two method are not part of the proposed Rack spec additions but
548
+ make stateless WebSocket and SSE use simple. Us this with the `websocket.html`
549
+ and `sse.html` as with the previous examples.
550
+
551
+ ```ruby
552
+ # pubsub.rb
553
+ require 'agoo'
554
+
555
+ class Clock
556
+ def self.call(env)
557
+ unless env['rack.upgrade?'].nil?
558
+ env['rack.upgrade'] = Clock
559
+ [ 200, { }, [ ] ]
560
+ else
561
+ [ 404, { }, [ ] ]
562
+ end
563
+ end
564
+
565
+ def self.on_open(client)
566
+ client.subscribe('time')
567
+ end
568
+
569
+ Thread.new {
570
+ loop do
571
+ now = Time.now
572
+ Agoo.publish('time', "%02d:%02d:%02d" % [now.hour, now.min, now.sec])
573
+ sleep(1)
574
+ end
575
+ }
576
+ end
577
+
578
+ Agoo::Server.init(6464, '.', thread_count: 0)
579
+ Agoo::Server.handle(:GET, '/upgrade', Clock)
580
+ Agoo::Server.start()
581
+ ```