Skip to content

Commit 3f58eb0

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 3140132 commit 3f58eb0

5 files changed

Lines changed: 92 additions & 4 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
@@ -58,11 +58,25 @@ def fetch_external_data
5858
Success(etablissement:, value: external_id)
5959
in Failure(type: :not_found, **)
6060
Failure(retryable: false, error: StandardError.new('NotFound'), code: 404)
61-
in Failure(type:, code:, retryable:, **)
62-
Failure(retryable:, error: StandardError.new("API Entreprise: #{type}"), code:)
61+
in Failure(type:, code:, retryable: true, **)
62+
Failure(retryable: true, error: StandardError.new("API Entreprise: #{type}"), code:)
63+
in Failure(type:, code:, **)
64+
# Any other technical error also converges to degraded mode: only a 404
65+
# (nonexistent SIRET) leaves the champ in external_error.
66+
Sentry.capture_message("SiretChamp: degraded mode fallback after API Entreprise error", extra: { champ: id, type:, code: })
67+
Success(etablissement: create_degraded_etablissement, value: external_id)
6368
end
6469
end
6570

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

7690
private
7791

92+
def create_degraded_etablissement
93+
APIEntrepriseService.create_etablissement_as_degraded_mode(self, external_id.delete(" "), dossier.user&.id)
94+
end
95+
7896
# We want to validate if SIRET really exists
7997
# It's valid when an etablissement have been created in turbo with SIRET controller
8098
# 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
@@ -79,6 +81,11 @@ def has_async_external_data? = false
7981

8082
def external_data_needed_for_validation? = has_async_external_data?
8183

84+
# Called by ChampFetchExternalDataJob when all retry attempts are exhausted.
85+
def handle_exhausted_external_data_retries!
86+
external_data_error!
87+
end
88+
8289
private
8390

8491
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: 38 additions & 0 deletions
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,6 +290,29 @@
275290
end
276291
end
277292

293+
describe '#handle_exhausted_external_data_retries!' do
294+
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }]) }
295+
let(:dossier) { create(:dossier, procedure:) }
296+
let(:champ) { dossier.champs.first }
297+
298+
before do
299+
champ.update_columns(
300+
external_id: '41816609600051',
301+
external_state: 'waiting_for_job',
302+
fetch_external_data_exceptions: [ExternalDataException.new(error: 'boom', code: 503)]
303+
)
304+
end
305+
306+
it 'converges to a degraded etablissement instead of external_error' do
307+
expect { champ.handle_exhausted_external_data_retries! }
308+
.to have_enqueued_job(APIEntreprise::EtablissementJob)
309+
310+
champ.reload
311+
expect(champ).to be_fetched
312+
expect(champ.etablissement).to be_as_degraded_mode
313+
end
314+
end
315+
278316
describe '#external_data_required_for_conditions?' do
279317
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }, { type: :text }]) }
280318
let(:siret_tdc) { procedure.draft_revision.types_de_champ_public.first }

0 commit comments

Comments
 (0)