feat(coordinator): worker registry, API contract, logging & DB retry

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.
This commit is contained in:
Efremenko Arhip
2026-07-23 13:45:31 +03:00
parent 5d6390fd98
commit dc92121acc
25 changed files with 635 additions and 66 deletions
+1
View File
@@ -10,6 +10,7 @@ import "errors"
var (
ErrJobNotFound = errors.New("job not found")
ErrTaskNotFound = errors.New("task not found")
ErrWorkerNotFound = errors.New("worker not found")
ErrLeaseConflict = errors.New("task leased to another worker")
ErrStaleAttempt = errors.New("attempt does not match lease")
ErrResultConflict = errors.New("different result already recorded")
+45
View File
@@ -0,0 +1,45 @@
package domain
import (
"time"
"github.com/google/uuid"
)
type WorkerStatus string
const (
WorkerOnline WorkerStatus = "online"
WorkerBusy WorkerStatus = "busy"
WorkerOffline WorkerStatus = "offline"
)
// Worker is a registered process/machine allowed to claim tasks. Its
// capabilities are the allowlisted workload names it can run; the coordinator
// never hands it a task outside that set.
type Worker struct {
ID uuid.UUID
Name string
Capabilities []string
Status WorkerStatus
LastHeartbeatAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// NewWorker registers a worker. A worker with no capabilities could never be
// handed a task, so an empty set is rejected rather than silently stored.
func NewWorker(name string, capabilities []string, now time.Time) (*Worker, error) {
if len(capabilities) == 0 {
return nil, ErrInvalidInput
}
return &Worker{
ID: uuid.New(),
Name: name,
Capabilities: capabilities,
Status: WorkerOnline,
LastHeartbeatAt: now,
CreatedAt: now,
UpdatedAt: now,
}, nil
}