Проблема


Было приложение, использующее Ruby on Rails, и стандартный набор гемов (вроде devise). На одной из страниц необходимо было выводить информацию о текущих активных пользователях.

Решение


Первой же мыслью было при каждом запросе записывать в текущего юзера время этого самого запроса и, таким образом, зная таймаут сессии, можно было вычислить, кто активен, а кто нет. Но таймаут стоял порядка 15 минут, поэтому если вкладку просто закрыли — то он все еще будет «активным» на протяжении этого времени. Уменьшать таймаут сессии было нельзя. Да и вариант каждый раз обновлять запись в базе выглядел немного костыльно, учитывая, что одновременных активных юзеров было порядка 2к. Из самых быстрых и простых вариантов — реализация используя вебсокеты + redis.

Faye vs WebsocketRails


tldr; В итоге был выбран faye.

Изначальный выбор был предоставлен двумя вариантами. Гугление ответ на вопрос что лучше не дало, поэтому все плюсы и минусы — это то, что удалось накопать из доков и статей.

Плюсов у websocket-rails я не нашел, зато минусы были очевидные: последнее обновление было довольно давно, на каждое подключение открывался отдельный поток, что потенциально могло заддосить наш и так не очень мощный сервер. Faye в свою очередь работает через event machine и полностью асинхронный, плюс постоянно обновляется.

Установка


Gemfile:

gem "hiredis", "~> 0.4.0"
gem 'redis'
gem 'faye'
gem 'faye-rails'


Настройка


В initializers/redis.rb была добавлена инициализация подключения к redis:

Redis.current = Redis.new(host: 'localhost', port: 6379, driver: :hiredis)

application.rb

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


В этом куске происходит подключение faye по урлу '/faye', и указание таймаута, что очень было важно в решении данной задачи. А так же маппинг канала на определенный обработчик, в моем случае это был ActiveUsersController. Так же добавил расширение для файе. Его код выглядит примерно так:

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

Это дало мне возможность узнавать кто отправил запрос на '/faye'. Внутри OnlineUsers было просто добавление id и client_ud (который выдается faye, при коннекте)юзера в редис внутрь хеша, что то вроде:

redis.hset(HASH_KEY, client_id, user_id)


чтобы можно было достать всех активных просто по ключу хеша.

Так же в контроллере сделал монитор события «unsubscribe», которое по идее должно было срабатывать, когда закрывается вкладка, но на практике срабатывало через раз. Так же срабатывало когда пользователь кликал на логаут и после клика удалял из редиса нашего клиента и по истечении таймаута, когда от клиента не слышно ничего.

channel '/channel_name' do
  monitor :unsubscribe do
    remove_online_user(client_id)
  end
end

На фронте был простой скрипт:

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

Для faye был поднят отдельный thin сервер, который слушал только порт на котором вещал faye. Таким образом, получилось сделать возможность мониторинга онлайн пользователей с дельтой в 30 секунд.

В итоге, что бы получить список id всех онлайн юзеров достаточно

redis.hgetall(HASH_KEY).values.uniq

Комментарии (12)


  1. AMar4enko
    24.04.2015 02:30

    Авторизацию для faye делать не стали?


    1. avdept Автор
      24.04.2015 10:06

      Что вы имеете ввиду под автоматизацией?


      1. Dlussky
        24.04.2015 13:04
        +1

        Авторизация != автоматизация, %username%!


        1. avdept Автор
          24.04.2015 13:05

          Точно, пропустил.


    1. avdept Автор
      24.04.2015 13:06

      Нет, т.к. не было в этом нужды.


  1. ZloyDyadka
    24.04.2015 14:18

    А если приложение упадет, то ведь при новом запуске приложения все эти пользователи останутся в сети. Очищаете базу при запуске?


    1. avdept Автор
      24.04.2015 14:45

      Для файе запущен отдельный thin сервер, и в случае если основное приложение падает, то файе, все равно работает, и по истечению таймаута удалит всех неактивных. Если же упадет сам thin сервер(который для файе) — то да, тут нету никаких запасных путей. Добавлю


  1. organium
    24.04.2015 19:41

    Есть еще вот такое решение github.com/igrigorik/em-websocket.
    А авторизация все же нужна, иначе к вам любой может подцепиться и сделать любого пользователя online.


  1. GearHead
    25.04.2015 09:32

    В теме с websockets-rails вы, похоже, так и не разобрались. Он сделан поверх faye, поэтому работает абсолютно также: в standalone-режиме требует запуска EM-сервера типа Thin и редиса.


    1. Envek
      25.04.2015 20:40

      Но проблем с ним очень много. Функциональность из коробки потрясающая (например, можно отправлять данные конкретному пользователю, поскольку websocket-rails из коробки автомагически отслеживает пользователей через метод current_user), но заставить его работать хорошо очень сложно. И багов хватает. И разработка еле теплится. И как делать функциональные тесты (браузерные) с ним — так до сих пор непонятно.


      1. GearHead
        03.05.2015 17:46

        За долгое время использования ни разу не натыкался на баги (кроме багов в своей голове с непониманием схемы работы вебсокетов). Сейчас у меня один инстанс на проекте обрабатывает больше сотни конкурентных подключений (правда, без DataStorage и синхронизации: у меня своя бизнес-логика с редисом).

        А по поводу faye-rails — попробуйте без танцев с бубном получить из контроллера доступ к сессии (а между прочим, куки отправляются при websocket-хендшейке).

        Олсо, совет на будущее: redis-objects — довольно гибкая и приятная вещь.


  1. Zex0n
    17.05.2015 09:49

    В redis можно указать время жизни записи. В схожей с вашей задаче я использовал эту фишку для автоматического удаления из базы пользователя если он долго не проявляет активности. Также это решило проблему с несработавшим unsubscribe. Чтобы мониторить активность, раз минуту отправляю пустое сообщение через faye сервер, который при получении любого сообщения продлевает время жизни записи на 3 минуты.