如何在Rails中取消昂贵的Postgresql查询?

6

在我的Rails应用程序中,我有一个控制器负责生成报告。其中一些报告需要很长时间才能生成,这超过了Heroku的30秒限制。在这种情况下,我希望在25秒后向用户显示通知,并取消数据库查询。我的初始想法是使用Timeout

class ReportsController < ApplicationController
  def expensive_report
    Timeout.timeout(25) do
      @results = ExpensiveQuery.new(params).results
    end
  rescue Timeout::Error
    render action: "timeout"
  end
end

计时器正常工作,但相应的查询没有被取消。在 Rails 控制台中很容易重现此问题。
begin
  Timeout.timeout(1) do
    ActiveRecord::Base.connection.execute("SELECT pg_sleep(10)")
  end
rescue Timeout::Error
  puts "Timeout"
end

result = ActiveRecord::Base.connection.execute("SELECT 1 AS value")
puts result[0]["value"]

这段代码将输出“超时”,随后在result = ActiveRecord::Base.connection.execute("SELECT 1 AS value")这一行上阻塞,直至pg_sleep查询结束。
如何在Rails内部取消这样的查询?我正在Heroku上托管我的应用程序,因此权限受到限制,无法运行诸如pg_cancel_backendpg_terminate_backend的命令。

我认为在这里更好的答案是使用后台作业来生成报告,而不是占用网络工作者。这样一来,您可以立即向用户提供反馈并释放网络工作者。http://edgeguides.rubyonrails.org/active_job_basics.html - max
你可以使用polling或者websockets来在任务完成时通知用户。 - max
@max 这是我的想法。我想显示一个带有启动后台任务选项的消息。这些昂贵的查询对于大多数用户来说都很好用,所以我不想为每个人都运行它作为后台任务。 - Michał Młoźniak
除此之外,它还会占用你想要空闲处理请求的动态资源。 - max
3个回答

1
你可以在会话级别上设置 statement_timeout(不需要事务,跳过 local)。
或者在事务中:
t=# begin; set local statement_timeout to 1; select pg_sleep(3);end;
BEGIN
Time: 0.096 ms
SET
Time: 0.098 ms
ERROR:  canceling statement due to statement timeout
Time: 1.768 ms
ROLLBACK
Time: 0.066 ms

或者作为用户的默认设置:
alter user no_long_qries set statement_timeout to 1;

谢谢。回答很好,但是没有事务是不可能设置的,这有点限制。我需要将所有查询包装在事务中。另外,statement_timeout是以毫秒为单位的。 - Michał Młoźniak
你可以按会话或用户设置它。 - Vao Tsun

0

您可以通过将以下内容添加到config/database.yml中,使超时时间适用于所有数据库请求:

default: &default
  adapter: postgresql
  ...
  variables:
    statement_timeout: 25000

将以下内容添加到config/environment.rb的末尾:
ActiveRecord::Base.connection.execute('set statement_timeout to 25000')

2
所以这是有问题的,因为它将被设置为所有查询。我需要仅针对特定查询进行设置。 - Michał Młoźniak

0

我找到了一个很酷的解决方案,不需要修改当前的查询。

class ReportsController < ApplicationController
  def expensive_report
    Timeout.timeout(25) do
      @results = ExpensiveQuery.new(params).results
    end
  rescue Timeout::Error
    ActiveRecord::Base.connection.raw_connection.cancel
    render action: "timeout"
  end
end

当在控制台运行时,我需要添加检查连接是否活动的代码。

begin
  Timeout.timeout(1) do
    ActiveRecord::Base.connection.execute("SELECT pg_sleep(10)")
  end
rescue Timeout::Error
  ActiveRecord::Base.connection.raw_connection.cancel
  ActiveRecord::Base.connection.active?
  puts "Timeout"
end

result = ActiveRecord::Base.connection.execute("SELECT 1 AS value")
puts result[0]["value"]

如果不调用ActiveRecord::Base.connection.active?,它会抛出ActiveRecord::StatementInvalid: PG::QueryCanceled异常。

这个方法运行得很好,但我还不确定是否存在任何隐藏的问题。


阅读了一些文章(例如https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/)后,应该(强烈)避免使用“timeout”,因为它可能在任何时候、任何代码行引发(中断)异常。 - Linh Dam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接