Skip to content

Commit e09ec54

Browse files
committed
feat(siret_champ): converge technical fetch failures to degraded mode
Exhausted retries or a non-retryable non-404 failure now create a degraded etablissement (champ fetched) so acceptance stays blocked and backfill jobs recover the data. Only a 404 ends in external_error.
1 parent 952e298 commit e09ec54

5 files changed

Lines changed: 93 additions & 5 deletions

File tree

app/jobs/champ_fetch_external_data_job.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class ChampFetchExternalDataJob < ApplicationJob
66

77
retry_on RetryableFetchError, attempts: 3, wait: :polynomially_longer do |job, err|
88
champ = job.arguments.first
9-
champ.external_data_error!
9+
champ.handle_exhausted_external_data_retries!
1010

1111
# Don't raise, otherwise it will pop forever as "working" queue without doing anything
1212
Sentry.capture_exception(err.cause)

app/models/champs/siret_champ.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,25 @@ def fetch_external_data
6060
Success(etablissement:, value: external_id)
6161
in Failure(type: :not_found, **)
6262
Failure(retryable: false, error: StandardError.new('NotFound'), code: 404)
63-
in Failure(type:, code:, retryable:, **)
64-
Failure(retryable:, error: StandardError.new("API Entreprise: #{type}"), code:)
63+
in Failure(type:, code:, retryable: true, **)
64+
Failure(retryable: true, error: StandardError.new("API Entreprise: #{type}"), code:)
65+
in Failure(type:, code:, **)
66+
# Any other technical error also converges to degraded mode: only a 404
67+
# (nonexistent SIRET) leaves the champ in external_error.
68+
Sentry.capture_message("SiretChamp: degraded mode fallback after API Entreprise error", extra: { champ: id, type:, code: })
69+
Success(etablissement: create_degraded_etablissement, value: external_id)
6570
end
6671
end
6772

73+
# Retries exhausted (API Entreprise flapping while the health checker still
74+
# reports it up): converge to a degraded etablissement instead of
75+
# external_error, so that Dossier#any_etablissement_as_degraded_mode? blocks
76+
# acceptance and the backfill jobs recover the data later.
77+
def handle_exhausted_external_data_retries!
78+
update_external_data!(etablissement: create_degraded_etablissement, value: external_id)
79+
external_data_fetched!
80+
end
81+
6882
def search_terms
6983
etablissement.present? ? etablissement.search_terms : [value]
7084
end
@@ -77,6 +91,10 @@ def save_additional_job_exception(exception, code)
7791

7892
private
7993

94+
def create_degraded_etablissement
95+
APIEntrepriseService.create_etablissement_as_degraded_mode(self, external_id.delete(" "), dossier.user&.id)
96+
end
97+
8098
# We want to validate if SIRET really exists
8199
# It's valid when an etablissement have been created in turbo with SIRET controller
82100
# When API Entreprise is down, user won't be stuck because

app/models/concerns/champ_external_data_concern.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ module ChampExternalDataConcern
4747
end
4848

4949
event :external_data_fetched do
50-
transitions from: [:fetching], to: :fetched
50+
# waiting_for_job: a champ can converge to fetched from the retry_on
51+
# exhaustion block (see #handle_exhausted_external_data_retries!)
52+
transitions from: [:fetching, :waiting_for_job], to: :fetched
5153
end
5254

5355
event :external_data_error do
@@ -72,6 +74,11 @@ def has_async_external_data? = false
7274

7375
def external_data_needed_for_validation? = has_async_external_data?
7476

77+
# Called by ChampFetchExternalDataJob when all retry attempts are exhausted.
78+
def handle_exhausted_external_data_retries!
79+
external_data_error!
80+
end
81+
7582
private
7683

7784
def ready_for_external_call? = external_id.present?

spec/jobs/champ_fetch_external_data_job_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,30 @@
6666
expect(champ.fetch_external_data_exceptions.size).to eq(3)
6767
end
6868
end
69+
70+
context 'when retries are exhausted on a siret champ' do
71+
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :siret }]) }
72+
let(:dossier) { create(:dossier, procedure:) }
73+
let(:external_id) { '41816609600051' }
74+
75+
before do
76+
champ.update_columns(external_id:, external_state: 'waiting_for_job')
77+
allow_any_instance_of(Champs::SiretChamp).to receive(:fetch_external_data).and_return(failure)
78+
end
79+
80+
it 'converges to a degraded etablissement instead of external_error' do
81+
described_class.perform_later(champ, external_id)
82+
83+
3.times do
84+
perform_enqueued_jobs(only: ChampFetchExternalDataJob)
85+
rescue StandardError
86+
end
87+
88+
champ.reload
89+
90+
expect(champ).to be_fetched
91+
expect(champ.etablissement).to be_as_degraded_mode
92+
end
93+
end
6994
end
7095
end

spec/models/champs/siret_champ_spec.rb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,21 @@
259259
end
260260
end
261261

262+
context 'when API returns a non-retryable technical failure' do
263+
before do
264+
allow(APIEntrepriseService).to receive(:create_etablissement_with_fallback)
265+
.and_return(Dry::Monads::Failure(type: :forbidden, code: 403, retryable: false))
266+
allow(Sentry).to receive(:capture_message)
267+
end
268+
269+
it 'converges to a degraded etablissement instead of external_error' do
270+
result = champ.fetch_external_data
271+
expect(result).to be_success
272+
expect(result.value![:etablissement]).to be_as_degraded_mode
273+
expect(Sentry).to have_received(:capture_message)
274+
end
275+
end
276+
262277
context 'when API returns Success in degraded mode' do
263278
let(:etablissement) { instance_double(Etablissement, as_degraded_mode?: true) }
264279
before do
@@ -300,7 +315,7 @@
300315
end
301316
end
302317

303-
describe '#external_data_status_message' do
318+
describe '#external_data_status_message (unit)' do
304319
let(:champ) { Champs::SiretChamp.new }
305320

306321
it 'returns nil when fetched' do
@@ -349,6 +364,29 @@
349364
end
350365
end
351366

367+
describe '#handle_exhausted_external_data_retries!' do
368+
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }]) }
369+
let(:dossier) { create(:dossier, procedure:) }
370+
let(:champ) { dossier.champs.first }
371+
372+
before do
373+
champ.update_columns(
374+
external_id: '41816609600051',
375+
external_state: 'waiting_for_job',
376+
fetch_external_data_exceptions: [ExternalDataException.new(error: 'boom', code: 503)]
377+
)
378+
end
379+
380+
it 'converges to a degraded etablissement instead of external_error' do
381+
expect { champ.handle_exhausted_external_data_retries! }
382+
.to have_enqueued_job(APIEntreprise::EtablissementJob)
383+
384+
champ.reload
385+
expect(champ).to be_fetched
386+
expect(champ.etablissement).to be_as_degraded_mode
387+
end
388+
end
389+
352390
describe '#external_data_required_for_conditions?' do
353391
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }, { type: :text }]) }
354392
let(:siret_tdc) { procedure.draft_revision.types_de_champ_public.first }

0 commit comments

Comments
 (0)