Free your port 80 with HAProxy
One of the caveats of Comet, and more generally any Ajax-like interaction method for web applications, is the same-domain restrictions on Javascript initiated connections. Your Javascript client code is limited by the browser to only open connections to the domain that served the page it has been embedded or linked from.
There are workarounds depending on the connection method you choose to connect to the remote server. One of them is to dynamically generate <script> tags that invoke a local callback with remote data (I wrote an article about it.) Another alternative is executing Javascript inside an invisible <iframe> loaded from a sub domain of the main domain. This also allows data exchange from the <iframe> code and the main page code.
The user experience
The workaround methods are not exempt of flaws. The dynamic <script> tags loads make the message bar display a "Transferring..." message in some browsers, or a full-blown spinning logo animation in some others. The <iframe> method can cause the same problem, in addition to altering the browsing history (and Ajax applications already have enough problems with that.)
The best user experience comes from using XMLHTTPRequest. The browser doesn't bother the user with loading messages or animations. Instead it exposes to the Javascript application a series of callbacks and status codes that allow the programmer to make data loading as subtle or as visible as desired. The problem is that the Javascript security model forbids XMLHTTPRequest from loading cross-domain resources. In the case of PubSub applications it is very common to have a separate server for serving the subscription connections, different from the one serving the main application.
Filtering proxy
The solution is to install a proxy with filtering capabilities in front of your servers. This is usually accomplished by a dedicated proxy server that is capable of parsing requests and choosing backends based on headers or paths. For this article I chose HAProxy. The other contender was nginx, which I had to discard since it lacks support for HTTP 1.1 features that are vital when dealing with mobile clients (namely support for chunked POST bodies). Both of these servers are excellent choices thanks to their single process, event-based programming that enables them to scale to many thousands of connections.
Use case
In this article I am going to allow my homebrew Twisted PubSub server that sits on notes.olivepeak.com:8080 to accept requests from notes.olivepeak.com:80. This host is already occupied by Apache, serving the Peak Notes application. Externally the PubSub server only deals with subscription requests sent to the URL path /subscriptions/channel/, and nothing else. Since both Twisted and Apache are going to sit behind HAProxy and never serve to the internet anymore, their listen addresses will be changed to 127.0.0.1:30200 and 127.0.0.1:30100 respectively.
Configuring HAProxy
With this information we are ready to let HAProxy take over the port 80 of our frontend server and write a config file for our setup. First we define the Apache backend:
backend apaches
mode http
timeout connect 10s
timeout server 30s
balance roundrobin
server apache1 127.0.0.1:30100 weight 1 maxconn 512
You can have as many server lines as you need, one for every server you want to balance inside the backend. I only have a simple VPS-like server so I only have one server entry, but you could define entire clusters of them, with complex load balancing rules.
The Twisted PubSub backend section:
backend pubsubs
mode http
timeout connect 5s
timeout server 5m
balance roundrobin
server twisted1 127.0.0.1:30200 weight 1 maxconn 10000
A much longer server timeout is required to support the XHR polling method (or any other HTTP polling method.) Also the maxconn parameter has been increased since the PubSub server is meant to be able to support a very large number of simultaneous connections.
The frontend section:
frontend http_proxy
bind 8.12.42.103:80
mode http
timeout client 5m
option forwardfor
default_backend apaches
acl req_pubsub_path path_beg /subscriptions/channel/
acl req_notes hdr_dom(host) -i notes.olivepeak.com
use_backend pubsubs if req_pubsub_path req_notes
First we bind the frontend to the public IP on port 80. The timeout client matches the 5 minutes we allowed for the PubSub backend. option forwardfor will include the X-Forwarded-For with the original IP in the proxied requests. The next 4 lines are the most interesting ones and I am going to explain them one by one. First we have the default backend:
default_backend apaches
This config line makes HAProxy use the apaches backed as the default target for incoming requests. If it's impossible to match a request with any ACL rule, it will be proxied to apaches. Next we define an ACL expression:
acl req_pubsub_path path_beg /subscriptions/channel/
This tells HAProxy to assign the comparison path_beg /subscriptions/channel/ to the expression req_pubsub_path. path_beg is one of the many filtering operators supported by HAProxy and it compares the beginning of the request path with the given string. You can also compare headers:
acl req_notes hdr_dom(host) -i notes.olivepeak.com
In this case we are comparing the Host header with the operator hdr_dom(host) -i to the string notes.olivepeak.com. hdr_dom will search for a substring in the given header that matches the given string like if it was a domain name.
Finally the intelligent bit:
use_backend pubsubs if req_pubsub_path req_notes
With a single line in its config file we tell HAProxy to use the backed pubsubs when both the req_pubsub_path and the req_notes ACL expressions are true (a logical and operator is implied between them). From now on any request made to notes.olivepeak.com/subscriptions/channel/ will be relayed to the pubsubs backend server(s), and everything else will will be handled by the apaches backend.
Conclusion
No matter how small your hosting infrastructure is, or how modest your application traffic is, you can benefit from an advanced proxy frontend. It will be ready to grow with your needs and it will enable you to choose the best tool for the job. Choosing an application server will be much easier since you can run many of them at the same time on the same host.
Implementing script tag long polling for Comet applications
In my previous post I showed how to implement a simple PubSub server with Twisted for asynchronous Comet updates to a web application. For the Javascript client side of the application I've choosen the <script> tag long polling method.
Choosing a connection method
There are many different ways to achieve the "instant update" feel in a Comet application. They mostly differ in their latency and the security restrictions imposed by the browsers. They are divided in two big groups:
- Streaming: a connection is kept open between the server and the browser. Regular updates are pushed through it and parsed by the client as they arrive. Offers the lowest latency.
- Long polling: a connection is kept open between the server and the browser. When an update arrives the connection is closed and a new one is opened.
Streaming connections offer the best user experience, but are more complicated to implement and are less tolerant of proxies and firewalls.
Why use <script> tag long polling
I decided to build my solution over the polling <script> tag method. It is a very simple idea: a <script> element is dynamically created and added to the <head> of the document. The src of this tag points to the subscription channel of the PubSub server. When the server wants to notify the client of new data it sends a JSONP string that invokes an existing method in the client. After processing the data this method starts a new connection to the server.
This method allows full cross domain requests and it is very broadly supported. After all it is based on the same concepts that enable online advertising, which depend on the browser allowing to load script sources from domains that are different from the originating one. I needed this feature since my hosting setup is very simple and I cannot unify different servers behind a load balancer like HAproxy.
Why not use <script> tag long polling
The biggest problem of long polling a script load is the lack of control over the connection. There is no feedback on the status of the load by the browser. onload callbacks are only supported by Firefox and Opera, Internet Explorer supports onreadystatechange, and Safari does not report anything. The callback call of the JSONP string makes it possible to detect a successful load. But a timed out or cancelled connection just fails silently.
Solving the timeout issue
The solution is to not wait for a timeout condition. In Peak Notes the client code deliberately "forgets" everything about the <script> load after 45 seconds, and starts a new one from scratch. An unique sequence number is sent along the request so when a stale script loads and invokes the JSONP callback can decide if the pushed data must be honored or just ignored. This means that for browsers that keep the script load active even when the <script> tag has been removed from the DOM (like Firefox) the application just discards anything that was sent along with it.
The server always sends a response and closes the connection after 60 to 65 seconds, even if there is no data to send to the client. This means that at most there are one stale and one valid connection concurrently. The stale connections get sent either no data all (on the server side timeout at 60 seconds) or redudant data that will be accepted on the connection with the valid sequence number (on a real push notification). The valid connections will always reinitiate the <script> load with a different, newer sequence number.
Implementation
ModelManager.startComet is the model method that starts the Comet connection. Its is called once during startup and then again in the Comet finalization callback.
ModelManager.prototype = {
...
startComet: function() {
getCometJSON('http://pubsub.example.com:8080/subscriptions/channel/'
+ globalModelManager.userModel.subscriptionChannel);
},
...
};
cometSerial is incremented by one on every new connection, cometRunID is a random number calculated during load and cometValidStamp contains the last valid sequence ID of the last initiated connection.
window.cometSerial = 0;
window.cometRunID = Math.floor((Math.random())*1000000);
window.cometValidSeq = null;
These variables are used for timing the connections and for detecting concurrent getCometJSON calls.
window.cometStartTime = 0;
window.cometEndTime = 0;
window.cometLastInterval = 0;
window.cometLastReason = 'juststarted';
cometFinalizeCB contains a closure with the local state of the last initiated connection.
window.cometFinalizeCB = null;
getCometJSON sets up a new <script> tag and a new closure for removing the tag and starting a new one. It can be called by the internal 45 seconds timeout or after a valid JSONP callback, whatever arrives first.
function getCometJSON(url) {
if (window.cometStartTime > 0)
return;
var nowd = new Date();
var now = nowd.getTime();
// stats
window.cometStartTime = now;
window.cometEndTime = 0;
window.cometSerial ++;
// add the new script tag
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
// calculate the new sequence ID
window.cometValidSeq = 'W' + window.cometRunID + '_' + window.cometSerial + '_' + now;
script.src = url + '?seq=' + window.cometValidStamp;
var timerObj = null;
// prepare the closure. all the manipulations occur in the lexical scope
// of the getCometJSON invocation
var loadCB = function() {
// whatever happens, delete the tag
head.removeChild(script);
script.src = '';
script = null;
// remove the timer obj
if (timerObj != null) {
clearTimeout(timerObj);
timerObj = null;
}
if (window.cometEndTime == 0) {
// the callback was never called
var nowd = new Date();
window.cometEndTime = nowd.getTime();
window.cometLastInterval = window.cometEndTime - window.cometStartTime;
window.cometStartTime = 0;
window.cometLastReason = 'closure';
}
// we are ready for a new connection
globalModelManager.startComet();
};
window.cometFinalizeCB = loadCB;
timerObj = setTimeout(loadCB, 45000);
head.appendChild(script);
}
remoteJSONReady is the JSONP callback.
function remoteJSONReady(data) {
if (data != null)
if ("seq" in data)
if (data.seq != window.cometValidSeq) {
// data.seq contains the sequence ID this script load was called with.
// if we are here it means this callback was done from a stale connection
return;
}
// stats
var nowd = new Date();
window.cometEndTime = nowd.getTime();
window.cometLastInterval = window.cometEndTime - window.cometStartTime;
window.cometStartTime = 0;
window.cometLastReason = 'callback';
// if we have valid data pass it to the data model.
// subject/observer pairs will do the rest
if (data != null)
if ("status" in data)
if (data.status == "changed") {
globalModelManager.newPushData(data);
}
// call the current closure
window.cometFinalizeCB();
}
Future improvements
The polling method works best when it is invoked from a XMLHTTPRequest connection. This interface has full connection status reporting and it is possible to detect a connection timeout or even cancel the connection from the client code, and it has been present in all mayor browsers for years now. For security reasons it requires connections to be made to the same domain as the calling document. An efficient front end server like nginx proxying to the application and the PubSub server, or a load balancer like HAproxy is required to effectively multiplex a single domain between multiple servers.
Coach Wei publishes a nice writeup of the recent voting on a features wish list at OpenAjax. Just lifting the limit of 2 concurrent XHR requests and adding native JSON parsing would be huge.
On the same vein it appears that Firefox 3 has an extension for native JSON parsing. It's called nsIJSON. Definitely going to use it in the future.
