Aller au contenu

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 @Scheduled ne 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(). Émet ING_PARSE_FAILED avec 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) : wraps IngestionService.tick(). Lit Environment.acceptsProfiles(Profiles.of("prod")) AND feature.artci.phone-auth.granted. Logique :
  • Sur les profils non-prod (dev, test, staging, local) → toujours procéder quel que soit le drapeau.
  • Sur prod AND 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 prod AND 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_SECONDS minimum 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 comme false avec 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 avec parse_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ÉLIMINAIRE per 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 false sur le profil prod ; 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 on IngestionInboxClient.
  • 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 skipped apparaît et que mc ls .../inbox/ montre toujours le fichier.
  • Insérer une ligne PICKED_UP périmée via psql ; redémarrer ; confirmer l'entrée audit_log et mc 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_FAILED par 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)