Skip to content

Guard campaign enrollment against stale state and duplicate writes - #17

Open
TheGuyWithoutH wants to merge 1 commit into
moaljumaa:mainfrom
Pilae:codex/enrollment-state-guard
Open

TheGuyWithoutH wants to merge 1 commit into
moaljumaa:mainfrom
Pilae:codex/enrollment-state-guard

Conversation

@TheGuyWithoutH

Copy link
Copy Markdown

Summary

Makes adding prospects to an existing campaign safer. This is a backend enrollment fix, not a change to the outreach runner.

What it fixes

  • Competing enrollment requests: eligibility was checked before the write transaction. Two requests could both see a prospect as eligible before either inserted them, causing duplicate enrollment across runs or a uniqueness error within the same run. Eligibility is now rechecked while holding SQLite's write lock, so a competing enrollment committed first is skipped.
  • Repeated target IDs: submitting [Alice, Alice, Bob] now enrolls Alice and Bob only once.
  • Stale campaign state: clients can optionally send expected_status (pending, paused, or running). If the run's current status differs at enrollment time, the API returns HTTP 409 run_status_changed without enrolling targets.
  • Accurate counts: the response reports how many prospects were actually inserted, rather than how many initially appeared eligible.

For example: a client previews a paused campaign, someone resumes it, then the client submits new prospects with expected_status: "paused". Enrollment is rejected, allowing the client to refresh before proceeding.

How it works

The handler starts an immediate SQLite transaction before checking the expected status and rechecking each eligible target's enrollment in the workflow and active enrollment elsewhere. Profile and track inserts remain in the same transaction, so an insertion failure rolls back the batch.

GET advertises support through { expectedStatusGuard: true }. Existing clients can omit expected_status and retain their current behavior. Requests with no eligible targets remain successful no-ops.

Scope and limits

  • The existing UI does not send expected_status. It benefits from duplicate protection, but stale-state protection requires a follow-up to send the previewed status and handle HTTP 409.
  • The separate new-campaign creation endpoint (POST /api/runs) is unchanged. This is not a system-wide enrollment-concurrency fix.
  • The status guard checks the current status, not whether the run changed status and later returned to the same value.

Validation

Implementation validation reported:

  • 11 regression tests passed using real in-memory SQLite and synthetic targets, covering duplicate IDs, repeat requests, matching statuses, state changes, competing enrollments, and rollback on insertion failure.
  • TypeScript check passed: tsc --noEmit --incremental false.
  • Tests ran in an isolated container with networking disabled; no live outreach or sandbox database changes.

The tests inject competing writes between preview reads and the transaction deterministically; they do not run simultaneous workers or contact LinkedIn.

The subsequent review inspected the diff and surrounding code but could not independently rerun the tests because local dependencies were absent (typescript missing).

Run after installing dependencies:

node --test tests/enrollment.test.cjs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant