En mi aplicación Rails tengo el controlador que se encarga de generar los informes. Algunos de esos informes tardan mucho tiempo en generarse y superan el límite de 30 segundos en Heroku. En tal caso, quiero mostrar una notificación al usuario después de 25 segundos y también cancelar la consulta de la base de datos. Mi idea inicial era usar Timeout .
class ReportsController < ApplicationController def expensive_report Timeout.timeout(25) do @results = ExpensiveQuery.new(params).results end rescue Timeout::Error render action: "timeout" end endEl tiempo de espera funciona bien, pero la consulta respectiva no se cancela. Es fácil de reproducir en la consola de 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"] Este código generará "Tiempo de espera" y luego bloqueará el result = ActiveRecord::Base.connection.execute("SELECT 1 AS value") hasta que finalice la consulta pg_sleep .
¿Cómo puedo cancelar dicha consulta desde Rails? Alojo mi aplicación en Heroku, por lo que los privilegios se limitan a ejecutar comandos como pg_cancel_backend o pg_terminate_backend .
puede configurar statement_timeout en el nivel de sesión (sin transacción y omitiendo local ).
o transacción:
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 mso por defecto para el usuario:
alter user no_long_qries set statement_timeout to 1;Puede hacer que ese tiempo de espera se aplique a todas las solicitudes de la base de datos agregando esto a config/database.yml :
default: &default adapter: postgresql ... variables: statement_timeout: 25000 O esto hasta el final de config/environment.rb :
ActiveRecord::Base.connection.execute('set statement_timeout to 25000')Encontré una solución genial que no requiere modificaciones a las consultas actuales.
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 endCuando se ejecuta en la consola, necesito agregar verificar si la conexión está activa.
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"] Sin llamar a ActiveRecord::Base.connection.active? generará ActiveRecord::StatementInvalid: PG::QueryCanceled .
Esto funciona bastante bien, pero todavía estoy seguro de si hay algún problema oculto con eso.