ASAQE / SYSTEMS / UNIT-TASKFLOW

TaskFlow

A Go task state machine. Six lifecycle stages, governed end to end by compare-and-swap, optimistic locking, and a versioned Mongo replica repository.

06
STAGES
CAS
GUARANTEE
OPT
LOCK
MONGO
STORE
STAGE 01

PENDING

Task accepted into the system. No worker is assigned. Awaits admission control.

version = 0QUEUED
STAGE 02

QUEUED

Admitted to the work queue. A single worker may claim it through compare-and-swap.

claimed_by = nullRUNNING
STAGE 03

RUNNING

Owned by exactly one worker. Heartbeats extend the lease; a lost lease reverts to QUEUED.

lease.holder = worker_idPAUSED / COMPLETED / FAILED
STAGE 04

PAUSED

Execution suspended and lease released. State and progress are persisted for resume.

checkpoint != nullRUNNING / CANCELLED
STAGE 05

COMPLETED

Work finished and result committed. No further transitions are permitted.

result != nullTERMINAL
STAGE 06

FAILED

Execution errored. Retried back to QUEUED while attempts remain, otherwise terminal.

attempts <= max_retriesQUEUED / 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.

PENDINGQUEUEDRUNNINGCOMPLETED

FAILED QUEUED   /   PAUSED RUNNING

CAS

Compare-and-swap governance

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.

OPT

Optimistic locking

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.

CNC

Concurrency control

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.

  1. 01READ

    Load the document and its current version from the replica set.

  2. 02GUARD

    Filter the update on the matched _id and the expected version.

  3. 03SWAP

    Set the next stage and increment the version in one atomic update.

  4. 04VERIFY

    matchedCount = 0 means a version conflict. Reload and retry.

TOPOLOGY
Replica set
WRITE CONCERN
majority
LOCK
Version check

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
}