背景:我们已经在我们现有的 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 线程概念作为单个长时间运行的后台进程更有意义,有点像垃圾线程收集器。我上面实现的问题是为每个连接生成一个新线程,这是没有意义的。任何人尝试实现这个概念应该更像一个单一的进程,而不是像我概述的那样。当我成功地将它重新实现为一个单一的后台进程时,我会更新这篇文章。)
这个解决方案的缺点是我们只是延迟或减少了问题,并没有完全解决它。除了 ajax 等其他请求外,我们每个用户仍然有 2 个线程,从扩展的角度来看,这似乎很糟糕;对于具有许多可能的并发连接的大型系统来说,这似乎是完全无法实现和不切实际的。
我觉得我错过了一些重要的东西;如果没有像我一样实现自定义连接检查器,我觉得有点难以相信 Rails 有一个明显被破坏的特性。
问题:我们如何允许连接/线程终止而不实现诸如“连接扑克”或垃圾线程收集器之类的老生常谈?
像往常一样让我知道我是否遗漏了任何东西。
更新
只是为了添加一些额外的信息:在 github 上的 Huetsch 发布了这条评论,指出 SSE 基于 TCP,它通常在连接关闭时发送一个 FIN 数据包,让另一端(在这种情况下为服务器)知道关闭连接是安全的。Huetsch 指出浏览器没有发送该数据包(可能是EventSource
库中的错误?),或者 Rails 没有捕获它或对其进行任何处理(如果是这种情况,肯定是 Rails 中的错误)。搜索还在继续……
使用 Wireshark 的另一个更新 ,我确实可以看到正在发送的 FIN 数据包。诚然,我对协议级别的东西不是很了解或经验丰富,但是据我所知,当我使用来自浏览器的 EventSource 建立 SSE 连接时,我肯定检测到从浏览器发送的 FIN 数据包,如果我没有发送数据包删除该连接(意味着没有 SSE)。虽然我对 TCP 的了解并不十分了解,但这似乎向我表明客户端确实正确终止了连接;也许这表明 Puma 或 Rails 中存在错误。
另一个更新
@JamesBoutcher / boutcheratwest(github) 向我指出了 redis 网站上关于这个问题的讨论.(p)subscribe
,特别是关于该方法永远不会关闭的事实。该站点上的发布者指出了与我们在这里发现的相同的事情,即当客户端连接关闭时,Rails 环境永远不会收到通知,因此无法执行该.(p)unsubscribe
方法。他询问超时.(p)subscribe
方法,我认为也可以,但我不确定哪种方法(我上面描述的连接扑克,或他的超时建议)会是更好的解决方案。理想情况下,对于连接扑克解决方案,我想找到一种方法来确定连接是否在另一端关闭而不写入流。正如你所看到的那样,我必须实现客户端代码来分别处理我的“戳”消息,我认为这很突兀和愚蠢。