Skip to content

fix(cohort): stop target retry storm and ledger duplicate-key failures#369

Merged
ExtraToast merged 9 commits into
mainfrom
platform/cohort-incident-fix
Jun 29, 2026
Merged

fix(cohort): stop target retry storm and ledger duplicate-key failures#369
ExtraToast merged 9 commits into
mainfrom
platform/cohort-incident-fix

Conversation

@jorisjonkers-dev-agents

Copy link
Copy Markdown
Contributor

Summary

Two cohort-sync failure modes produced the recent wave of failed job executions; both are fixed here, reproduced by tests first.

Missing external target (retry storm + wrong auto-create). A membership ADD for a cohort with no external target enqueued cohort.materialize-target and threw a retryable exception, so the job exhausted its four attempts within seconds, while materialize() created a brand-new Brevo list instead of binding the list the members were already in. add() now fails terminally when the target is missing — no materialize enqueue, no provider call — while a missing user external id stays retryable. materialize() returns an existing id or fails terminally and never creates a target. linkExisting now fills an existing unbound cohort row, so an operator can bind the real list.

Reconcile duplicate key. The verify path stamped a desired row's external_user_id and then queried for the matching stranger; that SELECT auto-flushed the desired update ahead of the stranger soft-delete, colliding on uk_cohort_member_external. The ledger now claims and soft-deletes a matching stranger with a single flush before stamping the desired row, and refuses to stamp an external id already owned by another desired row. Cohort-member soft-delete uses NOW(6) so a rapid re-add cannot collide on a second-truncated sentinel. Reconcile recomputes external-id mappings inside the write transaction.

An admin endpoint POST /management/cohorts/{id}/repair-missing-adds re-enqueues missing ADDs for a bound cohort so members that previously failed converge once the target is linked.

No external-system deletion paths are added or widened; all removals here are local soft-deletes.

Tests

  • ./gradlew :services:api:compileKotlin :services:api:compileTestKotlin :services:api:test --tests "*cohort*" — 111 passed
  • ./gradlew :services:api:compileIntegrationTestKotlin — passed
  • CohortLedgerAutoflushIT reproduces the duplicate-key autoflush and the rapid soft-delete/re-add precision case; it runs against MariaDB in CI.

Add red tests for terminal missing cohort targets, no-create materialization, and filling an unbound existing mapping.
Make missing cohort targets terminal, keep stale materialize jobs no-create, and allow existing unbound cohort mappings to be linked.
Add red ordering tests for claim-before-stamp ledger transitions and a MariaDB integration test for the reconcile duplicate-key and soft-delete precision cases.
Collapse matching stranger rows before desired rows receive external ids, use microsecond soft-delete timestamps, and recompute reconciliation mappings inside the write transaction.
Add a red remediation contract and unit test for re-enqueueing unsynced desired ADDs after an operator binds an external target.
Expose an admin repair action that requires a bound cohort and re-enqueues ADD jobs for desired rows stranded by no-op rule evaluation.
Remove stale lazy-create wording now that materialize-target is no-create compatibility behavior and target creation is explicit.

@ExtraToast ExtraToast left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 🚀

@jorisjonkers-dev-agents jorisjonkers-dev-agents Bot force-pushed the platform/cohort-incident-fix branch from 8d3a286 to c67fc0f Compare June 29, 2026 21:31
@ExtraToast ExtraToast merged commit 3cb54e7 into main Jun 29, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant