8.7 KiB
Task: Go/PostgreSQL task queue service
Goal
Implement a durable task-queue service in Go, backed by PostgreSQL. It must store task state, atomically lease one pending task to a worker, persist result metadata, and expose the data required by a future stitcher/report stage.
This task implements the queue and persistence layer from the architecture sketch. It does not implement the Worker Daemon or scientific/CV computation.
Technical baseline
- Language: Go 1.22+.
- Database: PostgreSQL 15+.
- PostgreSQL driver and pool:
github.com/jackc/pgx/v5/pgxpool. - HTTP server: standard
net/httpis sufficient; do not introduce a framework unless it solves a concrete requirement. - Schema migrations: versioned SQL migrations using
golang-migrate. - Result storage: a coordinator-managed local artifact directory in the first version; keep its interface replaceable by object storage later.
- Identifiers: UUID.
- Times: timezone-aware UTC timestamps.
- API boundary: the Go coordinator owns all database access. Python workers communicate only over HTTP and never receive database credentials.
If the coordinator application does not yet exist, create it with this minimal layout. Do not add a distributed scheduler.
coordinator/
cmd/coordinator/main.go
internal/config/
internal/httpapi/
internal/queue/
internal/store/postgres/
migrations/
main.go must create one pgxpool.Pool, apply no migrations automatically in
production, wire dependencies, and perform graceful shutdown. Migrations are a
separate explicit command in CI/deployment.
Data model
jobs
One user submission that may be split into several tasks.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | Generated by the service |
workload |
text | Allowed workload name |
input_uri |
text | Original dataset/video location |
parameters |
JSONB | Validated job-level parameters |
status |
enum | pending, running, completed, failed, cancelled |
created_at, completed_at |
timestamptz | UTC |
tasks
One independently executable chunk. For an unsplit SciMesh workload, create one task. For a video job, create one task per chunk.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | Task identifier returned to workers |
job_id |
UUID FK | References jobs.id |
chunk_index |
integer | Unique within a job |
workload |
text | Copied from job or an explicit task override |
input_uri |
text | Chunk or dataset artifact |
input_sha256 |
text | Required checksum |
parameters |
JSONB | Task-specific validated parameters |
status |
enum | pending, leased, completed, failed, cancelled |
attempt |
integer | Starts at 0; incremented atomically on claim |
max_attempts |
integer | Default configurable, e.g. 3 |
lease_owner |
text nullable | Worker ID |
lease_expires_at |
timestamptz nullable | UTC |
result_uri |
text nullable | Result CSV/artifact location |
result_sha256 |
text nullable | Checksum of result artifact |
metrics |
JSONB nullable | Safe execution metrics |
error_code, error_message |
text nullable | Sanitized failure information |
created_at, started_at, completed_at |
timestamptz | UTC |
version |
integer | Optimistic-concurrency/version marker |
Required constraints and indexes:
- unique
(job_id, chunk_index); - index for claims:
(status, lease_expires_at, created_at); - index on
job_id; - check
attempt >= 0andmax_attempts > 0; - completed tasks must have
result_uriandresult_sha256; - a leased task must have
lease_ownerandlease_expires_at.
Required Go service operations
Keep HTTP handlers thin. Define a small queue service interface and a PostgreSQL
implementation; do not expose pgx rows or SQL details to handlers.
type QueueService interface {
CreateJobWithTasks(ctx context.Context, input CreateJobInput) (Job, error)
ClaimNextTask(ctx context.Context, input ClaimInput) (*ClaimedTask, error)
RenewLease(ctx context.Context, input RenewLeaseInput) (ClaimedTask, error)
CompleteTask(ctx context.Context, input CompleteTaskInput) (Task, error)
FailTask(ctx context.Context, input FailTaskInput) (Task, error)
ExpireLeases(ctx context.Context, now time.Time) (int64, error)
GetJobStatus(ctx context.Context, jobID uuid.UUID) (JobStatus, error)
ListCompletedResults(ctx context.Context, jobID uuid.UUID) ([]ResultManifest, error)
}
Every database call must receive a request-scoped context.Context. Configure
connection-pool size, database URL, and query/request timeouts via environment
variables. Use parameterized pgx queries only; do not build SQL with string
interpolation.
claim_next_task must be atomic and safe with multiple coordinator processes.
Use one transaction with SELECT ... FOR UPDATE SKIP LOCKED, then update the
selected row to leased, increment attempt, and set the lease fields. Do not
implement claiming as SELECT followed by a separate unguarded UPDATE.
Pseudo-SQL:
WITH candidate AS (
SELECT id
FROM tasks
WHERE status = 'pending'
AND attempt < max_attempts
AND workload = ANY($1)
ORDER BY created_at, chunk_index
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE tasks
SET status = 'leased',
attempt = attempt + 1,
lease_owner = $2,
lease_expires_at = $3,
started_at = COALESCE(started_at, $4),
version = version + 1
FROM candidate
WHERE tasks.id = candidate.id
RETURNING tasks.*;
Expired leases must be returned to pending only when attempts remain; otherwise
mark the task failed with a lease-expired error. This should run before or as
part of each claim and as a periodic coordinator maintenance operation.
Go coordinator HTTP endpoints
The Worker Daemon task depends on these endpoints. Claiming mutates state, so it
must be POST rather than GET.
| Endpoint | Behavior |
|---|---|
POST /jobs |
Validate submission, create job and pending tasks transactionally |
POST /tasks/claim |
Atomically lease one compatible task; 204 when none exists |
POST /tasks/{task_id}/heartbeat |
Renew the current worker's lease; return the new lease_expires_at |
PUT /tasks/{task_id}/artifacts/{filename} |
Stream one result artifact into coordinator storage; verify the current worker lease |
POST /tasks/{task_id}/result |
Idempotently persist a completed result manifest |
POST /tasks/{task_id}/failure |
Record a safe failure or retryable state |
GET /jobs/{job_id} |
Return aggregate job/task progress |
Every task-mutating handler must validate JSON, verify worker_id and
attempt, and map typed service errors to HTTP responses. Reject a stale or
foreign lease with 409 Conflict. Completing the same task/attempt with the
same result manifest must be idempotent; a different manifest is a conflict.
Use JSON request/response DTOs in internal/httpapi; map them to typed queue
inputs in the handler. Do not return raw database errors to clients. Return a
request ID in error responses and emit structured logs with the request ID,
task ID, worker ID, and operation.
The artifact endpoint receives binary content with Content-Type,
X-Worker-ID, and X-Task-Attempt headers. It must stream the request body to
the coordinator artifact directory instead of buffering it in memory, calculate
or verify its SHA-256, and return 201 with the durable artifact uri. Only
the worker holding the current lease may upload. POST /result accepts only
URIs returned by this endpoint for the same task and attempt.
Tests and acceptance criteria
golang-migrateupgrades an empty PostgreSQL database to the current schema and can roll back the latest migration in a test database.- Creating a job either creates all of its tasks or creates none.
- Concurrent claim tests prove that each pending task is leased to one worker
only; use a real PostgreSQL instance supplied through
TEST_DATABASE_URL, not SQLite or an in-memory mock. - Lease expiry returns an unfinished task to the queue or fails it after the final permitted attempt.
- A worker cannot renew, fail, or complete a task leased to another worker.
- Result submission is idempotent for the same attempt and manifest.
list_completed_results(job_id)returns a deterministic order bychunk_index, suitable for the future stitcher.- API tests cover
204for an empty queue,409for stale attempts, and aggregate job status. go test ./...passes; rungo vet ./...in CI.
Out of scope
- Python Worker Daemon execution and input download;
- video chunk generation and trajectory stitching;
- authentication provider, UI, object-storage implementation, and PDF report;
- network/distributed coordinator deployment beyond the local coordinator process.