首页 > 解决方案 > 当存在需要数据库操作的服务器发送事件 (SSE) 时,Rails 应用程序挂起

问题描述

我是第一次在 Rails 中学习和做 SSE!我的控制器代码:

  def update
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream, event: 'notice')
    begin
      User.listen_to_creation do |user_id|
        sse.write({id: user_id})
      end
    rescue ClientDisconnected
    ensure
      sse.close
    end
  end

前端:

  var source = new EventSource('/site_update');
  source.addEventListener('notice', function(event) {
    var data = JSON.parse(event.data)
    console.log(data)
  });

模型发布/订阅

class User
  after_commit :notify_creation, on: :create

  def notify_creation
      ActiveRecord::Base.connection_pool.with_connection do |connection|
        self.class.execute_query(connection, ["NOTIFY user_created, '?'", id])
      end
  end

  def self.listen_to_creation
    ActiveRecord::Base.connection_pool.with_connection do |connection|
      begin
        execute_query(connection, ["LISTEN user_created"])
        connection.raw_connection.wait_for_notify do |event, pid, id|
          yield id
        end
      ensure
        execute_query(connection, ["UNLISTEN user_created"])
      end
    end
  end

  def self.clean_sql(query)
    sanitize_sql(query)
  end

  private

  def self.execute_query(connection, query)
    sql = self.clean_sql(query)
    connection.execute(sql)
  end
end

我注意到,如果我正在写信给 SSE,就像教程中的一些琐碎的事情...... sse.write({time_now: Time.now}),一切都很好。在命令行中,CTRL+C成功关闭本地服务器。

但是,每当我需要编写需要某种数据库操作的东西时,例如当我在本教程中执行 postgres pub/sub 时,CTRL+C并没有关闭本地服务器,它只是卡住并挂起并需要我手动杀了PID

在实际启动的服务器上,有时页面刷新也会永远挂起。其他时候,它会抛出一个超时错误:

ActiveRecord::ConnectionTimeoutError (could not obtain a connection from the pool within 5.000 seconds (waited 5.001 seconds); all pooled connections were in use):

不幸的是,这个问题在我使用 Heroku 的生产环境中仍然存在。我只是收到很多超时错误。但我认为我已经正确配置了 Heroku,以及本地设置......我的理解是我只需要有一个相当大的池(我有5)来拉出连接并允许多个线程。下面你会找到一些配置代码。

没有 ENV 变量,使用默认值!

# config/database.yml
default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: proper_development


# config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 1)

threads_count = Integer(ENV['MAX_THREADS'] || 5)
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection
end

如果有帮助,这是我运行时的输出rails s

=> Booting Puma
=> Rails 5.0.2 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 4.3.3 (ruby 2.4.0-p0), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

标签: ruby-on-railsherokuserver-sent-eventspuma

解决方案


这里的问题似乎是 puma 线程和数据库连接之间缺乏一致性。如果某些连接是由中间件等通过 AR 发起的,那么您编写的代码可能会导致两个连接保持在同一个请求周期中,直到您收到通知并且线程完成其工作。AR 缓存每个线程的连接,因此如果发出请求并且从池中检出连接,它将由该池保存。查看此问题以获取更多详细信息。如果您最终使用连接池来检查另一个连接并让该连接等待,直到您收到来自 Postgres 的通知,则可能两个连接可能由正在等待的同一个 Puma 线程持有。

要查看实际情况,请在开发中启动一个新的 Rails 服务器实例并向您的 SSE 端点发出请求。如果您在向新启动的服务器发出一个请求时可能会看到两个到 Postgres 的连接之前出现超时。因此,即使您的线程数和连接池大小相同,您也可能会用完池中的空闲连接。一种更简单的方法可能是在您签出连接后在开发中添加这一行,以查看现在有多少缓存连接。

def self.listen_to_creation
    ActiveRecord::Base.connection_pool.with_connection do |connection|
      # Print or log this array
      p ActiveRecord::Base.connection_pool.instance_variable_get(:@thread_cached_conns).keys.map(&:object_id)

      begin
        execute_query(connection, ["LISTEN user_created"])
.........
.........

此外,您发布的片段似乎表明您在开发环境中的大小为 5 的连接池上运行多达 16 个线程,所以这是一个不同的问题。

要解决此问题,您需要调查哪个线程正在保持连接,以及是否可以将其重用于通知或只是增加数据库池大小。

现在,来到 SSE 本身。由于 SSE 连接会阻塞并在当前设置中保留一个线程。如果您对该端点有多个请求,您可能会很快耗尽 Puma 线程本身,从而使请求等待。如果您不希望对此端点有很多请求,这可能会起作用,但如果您是,您将需要更多空闲线程,因此您甚至可能想要增加 Puma 线程数。理想情况下,虽然非阻塞服务器在这里会更好地工作。

编辑:另外,忘记添加Rails 中的SSE如果不知道连接已死,则存在保持死连接活动的问题。您可能会让线程无休止地等待,直到一些数据到来并且他们意识到连接不再有效。


推荐阅读