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
@@ -0,0 +1,65 @@
package postgres
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
)
// WorkerRepo implements usecase.WorkerRepository.
type WorkerRepo struct {
pool *pgxpool.Pool
}
func NewWorkerRepo(pool *pgxpool.Pool) *WorkerRepo {
return &WorkerRepo{pool: pool}
}
const workerColumns = `id, name, capabilities, status, last_heartbeat_at, created_at, updated_at`
const insertWorkerSQL = `
INSERT INTO workers (` + workerColumns + `)
VALUES ($1, $2, $3, $4, $5, $6, $7)`
func (r *WorkerRepo) Insert(ctx context.Context, w *domain.Worker) error {
// capabilities is a jsonb column; pgx marshals the []string to a JSON array.
_, err := conn(ctx, r.pool).Exec(ctx, insertWorkerSQL,
w.ID, w.Name, w.Capabilities, string(w.Status),
w.LastHeartbeatAt, w.CreatedAt, w.UpdatedAt)
if err != nil {
return fmt.Errorf("insert worker: %w", err)
}
return nil
}
const getWorkerSQL = `SELECT ` + workerColumns + ` FROM workers WHERE id = $1`
func (r *WorkerRepo) Get(ctx context.Context, id uuid.UUID) (*domain.Worker, error) {
w, err := scanWorker(conn(ctx, r.pool).QueryRow(ctx, getWorkerSQL, id))
if errors.Is(err, pgx.ErrNoRows) {
return nil, domain.ErrWorkerNotFound
}
if err != nil {
return nil, fmt.Errorf("get worker: %w", err)
}
return w, nil
}
func scanWorker(row pgx.Row) (*domain.Worker, error) {
var (
w domain.Worker
status string
)
if err := row.Scan(&w.ID, &w.Name, &w.Capabilities, &status,
&w.LastHeartbeatAt, &w.CreatedAt, &w.UpdatedAt); err != nil {
return nil, err
}
w.Status = domain.WorkerStatus(status)
return &w, nil
}