Skip to content

Commit 1e78a68

Browse files
authored
Merge pull request #13185 from colinux/perf/discarded-dossiers-deletion-job-memory
Tech: réduction de l’empreinte mémoire du job de suppression des dossiers expirés
2 parents 037e471 + d3a6ae5 commit 1e78a68

14 files changed

Lines changed: 219 additions & 143 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class Cron::DiscardedBrouillonDossiersDeletionJob < Cron::DiscardedDossiersDeletionBaseJob
4+
self.schedule_expression = 'every day at 02:00'
5+
6+
private
7+
8+
def scope = Dossier.en_brouillon_expired_to_delete
9+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
class Cron::DiscardedDossiersDeletionBaseJob < Cron::CronJob
4+
MAX_DOSSIERS_PER_RUN = 100
5+
6+
# Classe abstraite : empêche `rake jobs:schedule` de tenter d'enregistrer
7+
# cette base (qui n'a pas de schedule_expression) dans Sidekiq Cron.
8+
def self.schedulable?
9+
schedule_expression.present? && super
10+
end
11+
12+
def perform
13+
scope.limit(MAX_DOSSIERS_PER_RUN).each(&:purge_discarded)
14+
self.class.perform_later if scope.exists?
15+
end
16+
17+
private
18+
19+
def scope = raise NotImplementedError
20+
end

app/jobs/cron/discarded_dossiers_deletion_job.rb

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class Cron::DiscardedEnConstructionDossiersDeletionJob < Cron::DiscardedDossiersDeletionBaseJob
4+
self.schedule_expression = 'every day at 03:10'
5+
6+
private
7+
8+
def scope = Dossier.en_construction_expired_to_delete
9+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class Cron::DiscardedTermineDossiersDeletionJob < Cron::DiscardedDossiersDeletionBaseJob
4+
self.schedule_expression = 'every day at 04:20'
5+
6+
private
7+
8+
def scope = Dossier.termine_expired_to_delete
9+
end

app/models/concerns/dossier_searchable_concern.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module DossierSearchableConcern
88
LIGHT_USER_DOSSIERS_THRESHOLD = 5
99

1010
included do
11-
after_commit :index_search_terms_later, if: -> { previously_new_record? || user_previously_changed? || mandataire_first_name_previously_changed? || mandataire_last_name_previously_changed? }
11+
after_commit :index_search_terms_later, on: [:create, :update], if: -> { previously_new_record? || user_previously_changed? || mandataire_first_name_previously_changed? || mandataire_last_name_previously_changed? }
1212

1313
kredis_flag :debounce_index_search_terms_flag
1414
end

app/models/dossier.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,18 +1097,20 @@ def purge_discarded
10971097
transaction do
10981098
DeletedDossier.create_from_dossier(self, hidden_by_reason)
10991099
dossier_operation_logs.purge_discarded
1100+
# Vider les champs par lots libère leur cascade (geo_areas, etablissement,
1101+
# AS attachments) avant le destroy du dossier, évitant le pic mémoire dû
1102+
# au chargement complet de la cascade dependent: :destroy.
1103+
# Important: destroy_all (et non delete_all) preserve les callbacks Rails.
1104+
champs.in_batches(of: 50).each(&:destroy_all)
11001105
destroy
11011106
rescue => e
11021107
Sentry.capture_exception(e, extra: { dossier: id })
1108+
# Rollback explicite : sans cela, le rescue avale l'erreur et la transaction
1109+
# commit un état partiel (champs deja batch-destroy, dossier intact).
1110+
raise ActiveRecord::Rollback
11031111
end
11041112
end
11051113

1106-
def self.purge_discarded
1107-
en_brouillon_expired_to_delete.find_each(&:purge_discarded)
1108-
en_construction_expired_to_delete.find_each(&:purge_discarded)
1109-
termine_expired_to_delete.find_each(&:purge_discarded)
1110-
end
1111-
11121114
def skip_user_notification_email?
11131115
return true if brouillon? && procedure.declarative?
11141116
return true if for_procedure_preview?
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Cron::DiscardedBrouillonDossiersDeletionJob, type: :job do
4+
describe '#perform' do
5+
let(:brouillon_dossier) { create(:dossier, :brouillon, hidden_by_user_at: 5.weeks.ago, hidden_by_reason: 'user_request') }
6+
let(:en_construction_dossier) { create(:dossier, :en_construction, hidden_by_user_at: 5.weeks.ago, hidden_by_reason: 'user_request') }
7+
8+
it 'purges only brouillon dossiers' do
9+
brouillon_dossier
10+
en_construction_dossier
11+
described_class.perform_now
12+
expect { brouillon_dossier.reload }.to raise_error(ActiveRecord::RecordNotFound)
13+
expect { en_construction_dossier.reload }.not_to raise_error
14+
end
15+
end
16+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Cron::DiscardedDossiersDeletionBaseJob, type: :job do
4+
describe '.schedulable?' do
5+
it 'is false so rake jobs:schedule skips this abstract base class' do
6+
expect(described_class.schedulable?).to be false
7+
end
8+
end
9+
10+
describe '#perform with MAX_DOSSIERS_PER_RUN drainage' do
11+
let(:dossier_ids) { create_list(:dossier, 3).map(&:id) }
12+
let(:job_class) do
13+
ids = dossier_ids
14+
Class.new(described_class).tap do |klass|
15+
klass.define_singleton_method(:name) { 'TestDrainJob' }
16+
klass.define_method(:scope) { Dossier.where(id: ids) }
17+
end
18+
end
19+
20+
before do
21+
stub_const("#{described_class}::MAX_DOSSIERS_PER_RUN", 2)
22+
end
23+
24+
it 'processes exactly MAX_DOSSIERS_PER_RUN dossiers and re-enqueues self when more remain' do
25+
expect {
26+
job_class.perform_now
27+
}.to change { Dossier.where(id: dossier_ids).count }.from(3).to(1)
28+
.and have_enqueued_job(job_class)
29+
end
30+
31+
it 'does not re-enqueue when the scope is drained exactly at MAX_DOSSIERS_PER_RUN' do
32+
drainage_ids = create_list(:dossier, 2).map(&:id)
33+
drainage_class = Class.new(described_class).tap do |klass|
34+
klass.define_singleton_method(:name) { 'TestDrainExactJob' }
35+
klass.define_method(:scope) { Dossier.where(id: drainage_ids) }
36+
end
37+
38+
expect { drainage_class.perform_now }
39+
.to change { Dossier.where(id: drainage_ids).count }.from(2).to(0)
40+
.and have_enqueued_job(drainage_class).exactly(0).times
41+
end
42+
end
43+
end

spec/jobs/cron/discarded_dossiers_deletion_job_spec.rb

Lines changed: 0 additions & 107 deletions
This file was deleted.

0 commit comments

Comments
 (0)