Adds the SciMesh coordinator: a durable task-queue server on PostgreSQL that owns all database access, with workers reaching it over HTTP only. Structured as a modular monolith following Clean Architecture: domain entities and their invariants, no I/O usecase business operations + repository/clock ports transport HTTP handlers, DTOs, auth, error mapping storage PostgreSQL repositories, transactions carried in context infra config, pool, clock, server, lease reaper Dependencies point strictly inward; domain imports nothing from the module. Working: layer wiring, routing, shared-token auth, access logging, request IDs, domain-error to status-code mapping, transactional boundaries, graceful shutdown (HTTP drain -> reaper stop -> pool close), migrations, and a Compose stack starting Postgres -> migrations -> coordinator. The domain is complete and covered by unit tests that need no database: lease ownership, stale attempts, idempotent result replay, retry budgets, and lease expiry. Repository methods are stubs returning ErrNotImplemented (HTTP 501). The SQL for atomic claiming (FOR UPDATE SKIP LOCKED) and for lease expiry is written and ready to wire up. See coordinator/ARCHITECTURE.md for the layer map and a request traced through every layer.
73 lines
1.9 KiB
Go
73 lines
1.9 KiB
Go
// Server: the HTTP listener and the background lease reaper, both shut down
|
|
// cleanly on a signal.
|
|
package infra
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/usecase"
|
|
)
|
|
|
|
const shutdownGrace = 15 * time.Second
|
|
|
|
// Run serves handler until ctx is cancelled, then drains in-flight requests.
|
|
func RunServer(ctx context.Context, log *slog.Logger, addr string, handler http.Handler) error {
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: handler,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
|
|
// Buffered so this goroutine can exit even when nobody reads the channel
|
|
// (the ctx.Done branch below) — an unbuffered send would leak it forever.
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
log.Info("coordinator listening", "addr", addr)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
return err
|
|
case <-ctx.Done():
|
|
log.Info("shutdown signal received")
|
|
}
|
|
|
|
// A fresh context: ctx is already cancelled, and reusing it would abort the
|
|
// very requests we are trying to let finish.
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGrace)
|
|
defer cancel()
|
|
return srv.Shutdown(shutdownCtx)
|
|
}
|
|
|
|
// RunReaper periodically reclaims tasks whose lease elapsed, so a worker that
|
|
// died without a heartbeat cannot strand its task in 'leased' forever.
|
|
func RunReaper(ctx context.Context, log *slog.Logger, uc *usecase.ExpireLeases, interval time.Duration) {
|
|
t := time.NewTicker(interval)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
n, err := uc.Execute(ctx)
|
|
if err != nil {
|
|
// Demoted to debug while the repository is still a stub;
|
|
// raise to Warn once phase 5 lands.
|
|
log.Debug("reaper skipped", "err", err)
|
|
continue
|
|
}
|
|
if n > 0 {
|
|
log.Info("reaper requeued expired leases", "count", n)
|
|
}
|
|
}
|
|
}
|
|
}
|