Skip to content

Commit 5a370cc

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 63b97da commit 5a370cc

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
@@ -234,6 +234,21 @@
234234
end
235235
end
236236

237+
context 'when API returns a non-retryable technical failure' do
238+
before do
239+
allow(APIEntrepriseService).to receive(:create_etablissement_with_fallback)
240+
.and_return(Dry::Monads::Failure(type: :forbidden, code: 403, retryable: false))
241+
allow(Sentry).to receive(:capture_message)
242+
end
243+
244+
it 'converges to a degraded etablissement instead of external_error' do
245+
result = champ.fetch_external_data
246+
expect(result).to be_success
247+
expect(result.value![:etablissement]).to be_as_degraded_mode
248+
expect(Sentry).to have_received(:capture_message)
249+
end
250+
end
251+
237252
context 'when API returns Success in degraded mode' do
238253
let(:etablissement) { instance_double(Etablissement, as_degraded_mode?: true) }
239254
before do
@@ -275,7 +290,7 @@
275290
end
276291
end
277292

278-
describe '#external_data_status_message' do
293+
describe '#external_data_status_message (unit)' do
279294
let(:champ) { Champs::SiretChamp.new }
280295

281296
it 'returns nil when fetched' do
@@ -324,6 +339,29 @@
324339
end
325340
end
326341

342+
describe '#handle_exhausted_external_data_retries!' do
343+
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }]) }
344+
let(:dossier) { create(:dossier, procedure:) }
345+
let(:champ) { dossier.champs.first }
346+
347+
before do
348+
champ.update_columns(
349+
external_id: '41816609600051',
350+
external_state: 'waiting_for_job',
351+
fetch_external_data_exceptions: [ExternalDataException.new(error: 'boom', code: 503)]
352+
)
353+
end
354+
355+
it 'converges to a degraded etablissement instead of external_error' do
356+
expect { champ.handle_exhausted_external_data_retries! }
357+
.to have_enqueued_job(APIEntreprise::EtablissementJob)
358+
359+
champ.reload
360+
expect(champ).to be_fetched
361+
expect(champ.etablissement).to be_as_degraded_mode
362+
end
363+
end
364+
327365
describe '#external_data_required_for_conditions?' do
328366
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }, { type: :text }]) }
329367
let(:siret_tdc) { procedure.draft_revision.types_de_champ_public.first }

0 commit comments

Comments
 (0)