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 :
- Batch size configurable :
op.recovery.batch-size=100. - Lock distribuée :
SELECT FOR UPDATE SKIP LOCKEDsur les campagnes éligibles → tolère réplicas multiples (V1+). -
Idempotence : repose entièrement sur
notifications.UNIQUE(campaign_contract_id)côté NOTIF etUNIQUEcôté AUTH (déjà testée en OP-003). -
CampaignDispatchRecoveryHealthIndicator(HealthIndicatorSpring Actuator) : - Expose
op_dispatch_recovery_lag= nombre de cc INCLUS dans des campagnes ENVOYÉES depuis > 10 min. - Status
OUT_OF_SERVICEsi 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 queTenantDataSourceResolverroute 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_idplus 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.md—SELECT FOR UPDATE SKIP LOCKEDpour concurrence.
Testing Requirements¶
Unit Tests¶
CampaignDispatchRecoveryTest: algo de scan, batch size respecté.
Integration Tests¶
RecoveryAfterPartialFanOutITAC-001 (réutilise §13.4 arch).RecoveryIdempotenceITAC-002.RecoveryStartupScanITAC-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¶
- [ ]
CampaignDispatchRecoveryimpl 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-msdocumentée dans.env.example - [ ] Tests intégration verts