Why are we here?
We all enjoy fast and responsive apps which update in near real time without the need for javascript long poling or having to manually refresh the page.
I'm here to share some of the joy I've had working with Node.js's socket.io library to provide an easy to use WebSocket API.
Now you are thinking, why are you talking about JavaScript on the server side, when this is a Rails group. Not all problems require the same solution, as if you rely on strict cookie cutter monkey see monkey do coding you will wind up in a world of hurt. Yes, there are high performing Ruby solutions which sit on top of EventMachine that can accomplish pushing updates to the client. The main reason for sticking with Socket.io for this discussion is the fact that EventMachine only performs well with a fairly small number of concurrent request.
You may have seen references to https://gist.github.com/Evangenieur/889761 that indicate EventMachine is faster than Node.js:
node server.js
ab -n 10000 -c 100 http://localhost:3000/
-> 20ms / request
ruby em_http_serv.rb
ab -n 10000 -c 100 http://localhost:3000/
-> 12ms / request
Out of curiosity additional benchmarks have been performed using Apache's jmeter (https://gist.github.com/ccyphers/5620898):
JMeter report against Node
No HTTP error reported after several executions
sampler_label |
aggregate_report_count |
aggregate_report_error% |
Request/Sec |
HTTP Request |
20000 |
0 |
8952.551477171 |
TOTAL |
20000 |
0 |
8952.551477171 |
JMeter report against EventMachine
After trying numerous executions I was unable to obtain results where all 20000 http request completed without error and many of the executions failed after just a few thousand request. The best results I could produce after many executions:
sampler_label |
aggregate_report_count |
aggregate_report_error% |
Request/Sec |
HTTP Request |
19070 |
0.0640797063 |
1323.7539913925 |
TOTAL |
19070 |
0.0640797063 |
1323.7539913925 |
Don't Drink the Cool-Aid
Just because the cool kids are using it should you?
While developing solutions utilizing third party libraries out there does save you time, if you don't consider the performance ramifications at every step of development, not only will your solution not scale well, but even if it can be scaled with additional servers, it will cost you more in hosting cost than your developers hourly wages compared to properly thought-out implementations.
Using what's popular at the time with performance blinders on is a little like trying to eat a civilized meal during a food fight:
Why bring up the performance rant?
Don't get me wrong I am an avid Ruby developer. At the same time, by keeping in mind a few techniques we can all have the flexibility Ruby provides in rapid application development with less re-factor/re-write work in the future if an application takes off and has a huge user base.
def create post = Post.find(params[:post_id]) comment = post.comments.create(params.permit(:commenter, :body, :post_id))
res = {:action => 'new', :comment => comment.attributes.merge(:post_id => post.id)} redis.publish("comments_for_post_#{post.id}", res.to_json) render :json => {:results => comment.errors.empty?}.to_json end
this.io.sockets.on('connection', options.connect_callback);
this.subscribe = function(room, push_emitter) {
if(this.rooms.indexOf(room, push_emitter) == -1) {
this.rooms.push(room);
this.client.subscribe(room);
this.client.on('message', function(channel, msg) {
console.log("MSG: " + msg)
console.log(room);
console.log("EMIT: " + push_emitter);
self.io.sockets.in(room).emit(push_emitter, msg);
});
}
}
queue_socket_bridge_options = {
http_server: server,
authorization: function(data, accept) {
console.log(data);
if(data.headers.cookie) {
cook = data.headers.cookie.split("express.sid")
if(cook.length == 2) {
key = cook[1];
data.client = key;
} else {
return accept(null, false);
}
} else {
return accept(null, false);
}
return accept(null, true);
},
connect_callback: function (socket) {
client_key = socket.handshake.client;
socket.client_key = client_key;
self.client_connections[client_key] = {'socket': socket, 'room': ''};
socket.on('comments_for_post', function(data) {
room = 'comments_for_post_' + String(data);
console.log("ROOM: " + room);
if(self.client_connections[client_key].room != room) {
// leave the old room
socket.leave(self.client_connections[client_key].room);
console.log("in another room, must leave");
// join the new room
socket.join(room);
self.client_connections[client_key].room = room
}
//self.client_connections[client_key].socket.join(room);
self.subscribe(room, 'comment_data');
});
}
}
require('./lib/queue_socket_bridge').QueueSocketBridge(
comment_client, queue_socket_bridge_options);