Ruby on rails 中间件中运行的线程正在使用旧版本的父';s实例变量

Ruby on rails 中间件中运行的线程正在使用旧版本的父';s实例变量,ruby-on-rails,multithreading,ruby-on-rails-4,puma,rack-middleware,Ruby On Rails,Multithreading,Ruby On Rails 4,Puma,Rack Middleware,我曾经实现过WebSocket 它适用于瘦,但不适用于独角兽和美洲狮 还实现了一个echo消息,它响应客户端的消息。它在每台服务器上都能正常工作,因此WebSocket实现没有问题 Redis设置也正确(它捕获所有消息,并执行subscribe块中的代码) 它现在是如何工作的: 在服务器启动时,将初始化空的@clients数组。然后启动新线程,该线程正在侦听Redis,并打算从@clients数组将该消息发送给相应的用户 在页面加载时,将创建新的websocket连接,并将其存储在@client

我曾经实现过WebSocket

它适用于瘦,但不适用于独角兽和美洲狮

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

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

它现在是如何工作的:

在服务器启动时,将初始化空的
@clients
数组。然后启动新线程,该线程正在侦听Redis,并打算从@clients数组将该消息发送给相应的用户

在页面加载时,将创建新的websocket连接,并将其存储在@clients数组中

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

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

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

  • 如果使用Puma/Unicorn运行,@clients数组始终为空,即使我们按该顺序尝试(不重新加载页面或任何操作):

  • 从浏览器发送消息->
    @客户端。长度
    为1,消息已传递
  • 通过Redis->
    @客户端发送消息。长度
    为0,消息丢失
  • 从浏览器发送消息->
    @客户端。长度仍然为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
独角兽(显然还有美洲狮)都启动了一个主流程,然后雇佣一个或多个工人。fork复制(或者至少表现出复制的假象——实际的复制通常只在您写入页面时发生)您的整个进程,但新进程中只存在调用
fork
的线程

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


您可能可以通过延迟redis线程的创建或禁用应用程序预加载来解决此问题,但是您应该知道,您的设置将阻止您扩展到单个工作进程之外(使用puma和jruby等线程友好的JVM,这对您来说不是什么限制)为了防止有人面临同样的问题,我提出了两种解决方案:

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

只需删除
preload\u应用程序来自puma.rb文件。因此,所有线程都将有自己的
@客户端
变量。并且它们可以通过其他中间件方法(如
call
等)访问

缺点:您将失去应用程序预加载的所有好处。如果你只有一到两个工作线程,这是可以的,但是如果你需要很多线程,那么最好是应用程序预加载。所以我继续我的研究,这里是另一个解决方案:

2。将线程初始化移出
initialize
method
(这就是我现在使用的)

例如,我将其移动到
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侦听线程将针对每个Puma工作线程/线程初始化,而不是针对主进程(实际上不服务于请求)。

如果在Redis线程接收事件时记录进程id,并且在修改@clients时,是否得到相同的值?@FrederickCheung刚刚检查过,他们是不同的。Initialize方法和Redis侦听器线程具有相同的PID,但它与修改
@clients
的PID不同(更低)。顺便说一句,所有客户端都存储在同一个进程中(它们都属于相同的PID和
@clients
数组),感谢您的解释。至于禁用应用程序预加载(虽然我不喜欢这个想法,但现在还可以):据我所知,它将引发的唯一问题与.message上的
块有关。因此,如果我将在那里收到的消息发布到Redis,所有工作人员都将收到该消息,这将允许我扩展到多个工作人员。还有什么我不知道的吗?我不太明白你在做什么,但是当你依赖于共享一些只存储在内存中的变量时,你添加更多进程的能力就会受到限制