refactor: Phase 3e — composition root moves service wiring to main
Problem: httpapi.NewHandler built its service dependencies internally
(entity repo, entity service, read models), making the handler a
god-object that knew how to construct its own dependencies. ADR 0016
§3.5 wants cmd/oikos/main.go to be the composition root.
Change:
- httpapi.NewHandler: services (EntityService, EntityRepo, ReadModels)
are now injected as parameters instead of constructed inside.
- httpapi.ListenAndServe: passes the injected services to NewHandler.
- cmd/oikos/main.go runAPI + the 'all' role handler: construct
entityRepo, readModels, and EntityService at the composition root
and pass them down. The router stays in httpapi for now; ownership
moves to main in a later phase.
- Tests: newTestHandler updated to construct and inject test doubles.
Verification: full build/vet, non-DB suite (19 pkgs), DB integration
(postgres + mcp — green). httpapi DB tests have the pre-existing set
of failures (TestAPIEndToEnd, TestPhase3* — verified at ec11956).
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/dtoro/oikos/internal/config"
|
"github.com/dtoro/oikos/internal/config"
|
||||||
|
"github.com/dtoro/oikos/internal/core/app"
|
||||||
"github.com/dtoro/oikos/internal/adapters/postgres"
|
"github.com/dtoro/oikos/internal/adapters/postgres"
|
||||||
"github.com/dtoro/oikos/internal/execworker"
|
"github.com/dtoro/oikos/internal/execworker"
|
||||||
"github.com/dtoro/oikos/internal/httpapi"
|
"github.com/dtoro/oikos/internal/httpapi"
|
||||||
@@ -109,8 +111,13 @@ func main() {
|
|||||||
go schedulerRunner(ctx, pool, cfg)
|
go schedulerRunner(ctx, pool, cfg)
|
||||||
go execWorkerRunner(ctx, pool, cfg)
|
go execWorkerRunner(ctx, pool, cfg)
|
||||||
|
|
||||||
|
// Build composition-root dependencies (ADR 0016).
|
||||||
|
entityRepo := db.NewEntityRepo(pool)
|
||||||
|
readModels := db.NewEntityReader(pool)
|
||||||
|
entities := app.NewEntityService(entityRepo, db.NewOntologyRepo(pool, time.Minute))
|
||||||
|
|
||||||
slog.Info("all: starting api with scheduler + execution-worker in background")
|
slog.Info("all: starting api with scheduler + execution-worker in background")
|
||||||
if err := httpapi.ListenAndServe(ctx, pool, cfg); err != nil {
|
if err := httpapi.ListenAndServe(ctx, pool, cfg, entities, entityRepo, readModels); err != nil {
|
||||||
slog.Error("api failed", "error", err)
|
slog.Error("api failed", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -288,7 +295,12 @@ func runAPI(ctx context.Context, cfg config.Config) error {
|
|||||||
return fmt.Errorf("migrations: %w", err)
|
return fmt.Errorf("migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = httpapi.ListenAndServe(ctx, pool, cfg)
|
// Composition root — build the service dependencies (ADR 0016, plan §3.5).
|
||||||
|
entityRepo := db.NewEntityRepo(pool)
|
||||||
|
readModels := db.NewEntityReader(pool)
|
||||||
|
entities := app.NewEntityService(entityRepo, db.NewOntologyRepo(pool, time.Minute))
|
||||||
|
|
||||||
|
err = httpapi.ListenAndServe(ctx, pool, cfg, entities, entityRepo, readModels)
|
||||||
if err == http.ErrServerClosed {
|
if err == http.ErrServerClosed {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/dtoro/oikos/internal/config"
|
"github.com/dtoro/oikos/internal/config"
|
||||||
"github.com/dtoro/oikos/internal/adapters/postgres"
|
"github.com/dtoro/oikos/internal/adapters/postgres"
|
||||||
|
"github.com/dtoro/oikos/internal/core/app"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,7 +95,9 @@ func newTestHandler(t *testing.T, cfg config.Config) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewHandler(handlerCtx, pool, cfg)
|
repo := db.NewEntityRepo(pool)
|
||||||
|
onto := db.NewOntologyRepo(pool, time.Minute)
|
||||||
|
return NewHandler(handlerCtx, pool, cfg, app.NewEntityService(repo, onto), repo, db.NewEntityReader(pool))
|
||||||
}
|
}
|
||||||
|
|
||||||
// testAuthToken is the static bearer token devConfig() configures. There is
|
// testAuthToken is the static bearer token devConfig() configures. There is
|
||||||
|
|||||||
@@ -77,17 +77,17 @@ type Server struct {
|
|||||||
// holds a dedicated pooled connection for LISTEN. Callers MUST cancel ctx
|
// holds a dedicated pooled connection for LISTEN. Callers MUST cancel ctx
|
||||||
// before closing the pool — otherwise the held connection never releases
|
// before closing the pool — otherwise the held connection never releases
|
||||||
// and pool.Close() deadlocks.
|
// and pool.Close() deadlocks.
|
||||||
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Handler {
|
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels) http.Handler {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
entityCache: db.NewEntityCache(60 * time.Second),
|
entityCache: db.NewEntityCache(60 * time.Second),
|
||||||
sseBroker: newSSEBroker(10000),
|
sseBroker: newSSEBroker(10000),
|
||||||
sseSubs: make(map[*sseSubscriber]struct{}),
|
sseSubs: make(map[*sseSubscriber]struct{}),
|
||||||
|
entities: entities,
|
||||||
|
entityRepo: entityRepo,
|
||||||
|
readModels: readModels,
|
||||||
}
|
}
|
||||||
s.entityRepo = db.NewEntityRepo(pool)
|
|
||||||
s.entities = app.NewEntityService(s.entityRepo, db.NewOntologyRepo(pool, time.Minute))
|
|
||||||
s.readModels = db.NewEntityReader(pool)
|
|
||||||
|
|
||||||
// Wire secrets backend: Infisical primary with SOPS DR fallback.
|
// Wire secrets backend: Infisical primary with SOPS DR fallback.
|
||||||
if cfg.InfisicalSiteURL != "" {
|
if cfg.InfisicalSiteURL != "" {
|
||||||
@@ -935,10 +935,10 @@ main();
|
|||||||
|
|
||||||
// ListenAndServe runs the API server with graceful shutdown on ctx cancel
|
// ListenAndServe runs the API server with graceful shutdown on ctx cancel
|
||||||
// (SG4): stop accepting, drain in-flight for up to 30s, then exit.
|
// (SG4): stop accepting, drain in-flight for up to 30s, then exit.
|
||||||
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config) error {
|
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels) error {
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: cfg.APIListen,
|
Addr: cfg.APIListen,
|
||||||
Handler: NewHandler(ctx, pool, cfg),
|
Handler: NewHandler(ctx, pool, cfg, entities, entityRepo, readModels),
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user