Ruby on rails 中间件中运行的线程正在使用旧版本的父';s实例变量
我曾经实现过WebSocket 它适用于瘦,但不适用于独角兽和美洲狮 还实现了一个echo消息,它响应客户端的消息。它在每台服务器上都能正常工作,因此WebSocket实现没有问题 Redis设置也正确(它捕获所有消息,并执行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
subscribe
块中的代码)
它现在是如何工作的:
在服务器启动时,将初始化空的@clients
数组。然后启动新线程,该线程正在侦听Redis,并打算从@clients数组将该消息发送给相应的用户
在页面加载时,将创建新的websocket连接,并将其存储在@clients数组中
如果我们从浏览器收到消息,我们会将其发送回与同一用户连接的所有客户端(该部分在Thin和Puma上都正常工作)
如果我们收到来自Redis的消息,我们还将查找存储在@clients数组中的所有用户连接。
这就是奇怪的事情发生的地方:
- 如果使用Thin运行,它会在@clients数组中找到连接并将消息发送给它们
- 如果使用Puma/Unicorn运行,@clients数组始终为空,即使我们按该顺序尝试(不重新加载页面或任何操作):
- 从浏览器发送消息->
@客户端。长度
为1,消息已传递
- 通过Redis->
@客户端发送消息。长度
为0,消息丢失
- 从浏览器发送消息->
@客户端。长度仍然为1,消息已传递
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,所有工作人员都将收到该消息,这将允许我扩展到多个工作人员。还有什么我不知道的吗?我不太明白你在做什么,但是当你依赖于共享一些只存储在内存中的变量时,你添加更多进程的能力就会受到限制