Skip to content

Serialize first-injection handling per TaskID so concurrent injects can't both mint a reference ID #315

Description

@sthanikan2000

Problem

CreateApplication decides whether an application is new by reading it back first:

existing, err := s.store.GetByTaskID(req.TaskID)

Two concurrent injects of the same TaskID can both get gorm.ErrRecordNotFound, both generate a reference ID, and both call CreateOrUpdate. task_id is the primary key and Save upserts, so only one row survives — but two counter values are consumed and either ID can be the one that persists.

Tolerable today, and not introduced by the reference ID work — the CreateConsignment call in the same function already has the race. NSW doesn't send concurrent duplicate injects for one task, and refid counters are explicitly not gapless, so a wasted value is by design rather than a defect. Raised by review on #307 and deliberately deferred there.

Worth revisiting if inject volume grows or the service runs multiple replicas, since the window is a full read-then-write round trip.

Proposed Solution

Make first-injection handling atomic per TaskID. Two options:

  • A conditional insert plus retryINSERT ... ON CONFLICT (task_id) DO NOTHING, then treat "no row inserted" as "already existed" and re-read instead of generating. Portable across SQLite and PostgreSQL, and needs no new locking primitive.
  • A PostgreSQL advisory lock keyed on the task ID around the read-then-write. Simpler to reason about, but Postgres-only, so the SQLite dev/test path needs its own answer.

Either way, add a concurrency regression test asserting one Generate call and one persisted reference ID for N parallel injects of the same task.

Alternatives

  • Leave as-is — what feat(refid): generate agency reference IDs on application inject #307 does. The failure mode is a wasted counter value and a coin-flip between two valid IDs, neither of which corrupts anything.
  • Generate inside CreateOrUpdate's transaction — narrows the window but doesn't close it, since the existence check would still happen outside, and it couples the store to reference ID generation.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type/ImprovementEnhancement to existing functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions