sometimes web sockets_dont_work
DESCRIPTION
Presentation by Pawel Ledwon at the 1st XMPP / Realtime MeetupTRANSCRIPT
SOMETIMES WEBSOCKETS DON’T WORK
@pawel_ledwon
WE DO WEBSOCKETS
…and a bit more
REASONS TO LOVE
persistent and bi-directional
very straightforward API
don’t incur too much latency
WebSockets
var websocket = new WebSocket("ws://example.com");
websocket.onopen = function() { websocket.send("Howdy!");};
websocket.onmessage = function(message) { console.log("BEEP", message.data);};
DONE!
NOPE!
1.ancient browsers
2.corporate firewalls
3.restrictive proxies
WHAT’S THE ISSUE?
...
L E T ’SSOLVETHEM!
1.improve connectivity
2.provide cl ients with
best possible transport
3.improve initial latency
PRIMARY OBJECTIVES
1.keep costs reasonable
2.gather some metrics
3.allow experimenting
SECONDARY OBJECTIVES
DESIGN
a t y p e o f
connection
TRANSPORT
WebSockets
Flash
HTTP-based
(SSL/Non-SSL)
DIFFERENT TRANSPORTS
Transport.connect(url)
...
TRANSPORT CLASS
a recipe for finding
b e s t wo r k i n g
t r a n s p o r t
STRATEGY
1.quick and effective
2.easy to understand
3.also easy to test
4.simple to modify
STRATEGIES SHOULD BE
1.a n c i e n t b rows e r s
2.corporate firewalls
3.restrictive proxies
BACK TO OUR PROBLEMS
ANCIENT BROWSERS
easiest problem to solve
client-side checks
no communication needed
if (WebSocketTransport.isSupported()) { return WebSocketTransport;} else if (FlashTransport.isSupported()) { return FlashTransport;} else { return HTTPTransport;}
Transport.connect(url)
Transport.isSupported()
TRANSPORT CLASS
1.ancient browsers
2.corporate firewalls
3.restrictive proxies
BACK TO OUR PROBLEMS
FLASH & FIREWALLS
neeeds port 843 to be open or…
waits 3s before trying original port
…
you never know what’s coming
FLASH IS NOT THAT BAD
1.it’s basically a WebSocket
2.has low message latency
3.requires less infrastructure
DIFFERENT SOLUTIONS
1.give up and avoid Flash
2.wait and switch to HTTP
3.try Flash & HTTP in parallel
1.use first connected
2.s w i tc h to F l a s h
CONNECTING IN PARALLEL
a)
b)
c)
Flash
HTTP
Flash
HTTP
Flash
HTTP
1.ancient browsers
2.corporate firewalls
3.restrictive proxies
BACK TO OUR PROBLEMS
WEBSOCKETS & PROXIES
some work just fine
some are too old
some just hate us all
WEBSOCKETS & PROXIES
issues with upgrading
frequent disconnections
same issue with Flash
DEALING WITH IT
1.handle WebSockets like Flash
2.use encrypted connections
3.detect broken connections
4.disable transports temporarily
CACHING
1.run full strategy on 1st attempt
2.cache info about best transport
3.use cache on subsequent attempts
STRATEGIES - RECAP
detect browser features
connect in parallel
retry attempts
support delays
cache transport info
detect disconnections
disable transports
support timeouts
IMPLEMENTATION
SINGLE RESPONSIBILITY PRINCIPLE
one decision per strategy
simple interface
lots different types
var runner = strategy.connect(function(error, connection) { if (error) { console.log(":("); } else { // we can even get multiple connections console.log("We’ve got a connection!"); }});
// ok, it has been long enough, I’m giving up
runner.abort();
COMPOSING STRATEGIES
strategies form trees
they can use other strategies
decisions are still simple and local
STRATEGIES
1.transport
2.if
3.best connected
4.sequential
5.delayed
6.cached
7.first connected
provides transport to strategy adapter
runs a test and choses another strategy
runs in parallel to find best strategy
runs strategies sequentially
runs a strategy with a delay
stores and fetches cached transport info
terminates a strategy on first connection
ws
transport
ws
transport
sequential
ws sockjs
transport transport
sequentialsequential
ws sockjs
transport transport
delayed
sequentialsequential
ws sockjs
transport transport
delayed
sequentialsequential
best connected
ws sockjs
transport transport
ifws.isSupported()
delayed
sequentialsequential
best connected
true
ws sockjs
transport transport
ifws.isSupported()
delayed
sequentialsequential
best connected
sockjs
transport
sequential
true false
ws sockjs
transport transport
ifws.isSupported()
delayed
sequentialsequential
best connected
cached
sockjs
transport
sequential
true false
REPRESENTATION
REPRESENTATION SHOULD BE
1.understandable by humans
2.easy to read in JavaScript
3.possible to send to clients
4.simple to modify and test
1.it’s dangerous
2.too expressive
3.difficult to test
SENDING JS IS NOT AN OPTION
JSON
1.has no side-effects
2.widely supported
3.known by everyone
needs an interpreter
JSON
DESIRABLE FEATURES
1.ability to define variables
2.passing by reference
3.predictable execution
4.limited expressiveness
[ [":def", "ws_options", { hostUnencrypted: Pusher.host + ":" + Pusher.ws_port, hostEncrypted: Pusher.host + ":" + Pusher.wss_port, lives: 2 }], [":def", "sockjs_options", { hostUnencrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_http_port, hostEncrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_https_port }], [":def", "timeouts", { loop: true, timeout: 15000, timeoutLimit: 60000 }],
[":def_transport", "ws", "ws", 3, ":ws_options"], [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]],
[":def", "strategy", [":cached", 1800000, [":if", [":is_supported", ":ws"], [ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]] ], [ ":sockjs_loop" ] ]] ] ]]
[
[":def", "ws_options", { hostUnencrypted: Pusher.host + ":" + Pusher.ws_port, hostEncrypted: Pusher.host + ":" + Pusher.wss_port, lives: 2 }], [":def", "sockjs_options", { hostUnencrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_http_port, hostEncrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_https_port }], [":def", "timeouts", { loop: true, timeout: 15000, timeoutLimit: 60000 }],
[":def_transport", "ws", "ws", 3, ":ws_options"], [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]],
[":def", "strategy", [":cached", 1800000, [":if", [":is_supported", ":ws"], [ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]] ], [ ":sockjs_loop" ] ]] ] ]
]
[ [":def", "ws_options", { hostUnencrypted: Pusher.host + ":" + Pusher.ws_port, hostEncrypted: Pusher.host + ":" + Pusher.wss_port, lives: 2 }], [":def", "sockjs_options", { hostUnencrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_http_port, hostEncrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_https_port }], [":def", "timeouts", { loop: true, timeout: 15000, timeoutLimit: 60000 }],
[":def_transport", "ws", "ws", 3, ":ws_options"], [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]], [":def", "strategy", [":cached", 1800000, [":if", [":is_supported", ":ws"], [ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]] ], [ ":sockjs_loop" ] ]] ] ]
]
[ [":def", "ws_options", { hostUnencrypted: Pusher.host + ":" + Pusher.ws_port, hostEncrypted: Pusher.host + ":" + Pusher.wss_port, lives: 2 }], [":def", "sockjs_options", { hostUnencrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_http_port, hostEncrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_https_port }], [":def", "timeouts", { loop: true, timeout: 15000, timeoutLimit: 60000 }],
[":def_transport", "ws", "ws", 3, ":ws_options"], [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]],
[":def", "strategy", [":cached", 1800000, [":if", [":is_supported", ":ws"], [ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]] ], [ ":sockjs_loop" ] ]] ] ]]
METRICS
WE COLLECT
1.transport state changes
2.browser name, features
3.some strategy actions
BETA DEPLOYMENTS
1.deploy a new feature
2.wait for enough data
3.evaluate all metrics
4.keep or revert changes
THANKS!
@pawel_ledwon