Jugglite
Jugglite is a replacement for the incredible Juggernaut by Maccman. It uses Server Sent Events to push events from your application to the client's browser. It uses Redis for publish/subscribe and Thin + EventMachine to run an evented server that can handle 10K+ concurrent connections.
Installation
Add this line to your application's Gemfile:
gem 'jugglite'
And then execute:
$ bundle
Or install it yourself as:
$ gem install jugglite
Server Usage
I used to use Jugglite as rack middleware in development and as a standalone binary behind nginx in production. Nowadays I run my rails application using thin in production so I can mount Jugglite in the routes.rb file.
Inside Rails's routes.rb
This only works with an EventMachine based webserver that supports rack's async.callback. I have only tested this in production with Thin, but it might work with Rainbows or Puma.
This setup is great because it allows you to do channel authorization on a per request basis.
# config/routes.rb
# ...
@allowed_channels = ->(request) {
user_id = request.session['user_id']
user_id ? ['broadcast', "player_#{user_id}"] : []
}
@on_register = ->(connection) {
# Store something in connection.data
# connection.request holds the Rack::Request
}
@on_unregister = ->(connection) {
# Called when the connection is dropped or closed.
# You can access the connection.data set in the +on_register+ call
# Or use the connect.request.
}
get 'stream', to: Jugglite::App.new(nil, namespace: "app:#{Rails.env}:", allowed_channels: @allowed_channels, on_register: @on_register, on_unregister: @on_unregister)
# ...
Stand-alone binary
Jugglite comes with a binary. This binary runs a thin server that listens on redis for application messages and passes it along to all connected clients.
You can run the binary from any terminal like this (these options are the defaults):
jugglite --address 0.0.0.0 --port 3000 --max-conns 1024
As Rack middleware
Add it to your config.ru
file and make sure your application runs using Thin:
require ::File.expand_path('../config/environment', __FILE__)
# Embed Jugglite when running in development
use Jugglite::App, path: '/stream', namespace: 'myapp:' if ENV['RACK_ENV'] == 'development'
run MyRails::Application
Behind Nginx
NOTE: because the html5 SSE implementation requires the connection to have the same hostname and port, you'll need to add a reverse proxy in front of your app and jugglite.
This is an example nginx configuration with Unicorn and Jugglite. Make sure you set proxy_buffering off;
in your Nginx configuration.
# Start jugglite with: jugglite --socket /tmp/jugglite.sock
upstream jugglite-example {
server unix:/tmp/jugglite.sock fail_timeout=0;
}
# Start unicorn with: unicorn --listen /tmp/unicorn.sock --config-file unicorn_conf.rb
upstream unicorn-example {
server unix:/tmp/unicorn.sock fail_timeout=0;
}
server {
listen [::]:80 deferred;
server_name example.com;
root /var/www/example/current/public;
# Let Nginx serve assets statically
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
# Forward /stream to Jugglite and set proxy_buffering off
location ^~ /stream {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://jugglite-example;
}
# Forward all other requests to Unicorn
try_files $uri/index.html $uri @unicorn;
location @unicorn {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://unicorn-example;
}
}
Client Usage
Use the browser's native Server-Sent Events implementation:
es = new EventSource('/stream?channel=yourchannelname');
es.addEventListener('message', function(e) {
// Do something with the data
console.log(e.data);
// If you JSON encoded the message
msg = jQuery.parseJSON(e.data);
}, false);
es.onopen = function(e) {
// Connection was opened.
};
es.onerror = function(e) {
if (e.readyState == EventSource.CLOSED) {
// Connection was closed.
} else {
// Some other error?
};
};
To support older browsers, use Remy's excellent Pollyfill. It does revert to ajax long polling for browsers without a native EventSource implementation. Supports almost every old browser (even IE7).
Sending messages
Use your favorite Redis client to simply publish messages to the channel your clients are subscribing to:
redis = Redis.new
redis.publish('yourchannelname', 'This is a message')
# You may want to JSON encode your data
redis.publish('yourchannelname', {hello: 'world', number: 47}.to_json)
Performance
It's been tested on a local machine with the spec/benchmark/max_connections.rb
spec up to 16K concurrent connections.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
License
Jugglite is licensed under the MIT license.