feat(task): with_statement_timeout helper for long running collection or process query

This commit is contained in:
Colin Darie 2024-09-19 13:31:37 +02:00
parent c0ae02f458
commit d0f77d0aab
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
5 changed files with 82 additions and 0 deletions

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Maintenance
module StatementsHelpersConcern
extend ActiveSupport::Concern
included do
# Execute block in transaction with a local statement timeout.
# A value of 0 disable the timeout.
#
# Example:
# def collection
# with_statement_timeout("5min") do
# Dossier.all
# end
# end
def with_statement_timeout(timeout)
ApplicationRecord.transaction do
ApplicationRecord.connection.execute("SET LOCAL statement_timeout = '#{timeout}'")
yield
end
end
end
end
end

View file

@ -23,6 +23,7 @@ module TPS
Rails.autoloaders.main.ignore(Rails.root.join('lib/cops')) Rails.autoloaders.main.ignore(Rails.root.join('lib/cops'))
Rails.autoloaders.main.ignore(Rails.root.join('lib/linters')) Rails.autoloaders.main.ignore(Rails.root.join('lib/linters'))
Rails.autoloaders.main.ignore(Rails.root.join('lib/tasks/task_helper.rb')) Rails.autoloaders.main.ignore(Rails.root.join('lib/tasks/task_helper.rb'))
Rails.autoloaders.main.collapse('app/tasks/maintenance/concerns')
config.paths.add Rails.root.join('spec/mailers/previews').to_s, eager_load: true config.paths.add Rails.root.join('spec/mailers/previews').to_s, eager_load: true
config.autoload_paths << "#{Rails.root}/app/jobs/concerns" config.autoload_paths << "#{Rails.root}/app/jobs/concerns"

View file

@ -113,6 +113,29 @@
], ],
"note": "" "note": ""
}, },
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "7dc4935d5b68365bedb8f6b953f01b396cff4daa533c98ee56a84249ca5a1f90",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/tasks/maintenance/concerns/statements_helpers_concern.rb",
"line": 19,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "ApplicationRecord.connection.execute(\"SET LOCAL statement_timeout = '#{timeout}'\")",
"render_path": null,
"location": {
"type": "method",
"class": "Maintenance::StatementsHelpersConcern",
"method": "with_statement_timeout"
},
"user_input": "timeout",
"confidence": "Medium",
"cwe_id": [
89
],
"note": ""
},
{ {
"warning_type": "Cross-Site Scripting", "warning_type": "Cross-Site Scripting",
"warning_code": 2, "warning_code": 2,

View file

@ -5,6 +5,8 @@ module <%= tasks_module %>
class <%= class_name %>Task < MaintenanceTasks::Task class <%= class_name %>Task < MaintenanceTasks::Task
# Documentation: cette tâche modifie les données pour… # Documentation: cette tâche modifie les données pour…
include StatementsHelpersConcern
def collection def collection
# Collection to be iterated over # Collection to be iterated over
# Must be Active Record Relation or Array # Must be Active Record Relation or Array

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Maintenance::StatementsHelpersConcern do
let(:dummy_class) do
Class.new do
include Maintenance::StatementsHelpersConcern
end
end
let(:instance) { dummy_class.new }
describe '#with_statement_timeout' do
it 'applies the statement timeout and raises an error for long-running queries' do
expect {
instance.with_statement_timeout('1ms') do
# Cette requête devrait prendre plus de 1ms et donc déclencher un timeout
ActiveRecord::Base.connection.execute("SELECT pg_sleep(1)")
end
}.to raise_error(ActiveRecord::StatementInvalid, /canceling statement due to statement timeout/i)
end
it 'allows queries to complete within the timeout and returns the result' do
result = instance.with_statement_timeout('1s') do
ActiveRecord::Base.connection.execute("SELECT 42 AS answer").first['answer']
end
expect(result).to eq 42
end
end
end