Aller au contenu

Story OP-004: Backend CampaignDispatchRecovery (impl complète)

Module: operator-console Slice: Architecture §6.3 (filet recovery) + OP-ADR-01 Side: [BACKEND] Version target: V0.0.0 Priority: 5 Depends on: OP-003 (worker doit exister) Can develop concurrently with: OP-005, OP-006 Merge order: Any order after OP-003 Estimated complexity: S PRD User Stories: V0.0.0 envelope (CampaignDispatchRecovery) Wireframe: N/A — backend only


Objective

Implémenter complètement le job CampaignDispatchRecovery (stub OP-000) qui rattrape les campaign_contracts INCLUS bloqués dans des campagnes ENVOYÉES suite à un crash JVM mi-fan-out ou une corruption d'outbox. Filet de sécurité de OP-ADR-01.


Backend Scope

Entities

Lit campaign_contracts, notifications (NOTIF table, lookup via service). Aucune mutation directe ; délègue à CampaignSendWorker.

Migrations

Aucune. L'index partiel idx_cc_recovery ON campaign_contracts (tenant_id, campaign_id, status) WHERE status='INCLUS' est déjà créé en OP-000.

Service Layer

  • CampaignDispatchRecovery (@Component, remplace le stub OP-000) :
  • @Scheduled(fixedDelayString = "${op.recovery.interval-ms:300000}") — toutes les 5 min en prod, 60 s en dev (.env.example).
  • @Scheduled(initialDelay = 5000) au démarrage (startup scan).
  • Algo :
    Pour chaque tenant actif (TenantConfigCache.allTenants()) :
      Pour chaque campagne ENVOYÉE avec ≥ 1 cc INCLUS (index idx_cc_recovery) :
        Republier CampaignValidated (event_publication insert) → CampaignSendWorker reprend.
    
  • Batch size configurable : op.recovery.batch-size=100.
  • Lock distribuée : SELECT FOR UPDATE SKIP LOCKED sur les campagnes éligibles → tolère réplicas multiples (V1+).
  • Idempotence : repose entièrement sur notifications.UNIQUE(campaign_contract_id) côté NOTIF et UNIQUE côté AUTH (déjà testée en OP-003).

  • CampaignDispatchRecoveryHealthIndicator (HealthIndicator Spring Actuator) :

  • Expose op_dispatch_recovery_lag = nombre de cc INCLUS dans des campagnes ENVOYÉES depuis > 10 min.
  • Status OUT_OF_SERVICE si lag > 500 (déclenche alerte ops §10.2 arch).

API Endpoints

Aucun endpoint REST exposé directement. L'observation passe par OP-020 (health endpoint dédié).

Validation Rules

N/A (job interne).

Multi-Tenant Considerations

  • Itère sur tous les tenants ; pour chaque tenant, ouvre TenantContextHolder (set tenant_id) pour que TenantDataSourceResolver route correctement vers la datasource du tenant en profil BYO.
  • En profil SHARED (V0.0.0), une seule datasource → un seul scan SQL global avec GROUP BY tenant_id plus efficace ; deux implémentations possibles, l'arch préconise l'itération par tenant pour BYO compat.

Audit & Logging

  • Aucun audit event nouveau (le worker réémet CONTRACT_ADDED_TO_CAMPAIGN × N en cas de reprise, ce qui suffit à tracer).
  • Log INFO à chaque scan : {scannedTenants, scannedCampaigns, recoveredContracts, durationMs}.
  • Métriques Micrometer : op_dispatch_recovery_lag (gauge), op_dispatch_recovery_scans_total (counter), op_dispatch_recovery_duration (histogram). Émis dès cette story même si les métriques globales sont consolidées en OP-020.

Acceptance Criteria

AC-001: Recovery après crash mi-fan-out réinjecte les contrats restants
Given une campagne ENVOYÉE avec 12 cc, 5 NOTIF_EN_ATTENTE et 7 INCLUS
When CampaignDispatchRecovery.runRecovery() est appelé manuellement
Then les 7 INCLUS passent à NOTIF_EN_ATTENTE après convergence du worker
And aucun doublon dans notifications (vérifié count = 12, pas 19).

AC-002: Idempotence des invocations multiples
Given une campagne dans l'état post-AC-001
When CampaignDispatchRecovery.runRecovery() est rejoué 3 fois consécutives
Then le nombre de notifications reste à 12, les statuts inchangés.

AC-003: Scan startup déclenché au boot
Given une campagne ENVOYÉE avec 1 cc INCLUS bloqué (avant boot)
When l'application démarre
Then dans les 10 s, le cc est NOTIF_EN_ATTENTE (sans intervention manuelle).

Compliance Rules

  • OP-ADR-01 : recovery = filet de sécurité de l'outbox. VÉRIFIÉ.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md@Scheduled, @SchedulerLock (ShedLock optionnel V1).
  • docs/standards/database-guidelines.mdSELECT FOR UPDATE SKIP LOCKED pour concurrence.

Testing Requirements

Unit Tests

  • CampaignDispatchRecoveryTest : algo de scan, batch size respecté.

Integration Tests

  • RecoveryAfterPartialFanOutIT AC-001 (réutilise §13.4 arch).
  • RecoveryIdempotenceIT AC-002.
  • RecoveryStartupScanIT AC-003.

What QA Will Validate

  • Injecter manuellement 5 cc INCLUS, observer la convergence < 5 min.
  • Logs structurés présents.

Out of Scope

  • Métriques Prometheus agrégées + alertes → OP-020.
  • ShedLock multi-instance → V1 (un seul replica V0).

Definition of Done

  • [ ] CampaignDispatchRecovery impl complète remplace le stub OP-000
  • [ ] AC-001..AC-003 passent en intégration
  • [ ] Scan startup + scan périodique fonctionnent
  • [ ] Idempotence prouvée (AC-002)
  • [ ] Métriques émises (3 metrics nommées correctement)
  • [ ] HealthIndicator branché sur /actuator/health
  • [ ] Property op.recovery.interval-ms documentée dans .env.example
  • [ ] Tests intégration verts