当存在需要数据库操作的服务器发送事件 (SSE) 时,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(来从中提取连接并允许多个线程。下面是一些配置代码。

没有环境变量,使用默认值

# 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

这里的问题似乎是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连接会阻塞并保留当前设置中的线程。如果您有多个到此端点的请求,您可能会很快耗尽彪马线程本身,从而使请求等待。如果你不希望有很多请求到达这个端点,这可能会起作用,但如果是这样,你需要更多的空闲线程,所以你甚至可能想增加Puma线程数。理想情况下,尽管非阻塞服务器在这里会更好地工作。

编辑:此外,忘记添加rails中的SSE有一个问题,即如果它不知道连接是死的,那么它会保持死连接。您可能会让线程以这种方式无休止地等待,直到一些数据到来,他们意识到连接不再有效。

最新更新