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.
SciMesh Coordinator
Durable task-queue server for SciMesh, in Go on PostgreSQL. It owns all database access; workers talk to it only over HTTP and never receive DB credentials.
Built as a modular monolith following Clean Architecture — one binary, four
layers, dependencies pointing strictly inward. See
docs/database-integration-task.md and docs/worker-daemon-task.md in the repo
root for the full contract.
Layers
infra config, pgxpool, http.Server, clock ← frameworks & drivers
transport http handlers ← inbound: who calls us
storage sql repositories ← outbound: who we call
usecase business operations + PORTS ← application rules
domain Task, Job + their invariants ← enterprise rules
┌── transport ──┐
domain ◄── usecase ◄┤ ├◄── infra
└── storage ────┘
transport and storage are one layer — the "interface adapters" ring — split
by direction rather than by category, so a file's path tells you its role.
The rule that matters: source dependencies point only inward. domain
imports nothing from this module; usecase sees only domain; transport and
storage know nothing of each other. Verify it at any time with:
go list -f '{{range .Imports}}{{.}}{{"\n"}}{{end}}' ./internal/domain | grep internal # must be empty
Layout
coordinator/
cmd/coordinator/main.go # composition root: the only place with concrete types
internal/
domain/ # entities + rules, no I/O
task.go Task, lease/complete/fail/expire transitions
job.go Job, chunk fan-out, status derivation
errors.go business-rule violations
usecase/ # one type per operation, dependencies injected
ports.go TaskRepository, JobRepository, TxManager, Clock
dto.go use-case boundary inputs
task.go claim, renew, complete, fail, expire
job.go create, status, results, stitch
transport/http/ # routing, DTOs, middleware, error mapping
storage/postgres/ # SQL behind the ports; TxManager via context
infra/ # config.go db.go clock.go server.go
migrations/ # golang-migrate SQL, run as an explicit command
A full map — file-by-file table, a request traced through every layer, and a "where do I add X" guide — lives in ARCHITECTURE.md.
Quickstart
With Docker (nothing to install but Docker)
make up # Postgres → migrations → coordinator
curl localhost:8080/health
make logs # follow the coordinator
make down # stop (add down-clean to drop the DB volume)
up starts three services in order: Postgres waits until pg_isready passes, a
one-shot migrate container applies the schema and exits, and only then does the
coordinator start — so it never queries a database that has no tables.
Needs BuildKit. The Dockerfile uses
RUN --mount=type=cacheto reuse the Go module and compiler caches between builds. If the build fails with "the --mount option requires BuildKit", install the buildx plugin —pacman -S docker-buildxon Arch,apt install docker-buildx-pluginon Debian.
Locally, against your own Postgres
cp .env.example .env # then edit DATABASE_URL / WORKER_AUTH_TOKEN
# it is loaded automatically — no export needed
make tidy # fetch deps (needs network once)
make migrate-up # apply schema (needs the migrate CLI)
make run # start the server
Configuration
Settings come from the environment. A .env file is loaded at startup via
godotenv as a local-dev convenience (override its path with ENV_FILE):
- a missing
.envis not an error — production injects real env vars; - real environment variables always win over the file, so an orchestrator's
values are never shadowed by a stale
.envbaked into an image.
See .env.example; only DATABASE_URL is required.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /jobs |
Create job + pending tasks transactionally |
| POST | /tasks/claim |
Atomically lease one task (204 if none) |
| POST | /tasks/{task_id}/heartbeat |
Renew the caller's lease |
| POST | /tasks/{task_id}/result |
Record a completed result (idempotent) |
| POST | /tasks/{task_id}/failure |
Record failure / retryable state |
| GET | /jobs/{job_id} |
Aggregate job progress |
| GET | /health |
Liveness (unauthenticated) |
Poking the API
Two ways, both checked in:
make smoke # every endpoint, asserted; non-zero exit on failure
api/requests.http runs the same calls one at a time from an editor with a REST
client (VSCodium/VS Code "REST Client", JetBrains HTTP Client). Later requests
reuse ids captured from earlier responses, so it doubles as API documentation.
Status
The queue works end to end: a job can be submitted, split into tasks, leased to workers one at a time, heartbeated, completed, and reflected in job progress.
Roadmap:
- schema + migrations ✅
ClaimNext,InsertBatch— atomic claim viaFOR UPDATE SKIP LOCKED✅GetForUpdate,Update,CountByStatus— result/failure paths ✅- file upload / chunk download — next
ExpireLeases✅ (reaper + a sweep before every claim)- stitcher: merge per-chunk top-k into the final CSV
- more integration coverage as features land
Still stubbed: StitchJob.Execute, and there is no POST /upload or
GET /download_chunk yet — so chunk files must be referenced by URI for now.
Tests
internal/domain is covered by unit tests that need no database — lease
ownership, stale attempts, idempotent replays, retry budgets, and expiry are all
pure functions of entity state:
go test ./...
go vet ./...
Integration tests (concurrent claiming, migrations) come in phase 7 and require
a real PostgreSQL instance supplied through TEST_DATABASE_URL.