Who is logged in?

Problem

We have an app built using ruby on rails with default Gemfile(devise, pg, etc). And one page, where we have to print all logged in users.

Solution

The first thought was to save timestamp of each request to user profile in DB. Thus knowing the session timeout, we can figure out who is active and who is not. But the timeout was about 15 minutes, so if you simply closed tab – it will still be “active” during this time. Reducing the session’s timeout was impossible.

Also  updating user record in the database each time looked a little “dumb”, considering large number of simultaneous active users(about 2k). One of the fastest and easiest options – implementation using websockets + redis.

Faye vs WebsocketRails

tldr; In result we used Faye

Originally we had 2 options. Googling about which one is better didn’t give us reasonable answer, so all pros and cons were collected from multiple different articles and docs.

Actually I was unable to find obvious pros for websocket-rails. But there were few cons:

  • latest update was pretty long time ago
  • each connection opens new thread, what could potentially ddos our server in case of many connections.

Faye however works using EventMachine, completely async, and updates frequently.

Installation

Add to your Gemfile:

[ruby]
gem "hiredis", "~> 0.4.0"
gem ‘redis’
gem ‘faye’
gem ‘faye-rails’
[/ruby]

 

Config

To initializers/redis.rb I’ve added redis connection:

[ruby]
Redis.current = Redis.new(host: ‘localhost’, port: 6379, driver: :hiredis)
[/ruby]

application.rb:

[ruby]
config.middleware.delete Rack::Lock
config.middleware.use FayeRails::Middleware, mount: ‘/faye’, timeout: 25 do
map ‘/active_users’ => ActiveUsersController
add_extension(Inc.new)
end
[/ruby]

In this piece we connecting faye to url ‘/faye`, and setting up timeout, which was needed in another situation. Also mapped channel to specific controller, in this case – ActiveUsersController. Also added extension for faye, code below:

[ruby]
class Inc
def incoming(message, _request, callback)
if message["channel"] == "/active_users"
OnlineUsers.new(message["data"]["id"], message["clientId"]).online!
end
callback.call(message)
end
end
[/ruby]

This gave me ability to figure out who send request to `/faye`. Inside OnlineUsers was just simple adding id and client_id(which faye gives to each use) inside redis hash:

[ruby]
redis.hset(HASH_KEY, client_id, user_id)
[/ruby]

So later we could retrieve all active users by hash key.

Then i’ve added monitor for ‘unsubscribe’ event, which fired when user logs out manually via button, or timed out:

[ruby]
channel ‘/channel_name’ do
monitor :unsubscribe do
remove_online_user(client_id)
end
end
[/ruby]

Front-end contained simple js script like:

[js]
client = new Faye.Client(‘/faye’);
client.subscribe("/active_users", function(message){})
client.publish(‘/active_users’, {id: user_id});
client.disable(‘autodisconnect’);
[/js]

For faye we’ve setup standalone thin server, which listened only port specified for faye.

Thus we’ve got ability to monitor who is online within 30 seconds delta

To retrieve all active users ids we had to do

[ruby]
redis.hgetall(HASH_KEY).values.uniq
[/ruby]