PENDING
Task accepted into the system. No worker is assigned. Awaits admission control.
A Go task state machine. Six lifecycle stages, governed end to end by compare-and-swap, optimistic locking, and a versioned Mongo replica repository.
Task accepted into the system. No worker is assigned. Awaits admission control.
Admitted to the work queue. A single worker may claim it through compare-and-swap.
Owned by exactly one worker. Heartbeats extend the lease; a lost lease reverts to QUEUED.
Execution suspended and lease released. State and progress are persisted for resume.
Work finished and result committed. No further transitions are permitted.
Execution errored. Retried back to QUEUED while attempts remain, otherwise terminal.
Transitions are explicit and directional. The primary path advances left to right; recovery paths re-enter the queue. Every step below is a single compare-and-swap that either commits whole or not at all.
FAILED QUEUED / PAUSED RUNNING
Every transition is gated by a compare-and-swap on the current stage and version. The swap commits only if the observed state still matches the expected state. No transition is ever applied blind.
Each record carries a monotonically increasing version. A writer reads a version, mutates, and persists with a guard on that exact version. A mismatch means another writer won the race, and the write is rejected.
A stage transition is admitted from one writer at a time. Losing writers receive a conflict, reload current state, and re-evaluate. Lost worker leases are reclaimed without manual intervention.
State is persisted to a Mongo replica set. The repository never trusts a stale read: every write is conditioned on the version it observed, so a concurrent transition cannot be silently overwritten. The write majority spans the replica members before it is acknowledged.
Load the document and its current version from the replica set.
Filter the update on the matched _id and the expected version.
Set the next stage and increment the version in one atomic update.
matchedCount = 0 means a version conflict. Reload and retry.
The whole guarantee fits in one guarded update. The filter pins the expected stage and version; a zero match count is the conflict signal.
func (r *TaskRepo) Transition(ctx context.Context, id ID, from, to Stage, want int64) error {
filter := bson.M{"_id": id, "stage": from, "version": want}
update := bson.M{
"$set": bson.M{"stage": to, "updated_at": time.Now()},
"$inc": bson.M{"version": 1},
}
res, err := r.col.UpdateOne(ctx, filter, update)
if err != nil {
return fmt.Errorf("cas transition: %w", err)
}
if res.MatchedCount == 0 {
return ErrVersionConflict // another writer advanced the task
}
return nil
}