在Middleware中运行的线程正在使用旧版本的父实例变量

我使用Heroku教程来实现websockets。

它适用于Thin,但不适用于Unicorn和Puma。

还有一个回应消息实现,它响应客户端的消息。 它在每个服务器上都能正常工作,因此websockets实现没有问题。

Redis设置也是正确的(它捕获所有消息,并执行subscribe块内的代码)。

它现在如何工作:

在服务器启动时,初始化一个空的@clients数组。 然后启动新的Thread,它正在侦听Redis,它旨在从@clients数组向相应的用户发送该消息。

在页面加载时,会创建新的websocket连接,它存储在@clients数组中。

如果我们从浏览器收到消息,我们会将其发送回与同一用户连接的所有客户端(该部分在Thin和Puma上都正常工作)。

如果我们收到Redis的消息,我们也会查找存储在@clients数组中的所有用户连接。 这是奇怪的事情发生的地方:

  • 如果使用Thin运行,它会在@clients数组中找到连接并将消息发送给它们。

  • 如果使用Puma / Unicorn运行,@ client数组总是为空,即使我们按该顺序尝试它(没有页面重新加载或任何东西):

    1. 从浏览器发送消息 – > @clients.length为1,传递消息
    2. 通过Redis发送消息 – > @clients.length为0,消息丢失
    3. 从浏览器发送消息 – > @clients.length仍为1,消息已发送

请问有人请告诉我我错过了什么?

Puma服务器的相关配置:

 workers 1 threads_count = 1 threads threads_count, threads_count 

相关中间件代码:

 require 'faye/websocket' class NotificationsBackend def initialize(app) @app = app @clients = [] Thread.new do redis_sub = Redis.new redis_sub.subscribe(CHANNEL) do |on| on.message do |channel, msg| # logging @clients.length from here will always return 0 # [..] retrieve user send_message(user.id, { message: "ECHO: #{event.data}"} ) end end end end def call(env) if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on :open do |event| # [..] retrieve current user if user # add ws connection to @clients array else # close ws end end ws.on :message do |event| # [..] retrieve current user Redis.current.publish({user_id: user.id, { message: "ECHO: #{event.data}"}} ) end ws.rack_response else @app.call(env) end end def send_message user_id, message # logging @clients.length here will always return correct result # cs = all connections which belong to that client cs.each { |c| c.send(message.to_json) } end end 

Unicorn(显然是puma)都启动了一个主进程,然后分叉一个或多个worker。 fork副本(或至少表示复制的假象 – 实际副本通常只在您写入页面时发生)整个过程,但只有在新进程中存在调用fork的线程。

很明显,您的应用程序在分叉之前正在初始化 – 这通常是为了让工作人员可以快速启动并从写入内存节省的副本中受益。 因此,您的redis检查线程仅在主进程中运行,而@clients正在子进程中进行修改。

您可以通过推迟创建redis线程或禁用应用程序预加载来解决这个问题,但是您应该知道,您的设置将阻止您扩展到单个工作进程之外(使用puma和线程友好的JVM,如jruby会少受约束)

为了防止有人遇到同样的问题,我提出了两个解决方案:

1.禁用应用程序预加载 (这是我提出的第一个解决方案)

只需删除preload_app! 来自puma.rb文件。 因此,所有线程都有自己的@clients变量。 并且可以通过其他中间件方法(如call等)访问它们

缺点 :你将失去app预加载的所有好处。 如果你只有1或2个工作人员有几个线程,那就没关系,但是如果你需要很multithreading,那么最好有app预加载。 所以我继续我的研究,这是另一个解决方案:

2.将线程初始化移出initialize方法 (这是我现在使用的)

例如,我将它移动到call方法,所以这就是中间件类代码的样子:

 attr_accessor :subscriber def call(env) @subscriber ||= Thread.new do # if no subscriber present, init new one redis_sub = Redis.new(url: ENV['REDISCLOUD_URL']) redis_sub.subscribe(CHANNEL) do |on| on.message do |_, msg| # parsing message code here, retrieve user send_message(user.id, { message: "ECHO: #{event.data}"} ) end end end # other code from method end 

两种解决方案都解决了同样的问题:Redis-listening线程将为每个Puma工作者/线程初始化,而不是主进程(实际上不提供请求)。