Redis + ActionController ::活动线程没有死亡

Pau*_*ter 34 multithreading ruby-on-rails publish-subscribe redis ruby-on-rails-4

背景:我们在一个现有的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
Run Code Online (Sandbox Code Playgroud)

(注意:该events方法似乎在subscribe方法调用时被阻止.其他所有内容(流式传输)都正常工作,所以我认为这是正常的.)

(其他注意:flusher线程概念作为一个长时间运行的后台进程更有意义,有点像垃圾线程收集器.上面我的实现的问题是为每个连接生成一个新线程,这是没有意义的.尝试实现这个概念应该更像是一个单一的过程,而不是我概述的那样.当我成功地将其重新实现为单个后台进程时,我将更新这篇文章.)

这个解决方案的缺点是我们只是延迟或减少了问题,而没有完全解决它.除了其他请求(如ajax)之外,我们仍然有每个用户2个线程,从扩展的角度来看这似乎很糟糕; 对于具有许多可能的并发连接的大型系统来说,这似乎是完全无法实现的,也是不切实际的.

我觉得我错过了一些至关重要的东西; 我觉得有点难以相信Rails有一个明显被破坏的功能而没有像我那样实现自定义连接检查器.

问题:我们如何在不实现诸如"连接扑克"或垃圾线程收集器之类的东西的情况下允许连接/线程死亡?

如果我遗漏了任何东西,请随时告诉我.

更新 只是为了添加一些额外的信息:Huetsch在github发布了这条评论指出SSE基于TCP,它通常在连接关闭时发送FIN数据包,让另一端(本例中的服务器)知道它可以安全地关闭连接.Huetsch指出浏览器没有发送该数据包(可能是EventSource库中的错误?),或者Rails没有捕获它或用它做任何事情(如果是这种情况,肯定是Rails中的一个错误).搜索继续......

使用Wireshark的另一个更新,我确实可以看到FIN数据包被发送.不可否认,我对协议级别的东西知之甚少或不熟悉,但据我所知,当我使用浏览器中的EventSource建立SSE连接时,我肯定会检测到从浏览器发送的FIN数据包,如果我发送了NO数据包删除该连接(意味着没有SSE).虽然我对TCP的知识并不十分了解,但这似乎向我表明,客户端确实正确地终止了连接; 也许这表明Puma或Rails中存在错误.

另一个更新 @JamesBoutcher/boutcheratwest(github)向我指出了关于redis网站关于这个问题的讨论,特别是关于该.(p)subscribe方法永远不会关闭的事实.该站点上的海报指出了我们在此处发现的相同内容,即当客户端连接关闭时,Rails环境永远不会得到通知,因此无法执行该.(p)unsubscribe方法.他询问有关的超时.(p)subscribe方法,我认为也可以,但我不确定哪种方法(我上面描述的连接扑克,或他的超时建议)将是一个更好的解决方案.理想情况下,对于连接扑克解决方案,我想找到一种方法来确定连接是否在另一端关闭而不写入流.就像现在一样,正如你所看到的,我必须实现客户端代码来单独处理我的"戳"消息,我认为这是一种突兀和愚蠢的行为.

Jam*_*her 15

我刚刚做的一个解决方案(从@teeg借了很多)似乎工作正常(没有失败测试它,所以)

配置/初始化/ redis.rb

$redis = Redis.new(:host => "xxxx.com", :port => 6379)

heartbeat_thread = Thread.new do
  while true
    $redis.publish("heartbeat","thump")
    sleep 30.seconds
  end
end

at_exit do
  # not sure this is needed, but just in case
  heartbeat_thread.kill
  $redis.quit
end
Run Code Online (Sandbox Code Playgroud)

然后在我的控制器中:

def events
    response.headers["Content-Type"] = "text/event-stream"
    redis = Redis.new(:host => "xxxxxxx.com", :port => 6379)
    logger.info "New stream starting, connecting to redis"
    redis.subscribe(['parse.new','heartbeat']) do |on|
      on.message do |event, data|
        if event == 'parse.new'
          response.stream.write("event: parse\ndata: #{data}\n\n")
        elsif event == 'heartbeat'
          response.stream.write("event: heartbeat\ndata: heartbeat\n\n")
        end
      end
    end
  rescue IOError
    logger.info "Stream closed"
  ensure
    logger.info "Stopping stream thread"
    redis.quit
    response.stream.close
  end
Run Code Online (Sandbox Code Playgroud)


The*_*One 5

我目前正在制作一个围绕 ActionController:Live、EventSource 和 Puma 的应用程序,对于那些遇到关闭流等问题的应用程序,IOError在 Rails 4.2 中您需要拯救 ,而不是拯救ClientDisconnected。例子:

def stream
  #Begin is not required
  twitter_client = Twitter::Streaming::Client.new(config_params) do |obj|
    # Do something
  end
rescue ClientDisconnected
  # Do something when disconnected
ensure
  # Do something else to ensure the stream is closed
end
Run Code Online (Sandbox Code Playgroud)

我从这个论坛帖子中找到了这个方便的提示(一直在底部):http://railscasts.com/episodes/401-actioncontroller-live ?view=comments