Merge pull request #10863 from colinux/maintenance-tasks-template

ETQ dev, je peux programmer des maintenance tasks à jouer lors de leur déploiement
This commit is contained in:
Colin Darie 2024-10-09 12:16:56 +00:00 committed by GitHub
commit fbb9405e50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 349 additions and 41 deletions

View file

@ -2,6 +2,9 @@
module Maintenance
class BackfillBulkMessagesWithProcedureIdTask < MaintenanceTasks::Task
# Périmètre: envoi dun email groupé aux usagers ayant dossiers en brouillon.
# Change la manière dont ces messages sont liés aux démarches.
# 2024-03-12-01 PR #10071
def collection
BulkMessage
.where(procedure: nil)

View file

@ -2,6 +2,9 @@
module Maintenance
class BackfillCityNameTask < MaintenanceTasks::Task
# corrige des données du champ adresse suite à un bug
# introduit pendant quelques jours début mars
# 2024-04-09-02 PR #10290
attribute :champ_ids, :string
validates :champ_ids, presence: true

View file

@ -2,6 +2,9 @@
module Maintenance
class BackfillClonedChampsPrivatePieceJustificativesTask < MaintenanceTasks::Task
# Supprime les PJ dannotations privées
# qui étaient conservées par erreur lorsquun dossier était cloné
# 2024-05-27-01 PR #10435
def collection
Dossier.en_brouillon.where.not(parent_dossier_id: nil)
end

View file

@ -2,6 +2,9 @@
module Maintenance
class BackfillClosingReasonInClosedProceduresTask < MaintenanceTasks::Task
# Remet les messages de cloture d'une démarche proprement (sinon affichage KO).
# Suite de UpdateClosingReasonIfNoReplacedByIdTask
# 2024-05-27-01 PR #9930
def collection
Procedure
.with_discarded

View file

@ -2,6 +2,9 @@
module Maintenance
class BackfillCommuneCodeFromNameTask < MaintenanceTasks::Task
# corrige structure champs commune pour une démarche donnée. Suite à un bug ?
# 2024-05-31-01 PR #10469
attribute :procedure_id, :string
validates :procedure_id, presence: true

View file

@ -2,6 +2,9 @@
module Maintenance
class BackfillDepartementServicesTask < MaintenanceTasks::Task
# Fait le lien service département pour permettre
# le filtrage des démarches par département
# 2023-10-30-01 PR #9647
def collection
Service.where.not(etablissement_infos: nil)
end

View file

@ -2,6 +2,8 @@
module Maintenance
class BackfillDeposeAtOnDeletedDossiersTask < MaintenanceTasks::Task
# Améliore les stats à propos des dates de dépôts pour les dossiers supprimés
# 2024-04-05-01 PR #10259
def collection
DeletedDossier.where(depose_at: nil)
end

View file

@ -2,6 +2,8 @@
module Maintenance
class BackfillEffectifAnnuelAnneeTask < MaintenanceTasks::Task
# API entreprise: rattrape les informations d'effectif
# 2024-05-27-01 PR #10053
def collection
Etablissement.where.not(entreprise_effectif_annuel: nil).where(entreprise_effectif_annuel_annee: nil)
end

View file

@ -2,6 +2,8 @@
module Maintenance
class BackfillInvalidDossiersForTiersTask < MaintenanceTasks::Task
# Corrige les dossiers declarés pour un tiers mais sans avoir renseigné les infos du tiers
# 2024-05-22-01
def collection
Dossier.where(for_tiers: true).where(mandataire_first_name: nil)
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Maintenance
module RunnableOnDeployConcern
extend ActiveSupport::Concern
class_methods do
def run_on_first_deploy
@run_on_first_deploy = true
end
def run_on_deploy?
return false unless @run_on_first_deploy
task = MaintenanceTasks::TaskDataShow.new(name)
return false if task.completed_runs.not_errored.any?
return false if task.active_runs.any?
true
end
end
end
end

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

@ -2,6 +2,9 @@
module Maintenance
class CreatePreviewsForPjOfLatestDossiersTask < MaintenanceTasks::Task
# Génère les vignettes de PJ existantes pour les dossiers déposés entre 2 dates (facultatif)
# Elles sont affichées dans le nouvel onglet "Pièces jointes" des instructeurs.
# 2024-07-11-01
attribute :start_text, :string
validates :start_text, presence: true

View file

@ -2,6 +2,9 @@
module Maintenance
class CreateVariantsForPjOfLatestDossiersTask < MaintenanceTasks::Task
# Génère les vignettes de fichiers PDF pour les dossiers déposés entre 2 dates (facultatif)
# Elles sont affichées dans le nouvel onglet "Pièces jointes" des instructeurs.
# 2024-07-11-01
attribute :start_text, :string
validates :start_text, presence: true

View file

@ -2,10 +2,10 @@
module Maintenance
class DeleteDraftRevisionTypeDeChampsTask < MaintenanceTasks::Task
csv_collection
# Modifie le form dune démarche à partir dun CSV (dev spécifique Fonds Verts).
# See UpdateDraftRevisionTypeDeChampsTask for more information
# Just add delete_flag with "true" to effectively remove the type de champ from the draft.
# Just add delete_flag with "true" in CSV to effectively remove the type de champ from the draft.
csv_collection
def process(row)
return unless row["delete_flag"] == "true"

View file

@ -2,6 +2,10 @@
module Maintenance
class DestroyIncompleteBulkMessagesTask < MaintenanceTasks::Task
# Périmètre: envoi dun email groupé aux usagers ayant dossiers en brouillon.
# Change la manière dont ces messages sont liés aux démarches.
# Suite de BackfillBulkMessagesWithProcedureIdTask
# 2024-03-12-01 PR #10071
def collection
BulkMessage.where(procedure: nil).where.missing(:groupe_instructeurs)
end

View file

@ -2,6 +2,8 @@
module Maintenance
class DestroyProcedureWithoutAdministrateurAndWithoutDossierTask < MaintenanceTasks::Task
# suppression de procédures closes sans admin et sans dossier
# 2024-03-18-01 PR #10125
def collection
Procedure.with_discarded.where.missing(:administrateurs, :dossiers)
end

View file

@ -2,6 +2,8 @@
module Maintenance
class DisableRemainingInvalidMonAvisTask < MaintenanceTasks::Task
# Supprime les codes dintégration « mon avis » invalides
# 2024-03-18-01 PR #10120
def collection
# rubocop:disable DS/Unscoped
Procedure.unscoped.where.not(monavis_embed: nil)

View file

@ -2,6 +2,9 @@
module Maintenance
class FixDecimalNumberWithSpacesTask < MaintenanceTasks::Task
# normalise les champs nombres en y supprimant les éventuels espaces
# 2024-07-01-01 PR #10554
ANY_SPACES = /[[:space:]]/
def collection
Champs::DecimalNumberChamp.where.not(value: nil)

View file

@ -2,6 +2,10 @@
module Maintenance
class FixDureeConservationGreaterThanMaxDureeConservationTask < MaintenanceTasks::Task
# Corrige la durée de conservation des dossiers :
# pour toutes les démarches dont la durée de conservation est supérieure
# à celle de l'instance, on prend la durée max de DS (12 mois)
# 2024-05-27-01 PR #10107
def collection
Procedure.where('duree_conservation_dossiers_dans_ds > max_duree_conservation_dossiers_dans_ds')
end

View file

@ -2,6 +2,8 @@
module Maintenance
class FixOpenProceduresWithClosingReasonTask < MaintenanceTasks::Task
# Corrige des démarches avec un motif de fermerture alors quelles ont été publiées
# 2024-05-27-01 PR #10181
def collection
Procedure
.with_discarded

View file

@ -2,6 +2,10 @@
module Maintenance
class MoveDolToColdStorageTask < MaintenanceTasks::Task
# Opération de rattrapage suite à un cron qui ne fonctionnait plus.
# Permet de déplacer toutes les traces fonctionnelles (DossierOperationLog)
# vers le stockage object plutot que de les conserver en BDD
# 2024-04-15-01
attribute :start_text, :string
validates :start_text, presence: true

View file

@ -2,6 +2,11 @@
module Maintenance
class RecomputeBlobChecksumTask < MaintenanceTasks::Task
# Avant février 2024, les filigranes ont corrompu les hash des fichiers.
# Régulièrement, des dossiers en brouillon étaient déposés avec ce problème
# (on retrouve les fichiers corrompu dans l'onglet retry de sidekiq).
# Cette tache recalcule les hashes.
# 2024-05-27-01
attribute :blob_ids, :string
validates :blob_ids, presence: true

View file

@ -2,6 +2,9 @@
module Maintenance
class SamsungBrowserIsSupportedTask < MaintenanceTasks::Task
# Corrige une donnée si le navigateur utilisé
# dans lhistorique des Traitements des dossiers
# 2024-02-21-01
def collection
Traitement.where(browser_name: 'Samsung Browser', browser_version: 12..)
end

View file

@ -2,6 +2,8 @@
module Maintenance
class SpreadDossierDeletionTask < MaintenanceTasks::Task
# Contourne un égorgement de suppression de millions de dossiers qui aurait eu lieu le même jour
# 2024-05-27-01 PR #10062
ERROR_OCCURED_AT = Date.new(2024, 2, 14)
ERROR_OCCURED_RANGE = ERROR_OCCURED_AT.at_midnight..(ERROR_OCCURED_AT + 1.day)
SPREAD_DURATION_IN_DAYS = 150

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Maintenance
class T20241009NoopAttemptRunOnDeployTask < MaintenanceTasks::Task
# Documentation: cette tâche ne fait rien mais sert à vérifier
# qu'elle sera bien exécutée sur le déploiement suivant
# pour remplacer after party.
include RunnableOnDeployConcern
include StatementsHelpersConcern
# Uncomment only if this task MUST run imperatively on its first deployment.
# If possible, leave commented for manual execution later.
run_on_first_deploy
def collection
1.upto(10).to_a
end
def process(element)
# NOOP
end
def count
10
end
end
end

View file

@ -2,6 +2,9 @@
module Maintenance
class UpdateClosingReasonIfNoReplacedByIdTask < MaintenanceTasks::Task
# Remet les messages de cloture d'une démarche proprement (sinon affichage KO).
# Avant BackfillClosingReasonInClosedProceduresTask
# 2024-03-21-01 PR #10158
def collection
Procedure
.with_discarded

View file

@ -2,6 +2,10 @@
module Maintenance
class UpdateConditionsBasedOnCommuneOrEpciChampTask < MaintenanceTasks::Task
# Met à jour les conditions et règles de routage
# pour les champs communes et ECPI suite à l'ajout de nouveaux opérateurs
# Voir aussi UpdateRoutingRulesBasedOnCommuneOrEpciChampTask
# 2023-12-20-01 PR #9850
include Logic
def collection

View file

@ -2,6 +2,8 @@
module Maintenance
class UpdateDraftRevisionTypeDeChampsTask < MaintenanceTasks::Task
# Modifie le form dune démarche à partir dun CSV (dev pour les Fonds Verts)
csv_collection
# CSV structure:

View file

@ -2,6 +2,10 @@
module Maintenance
class UpdateRoutingRulesBasedOnCommuneOrEpciChampTask < MaintenanceTasks::Task
# Ces 2 tâches mettent à jour les conditions et règles de routage
# pour les champs communes et ECPI suite à l'ajout de nouveaux opérateurs
# Voir aussi UpdateConditionsBasedOnCommuneOrEpciChampTask
# 2023-12-20-01 PR #9850
include Logic
def collection

View file

@ -2,6 +2,9 @@
module Maintenance
class UpdateServiceEtablissementInfosTask < MaintenanceTasks::Task
# Géocode les services à partir des établissements
# 2024-05-27-01 PR #10106
# No more 20 geocoding by 10 seconds window
THROTTLE_LIMIT = 20
THROTTLE_PERIOD = 10.seconds

View file

@ -2,6 +2,8 @@
module Maintenance
class UpdateZonesTask < MaintenanceTasks::Task
# Synchronise les zones en base à partir du fichier de config zones.yml
# 2024-05-27-01 PR #10077
def collection
config = Psych.safe_load(Rails.root.join("config", "zones.yml").read)
config['ministeres']

View file

@ -37,6 +37,9 @@ FileUtils.chdir APP_ROOT do
puts "\n== Running after_party tasks =="
system! 'bin/rails after_party:run'
puts "\n== Running on deploy maintenance tasks =="
system! 'bin/rails deploy:maintenance_tasks'
puts "\n== Removing old logs =="
system! 'bin/rails log:clear'

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/linters'))
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.autoload_paths << "#{Rails.root}/app/jobs/concerns"

View file

@ -113,6 +113,29 @@
],
"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_code": 2,

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
Rails.application.config.after_initialize do
if defined?(Rails::Generators)
require "generators/maintenance_tasks/task_generator"
class MaintenanceTasks::TaskGenerator
alias_method :original_assign_names!, :assign_names!
source_paths << Rails.root.join("lib/templates/maintenance_tasks")
private
# Prefix the task name with a date so the tasks are better sorted.
def assign_names!(name)
timestamped_name = "T#{Date.current.strftime("%Y%m%d")}#{name}"
original_assign_names!(timestamped_name)
end
end
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rails/generators'
Rails.application.config.after_initialize do
module AfterParty
module Generators
class TaskGenerator
prepend Module.new {
def invoke_all
warn "[DEPRECATION] 'after_party:task' is deprecated. Use 'rails generate maintenance_tasks:task #{name}' instead."
end
}
end
end
end
end

View file

@ -1,42 +1,14 @@
# frozen_string_literal: true
def domains_for_stage
if ENV['DOMAINS'].present?
ENV['DOMAINS'].split
else
raise "DOMAINS is empty. It must be something like DOMAINS='web1.dev web2.dev'"
end
end
task :setup do
domains = domains_for_stage
domains.each do |domain|
sh "mina setup domain=#{domain}"
end
end
task :deploy do
domains = domains_for_stage
branch = ENV.fetch('BRANCH')
domains.each do |domain|
sh "mina deploy domain=#{domain} branch=#{branch} force_asset_precompile=true"
end
end
task :post_deploy do
domains = domains_for_stage
branch = ENV.fetch('BRANCH')
sh "mina post_deploy domain=#{domains.first} branch=#{branch}"
end
task :rollback do
domains = domains_for_stage
branch = ENV.fetch('BRANCH')
domains.each do |domain|
sh "mina rollback service:restart_puma service:reload_nginx service:restart_delayed_job domain=#{domain} branch=#{branch}"
namespace :deploy do
task maintenance_tasks: :environment do
tasks = MaintenanceTasks::Task
.load_all
.filter { _1.respond_to?(:run_on_deploy?) && _1.run_on_deploy? }
tasks.each do |task|
Rails.logger.info { "MaintenanceTask run on deploy #{task.name}" }
MaintenanceTasks::Runner.run(name: task.name)
end
end
end

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
module <%= tasks_module %>
<% module_namespacing do -%>
class <%= class_name %>Task < MaintenanceTasks::Task
# Documentation: cette tâche modifie les données pour…
include RunnableOnDeployConcern
include StatementsHelpersConcern
# Uncomment only if this task MUST run imperatively on its first deployment.
# If possible, leave commented for manual execution later.
# run_on_first_deploy
def collection
# Collection to be iterated over
# Must be Active Record Relation or Array
end
def process(element)
# The work to be done in a single iteration of the task.
# This should be idempotent, as the same element may be processed more
# than once if the task is interrupted and resumed.
end
def count
# Optionally, define the number of rows that will be iterated over
# This is used to track the task's progress
end
end
<% end -%>
end

View file

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Maintenance::RunnableOnDeployConcern do
let(:test_class) do
Class.new do
include Maintenance::RunnableOnDeployConcern
end
end
describe '.run_on_deploy?' do
context 'when run_on_first_deploy is not set' do
it 'returns false' do
expect(test_class.run_on_deploy?).to be false
end
end
context 'when run_on_first_deploy is set' do
before do
test_class.run_on_first_deploy
allow(MaintenanceTasks::TaskDataShow).to receive(:new).and_return(task_data_show)
end
let(:task_data_show) { instance_double(MaintenanceTasks::TaskDataShow, completed_runs: completed_runs, active_runs: active_runs) }
let(:completed_runs) { double(ActiveRecord::Relation, not_errored: not_errored_runs) }
let(:active_runs) { [] }
let(:not_errored_runs) { [] }
context 'when there are no run yet' do
it 'returns true' do
expect(test_class.run_on_deploy?).to be true
end
end
context 'when there are completed runs without errors' do
let(:not_errored_runs) { [instance_double(MaintenanceTasks::Run)] }
it 'returns false' do
expect(test_class.run_on_deploy?).to be false
end
end
context 'when there are active runs' do
let(:active_runs) { [instance_double(MaintenanceTasks::Run)] }
it 'returns false' do
expect(test_class.run_on_deploy?).to be false
end
end
end
end
end

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