Histoire ING-005 : Récupération au démarrage + ARTCI feature-flag prod gate + structured logging¶
Module : ingestion
Tranche : Sous-coupe de niveau 1 ING-001-d (architecture §11)
Side : [BACKEND]
Version cible : V0.0.0
Priorité : 5
Dépend de : ING-004 (table ingestion_import + status FSM existent ; émission d'audit pour ING_PARSE_FAILED câblé)
Peut être développé en même temps que : ING-006 (pack intégration-test - lecture seule sur les sorties de cette tranche)
Ordre de fusion : Après ING-004 ; avant ING-006
Complexité estimée : S
Histoires d'utilisateurs du PRD : Dérivé du PRD V0 §6.4 (ARTCI feature-flag prod gate) et de l'architecture §5.1 stage 4 (crash recovery)
Wireframe : N/A - backend uniquement
Objectif¶
Rendre le cron v0.0.0 sûr pour la production : le job IngestionStartupRecovery nettoie les lignes PICKED_UP bloquées et rendues orphelines par un crash en milieu d'analyse (architecture §5.1 stage 4 + §6.4), et le flag de la fonctionnalité feature.artci.phone-auth.granted court-circuite le cron sur le profil prod lorsque l'autorisation ARTCI est en attente (architecture §0 + ADR-08). La journalisation structurée rend le gated-skip visible aux ops sans enterrer l'avertissement.
Portée du backend¶
Entités¶
Aucune nouvelle. Lit + met à jour les lignes existantes de ingestion_import.
Migrations¶
Aucune.
Couche de service¶
IngestionStartupRecovery(ApplicationRunner, s'exécute UNE SEULE fois au démarrage avant que@Scheduledne s'active) :- Pour chaque tenant actif :
SELECT id, file_minio_inbox_key, filename FROM ingestion_import WHERE tenant_id= ? AND status='PICKED_UP' AND picked_up_at < now() - INTERVAL '${INGESTION_STARTUP_RECOVERY_STALE_THRESHOLD_SECONDS:300} seconds'. - Pour chaque correspondance : UPDATE
status='PARSE_FAILED',parse_error_detail = '{"reason" : "recovered_from_crash"}'::jsonb,finished_at = now(). ÉmetING_PARSE_FAILEDavec la même raison. Si le fichier inbox est toujours présent, MinIO COPY+DELETE inbox→/<tid>/rejeté/<ts>__<filename>avec un sidecar JSON{"reason" : "ingestion crashed during parse, please re-drop the file"}. - Exécution synchrone au démarrage, séquentielle par locataire, limitée par un budget total de 30 secondes - si cela prend plus de temps, l'application démarre quand même (la récupération reprend au prochain démarrage).
- Journal de niveau WARN par ligne récupérée :
[ING-RECOVERY] tenant=<tid> import=<id> filename=<f> reason=recovered_from_crash. ArtciProdGate(@Component) : wrapsIngestionService.tick(). LitEnvironment.acceptsProfiles(Profiles.of("prod"))ANDfeature.artci.phone-auth.granted. Logique :- Sur les profils non-prod (
dev,test,staging,local) → toujours procéder quel que soit le drapeau. - Sur
prodAND flag=false → log WARN[LEGAL-REVIEW] ingestion cron skipped - ARTCI phone-auth pending. files inbox NOT processed., return without calling MinIO LIST. Pas d'itération du locataire, pas de lecture de la base de données. - Sur
prodAND flag=true → procéder normalement. - Au démarrage (quel que soit le profil) lorsque le drapeau est faux, enregistrer une bannière WARN
[LEGAL-REVIEW] feature.artci.phone-auth.granted=false → cron d'ingestion désactivé dans le profil prod. - Crochet de journalisation structuré : chaque limite d'itération cron émet un INFO enrichi MDC d'une seule ligne avec
{tenantId, objectsFound, durationMs, status}(les journaux sont agrégés dans CloudWatch / Loki conformément àdocs/standards/java-spring-guidelines.md§ observability).
Points d'extrémité de l'API¶
Aucun nouveau.
Règles de validation¶
INGESTION_STARTUP_RECOVERY_STALE_THRESHOLD_SECONDSminimum 60 s (sauvegarde contre le marquage des lignes saines en vol comme ayant échoué). Valeur par défaut 300 (architecture §14.2).- La valeur de l'indicateur de fonctionnalité doit être un booléen analysable (
true/false) ; toute autre valeur est traitée commefalseavec un journal WARN (sécurité vers closed).
Considérations sur les multi-tenants¶
La récupération au démarrage itère TenantConfigCache.listActiveTenants() de la même manière que le cron. le champ d'application tenant_id est appliqué à chaque SELECT et UPDATE. Les émissions d'audit utilisent actor_type=SYSTEM, actor_id=ingestion-cron (identique à l'état stable). Les deux profils de base de données sont supportés - les requêtes de récupération passent par le même résolveur de source de données.
Audit & Logging¶
ING_PARSE_FAILEDémis une fois par ligne récupérée avecparse_error_detail.reason="recovered_from_crash"(catalogue déjà étendu par OQ-X10 dans le pré-requis d'ING-004).- WARN s'enregistre sur chaque saut de porte + chaque hit de démarrage-récupération.
- Aucun nouveau type d'événement n'a été ajouté.
Portée du frontend¶
N/A - backend uniquement.
États de l'interface utilisateur (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | N/A — backend only | — |
| Empty | N/A — backend only | — |
| Error | The sidecar JSON written by recovery is operator-facing | {"reason":"ingestion crashed during parse, please re-drop the file"} (English; matches existing sidecar shape from ING-004) |
| Offline / low network | N/A — backend only | — |
| Success | N/A — backend only | — |
Comportement réactif¶
N/A.
Interactions¶
N/A.
Critères d'acceptation¶
Architecture §11 ING-001 binding ACs 4-5 :
AC-001: Prod profile + flag=false ⇒ cron tick is a no-op
Given the application running with `spring.profiles.active=prod`
And `feature.artci.phone-auth.granted=false`
And a file in `/<tenantA>/inbox/clean-5-rows.csv`
When `IngestionService.tick()` fires
Then no MinIO LIST is performed
And no `ingestion_import` row is created
And the file remains in `/<tenantA>/inbox/`
And the WARN log line `[LEGAL-REVIEW] ingestion cron skipped — ARTCI phone-auth pending. files in inbox NOT processed.` is emitted.
AC-002: Non-prod profiles ignore the feature flag
Given the application running with `spring.profiles.active=dev` (or `test`, `staging`, `local`)
And `feature.artci.phone-auth.granted=false`
When `IngestionService.tick()` fires
Then the cron proceeds normally (MinIO LIST + downstream FSM)
And no `[LEGAL-REVIEW] cron skipped` log line is emitted.
AC-003: Stuck PICKED_UP row >5 min old is recovered at boot
Given an `ingestion_import` row inserted with `status='PICKED_UP'` and `picked_up_at = now() - 10 minutes` for tenant A
And the corresponding file `stale.csv` still present in `/<tenantA>/inbox/`
When the application boots and `IngestionStartupRecovery` runs once
Then the row is updated to `status='PARSE_FAILED'` with `parse_error_detail.reason="recovered_from_crash"` and `finished_at` set
And the file is moved to `/<tenantA>/rejected/<ts>__stale.csv` with sidecar JSON `{"reason":"ingestion crashed during parse, please re-drop the file"}`
And `ING_PARSE_FAILED` is present in `audit_log` for that `import_id`.
Règles de conformité¶
- ARTCI loi 2013-450 Art. 14 (consentement préalable au démarchage) -
[LEGAL-REVIEW] PRÉLIMINAIREper V0 PRD §6.4. Le drapeau default-false posture est la mitigation du temps de production. Le journal WARN au démarrage assure l'auditabilité. - Architecture ADR-08 : drapeau par défaut
falsesur le profilprod; ignoré ailleurs. - Règle critique #3 : chaque ligne récupérée émet
ING_PARSE_FAILED- le catalogue admet le type d'événement.
Normes et conventions¶
docs/standards/compliance-discipline.md-[LEGAL-REVIEW]warning posture.docs/standards/critical-rules.md(#3 audit catalog).docs/standards/java-spring-guidelines.md§ApplicationRunner, § MDC logging.docs/standards/multi-tenant-model.md§ contexte cron.
Exigences de test¶
Tests unitaires¶
ArtciProdGateTest- matrice{prod|dev|staging|test|local} × {flag=true|false|garbage|absent}; asserts skip vs proceed and exact log line.IngestionStartupRecoveryTest(mocked DB + MinIO) - threshold honored ; younger PICKED_UP rows untouched.
Tests d'intégration (TestContainers)¶
IngestionStartupRecoveryIT- AC-003 de bout en bout avec une vraie base de données Postgres + MinIO.IngestionProdFlagClosedIT- AC-001 setup ; assert zero MinIO requests via spy onIngestionInboxClient.IngestionDevFlagIgnoredIT- AC-002 affirmation que le cron se déroule.
Ce que QA validera¶
- Démarrez avec
SPRING_PROFILES_ACTIVE=prod FEATURE_ARTCI_PHONE_AUTH_GRANTED=false; déposez un fichier ; suivez les logs ; confirmez que la ligne[LEGAL-REVIEW] cron skippedapparaît et quemc ls .../inbox/montre toujours le fichier. - Insérer une ligne PICKED_UP périmée via psql ; redémarrer ; confirmer l'entrée
audit_logetmc ls .../rejected/.
Hors du champ d'application¶
- Le pack de tests d'intégration transversaux (isolation du locataire, fichier volumineux, octet magique, matrice complète de récupération en cas de crash) - ING-006.
- Coordination ShedLock / cron multi-instances - V1 (déploiement d'une seule instance dans la v0.0.0 ; ADR-03).
- Badge de notification de l'opérateur pour les fichiers rejetés - V0.0.1.
- Alerte par courriel / SMS en cas de récupération - V2.
- Réessai automatique des lignes récupérées - jamais (l'opérateur doit redéposer ; architecture §6.4 explicite).
- Interface utilisateur avec drapeau dans la console d'administration - V1+ (env-var seulement dans v0.0.0).
Définition de Fait¶
- [Tous les endpoints du backend sont implémentés et renvoient des réponses correctes (aucun n'a été ajouté)
- [Toutes les pages du frontend s'affichent correctement à 360 / 768 / 1024 px (N/A - backend uniquement)
- [Les 5 états de l'interface utilisateur sont implémentés (N/A - backend uniquement)
- [ ] La micro-copie française correspond exactement aux spécifications de conception (forme du sidecar réutilisée à partir d'ING-004 ; chaîne de motif de récupération définie)
- [Tous les critères d'acceptation sont passés manuellement
- [Tests unitaires et d'intégration rédigés et réussis
- [L'isolation des locataires est vérifiée (itérations de récupération par locataire ; fumée verte partagée + BYO)
- [ ] [Histoires de clients] payload + Slow-3G + resumability (N/A - backend uniquement)
- [Les entrées du journal d'audit sont présentes pour chaque événement requis (
ING_PARSE_FAILEDpar ligne récupérée) - [Aucune défaillance silencieuse sur les erreurs de réseau (les échecs de déplacement MinIO de la récupération sont consignés dans WARN ; la ligne reste PARSE_FAILED - la relance de la récupération est idempotente)