vet + lint + race unit tests, then bring up the stack and run the integration
suite and the end-to-end smoke test. One command for a reviewer to verify
everything.
A self-contained handoff for whoever (human or agent) implements the Python
worker: what the coordinator already does, where the contract lives
(openapi.yaml, building-workers.md), how to run it locally, the deliverable
(CTX-06), acceptance criteria, and the do-not-break rules.
- .github/workflows/coordinator.yml: vet, gofmt, race unit tests, golangci-lint,
and the integration suite against a Postgres service — on every push/PR that
touches coordinator/.
- docs/building-workers.md: a from-scratch guide for implementing a worker (the
claim/heartbeat/upload/complete loop, auth, lease semantics, status codes,
and the do-not-break rules), pointing at openapi.yaml for client generation.
- README: refresh the endpoint table, status, and test sections to match reality.
Polish pass hardening the queue and closing plan gaps.
- Task state machine gains `running`: the first heartbeat moves a task from
leased to running (migrations 0006/0007 add the enum value and extend the
lease-integrity check). verifyLease, ExpireLease, the reaper SQL, and job
progress all treat leased and running alike.
- Worker liveness: a heartbeat from a registered worker (UUID worker_id) bumps
its last_heartbeat_at online; a second background reaper marks workers offline
after WORKER_OFFLINE_AFTER of silence (RunReaper generalized to RunPeriodic).
- Request-size limits: JSON bodies capped at 1 MiB; dataset/artifact uploads
capped at MAX_UPLOAD_BYTES (default 1 GiB) via http.MaxBytesReader.
- Tests cover the running transition, liveness + offline reaper (unit over
memstore and integration over Postgres).
docs/openapi.yaml is a machine-readable mirror of the api-contract, covering
every coordinator endpoint with request/response schemas, auth, and error
codes. The Python author can generate a typed client or Pydantic models from it.
Its 12 paths match the router exactly; all $refs resolve.
- domain: NewJobWithTasks, DeriveStatus, NewUploadedJob, NewShardTask,
NewWorker, NewArtifact/SetContent (domain 47% -> 88%).
- internal/memstore: in-memory implementations of every usecase port, so
orchestration can be tested without Postgres or a filesystem.
- usecase: claim/renew/complete/fail/create/register/upload/submit-dataset
flows over memstore, including rule-10 cross-task rejection, idempotent
replay, lease sweep-on-claim, and dataset chunking (usecase 0% -> 73%).
- transport: httptest end-to-end over real use cases + memstore — auth,
readiness, full lifecycle, multipart upload + shard input, error mappings
(0% -> 70%).
- postgres integration: fix the tests broken by the artifact_id switch and add
worker-repo, artifact-repo, and shard-task (nullable input_uri) round-trips.
go test -race ./... is clean; golangci-lint (incl. integration tag) reports 0.
The coordinator can now ingest a dataset itself, not only accept client-supplied
chunk URIs.
- internal/chunk: a deterministic, generic TSV row splitter — repeats the header
per shard, buffers one shard at a time, rejects header-only input. Unit-tested.
- POST /jobs/upload (multipart): streams the dataset into an input artifact,
splits it into shard artifacts, and creates one shard task per shard, all in
one transaction; blobs are cleaned up if the transaction fails.
- GET /tasks/{id}/input streams a task's input shard back to the worker.
- domain: NewUploadedJob, NewShardTask, Task/Job.InputArtifactID; a shard task's
input is an artifact, not a URI. Claim response nests input:{uri,sha256} per
the contract, with uri = /tasks/{id}/input for shards.
- migration 0005 makes input_uri nullable and adds a has-input check.
- The existing URI-based POST /jobs path is untouched; both coexist.
The container runs as uid 10001, but the ./data and ./logs bind mounts were
root-owned, so blob storage failed with "mkdir .staging: permission denied"
and the coordinator crash-looped.
Pre-create the storage and log directories in the image owned by the
coordinator user, and switch the bind mounts to named volumes, which inherit
that ownership. The process can now write to them without running as root.
Task results are now coordinator-owned artifacts end to end.
- domain.Task carries ResultArtifactID instead of ResultURI/ResultSHA256;
CompleteWith and its idempotency key are keyed on the artifact id.
- CompleteTask verifies the referenced artifact was stored for this exact
task (rule 10): a worker cannot finish task B with task A's artifact, nor
name an id that isn't a partial_result. Mismatch → 409.
- POST /tasks/{id}/result takes {result:{artifact_id,...}}; ListResults and
ResultManifest follow.
- migration 0004 drops result_uri/result_sha256 and requires a completed task
to reference its result_artifact_id.
- smoke and requests.http exercise upload → complete-by-id → replay → conflict.
Wire the artifact storage foundation to HTTP.
- PUT /tasks/{id}/artifacts/{filename}: a worker streams a partial result;
the coordinator verifies lease ownership (foreign worker → 409), streams
the bytes to blob storage while hashing, and records the metadata. An
orphaned blob from a failed metadata insert is cleaned up.
- GET /artifacts/{id}/download: streams an artifact back with its content
type, length, and checksum.
- Ownership is read with a new non-locking TaskRepository.Get, so no row lock
is held across a long upload. Identity travels in X-Worker-ID / X-Task-Attempt
headers per the contract; upload/download bypass the short request timeout.
- docker-compose mounts ./data for durable artifact storage; smoke and
requests.http exercise an upload → foreign-409 → download round-trip.
Introduce durable, coordinator-owned artifacts — the model the master plan
requires instead of trusting worker-supplied result URIs.
- migration 0003: artifacts table + artifact_kind enum, plus nullable
input/result_artifact_id columns on jobs and tasks.
- domain.Artifact with NewArtifact/SetContent; storage keys derive from a
fresh UUID, never from a client filename (no path traversal).
- BlobStore and ArtifactRepository ports.
- blob.FSStore: filesystem blob storage that streams while hashing (SHA-256),
fsyncs, and atomically renames into place — a failed upload leaves neither a
committed artifact nor a staging file. Covered by unit tests.
- ArtifactRepo (squirrel) and COORDINATOR_STORAGE_DIR config.
HTTP upload/download handlers and the switch of result submission to
artifact_id come in the next parts.
Replace the positional-placeholder SQL in the repositories with the
Masterminds/squirrel builder, so column lists and $N numbering are no
longer maintained by hand. The optimistic-lock guard on task Update is now
a readable Where(id, version-1) instead of a $13-1 expression.
Two genuinely set-based statements stay as raw SQL on purpose — claimNext
(a FOR UPDATE SKIP LOCKED CTE) and expireLeases (CASE logic in the SET) —
because a builder cannot express them more clearly.
Align the coordinator with the master PLAN.md (CTX-00, CTX-04) and harden
process startup.
- CTX-00: freeze docs/api-contract.md as the v1 source of truth for the
Go coordinator and Python worker.
- CTX-04: worker registry — workers table (migration 0002), domain.Worker,
RegisterWorker use case, WorkerRepository, and POST /workers/register.
- Contract alignment: claim uses `capabilities` (was `workloads`),
COORDINATOR_TOKEN env (WORKER_AUTH_TOKEN kept as fallback), and
GET /health now reports database readiness (503 when the DB is down).
- Logging: logs are teed to stdout and an optional rotated file (LOG_FILE)
via lumberjack, so they survive a container rebuild.
- Startup resilience: the initial DB connection is retried with backoff,
so the coordinator waits for Postgres to boot instead of crash-looping.