Фон: Мы создали функцию чата в одном из наших существующих приложений Rails. Мы используем новый модуль ActionController::Live
и запускаем Puma (с выпуском Nginx) и подписываемся на сообщения через Redis. Мы используем клиентскую сторону EventSource
, чтобы установить соединение асинхронно.
Резюме проблемы: Нити никогда не умирают, когда соединение завершается.
Например, если пользователь переместится, закройте браузер или даже перейдете на другую страницу в приложении, появится новый поток (как и ожидалось), но старый продолжает жить.
Проблема, как я сейчас вижу, заключается в том, что когда возникает какая-либо из этих ситуаций, сервер не знает, завершается ли соединение в конце браузера, пока что-то не попытается написать этот сломанный поток, чего никогда не произойдет как только браузер уйдет с исходной страницы.
Эта проблема представляется документированной в github, и подобные вопросы задаются в StackOverflow здесь (довольно точно такой же вопрос) и здесь (относительно получения количества активных потоков).
Единственное решение, с которым я смог придумать, основываясь на этих сообщениях, - это реализовать тип ниток/подключение покера. Попытка написать сломанное соединение создает IOError
, который я могу уловить и правильно закрыть соединение, позволяя потоку умереть. Это код контроллера для этого решения:
def events
response.headers["Content-Type"] = "text/event-stream"
stream_error = false; # used by flusher thread to determine when to stop
redis = Redis.new
# Subscribe to our events
redis.subscribe("message.create", "message.user_list_update") do |on|
on.message do |event, data| # when message is received, write to stream
response.stream.write("messageType: '#{event}', data: #{data}\n\n")
end
# This is the monitor / connection poker thread
# Periodically poke the connection by attempting to write to the stream
flusher_thread = Thread.new do
while !stream_error
$redis.publish "message.create", "flusher_test"
sleep 2.seconds
end
end
end
rescue IOError
logger.info "Stream closed"
stream_error = true;
ensure
logger.info "Events action is quitting redis and closing stream!"
redis.quit
response.stream.close
end
(Примечание: метод events
, кажется, блокируется при вызове метода subscribe
. Все остальное (потоковая передача) работает правильно, поэтому я предполагаю, что это нормально.)
(Другое примечание: концепция потока flusher имеет больше смысла как один длительный фоновый процесс, немного похожий на сборщик мусорных потоков. Проблема с моей реализацией выше заключается в том, что для каждого соединения создается новый поток, который бессмысленно. Любой, кто пытается реализовать эту концепцию, должен сделать это больше как один процесс, а не столько, как я изложил. Я обновлю этот пост, когда я успешно повторю его реализацию как один фоновый процесс.)
Недостатком этого решения является то, что мы только задерживали или уменьшали проблему, а не полностью ее устраняли. У нас все еще есть 2 потока на пользователя, в дополнение к другим запросам, таким как ajax, который кажется ужасным с точки зрения масштабирования; он кажется совершенно недосягаемым и нецелесообразным для более крупной системы со многими возможными параллельными соединениями.
Я чувствую, что мне не хватает чего-то жизненно важного; Мне трудно поверить, что у Rails есть функция, которая настолько явно сломана, не реализуя настраиваемый контролер соединений, как я это делал.
Вопрос:. Как мы разрешаем соединениям/потокам умирать, не реализуя что-то банально, например, "покер покера" или сборщик мусора?
Как всегда, дайте мне знать, если я что-то оставил.
Обновление
Просто добавьте немного дополнительной информации: Huetsch over at github опубликовал этот комментарий, указав, что SSE основан на TCP, который обычно отправляет FIN пакет, когда соединение закрыто, что позволяет другому концу (сервер в этом случае) знать, что его безопасно закрыть соединение. Huetsch указывает, что либо браузер не отправляет этот пакет (возможно, ошибка в библиотеке EventSource
?), Либо Rails не ловит его или ничего не делает с ним (определенно, ошибка в Rails, если это так). Поиск продолжается...
Другое обновление Используя Wireshark, я действительно могу увидеть пакеты FIN, отправляемые. По общему признанию, я не очень осведомлен или опыт работы с уровнем протокола, однако из того, что я могу сказать, я определенно обнаруживаю, что пакет FIN, отправляемый из браузера, когда я устанавливаю SSE-соединение с использованием EventSource из браузера, и NO-пакет отправляется, если я удалите это соединение (что означает отсутствие SSE). Хотя я не ужасно осведомлен о своих знаниях TCP, это, по-видимому, указывает мне, что соединение действительно заканчивается клиентом; возможно, это указывает на ошибку в Puma или Rails.
Еще одно обновление
@JamesBoutcher/boutcheratwest (github) указал мне на обсуждение на веб-сайте redis относительноэтот вопрос, особенно в связи с тем, что метод .(p)subscribe
никогда не закрывается. Плакат на этом сайте указал то же самое, что мы обнаружили здесь, что среда Rails никогда не уведомляется, когда клиентское соединение закрыто и поэтому не может выполнить метод .(p)unsubscribe
. Он спрашивает о тайм-ауте для метода .(p)subscribe
, который, как я думаю, будет работать, хотя я не уверен, какой метод (связанный покер, который я описал выше, или его предложение тайм-аута) будет лучшим решением. В идеале, для решения для подключения покера я хотел бы найти способ определить, закрыто ли соединение на другом конце без записи в поток. Как сейчас, как вы можете видеть, мне нужно реализовать код на стороне клиента, чтобы обрабатывать мое "выталкивающее" сообщение отдельно, что, по моему мнению, навязчиво и тупо, как черт.