feat: gamemaster
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
---
|
||||
stage: 08
|
||||
title: Module skeleton
|
||||
---
|
||||
|
||||
# Stage 08 — GM module skeleton
|
||||
|
||||
This decision record captures the wiring choices made when bootstrapping
|
||||
the runnable `gamemaster` binary on top of the contracts and freeze
|
||||
tests landed by Stages 01–07.
|
||||
|
||||
## Context
|
||||
|
||||
[`../PLAN.md` Stage 08](../PLAN.md) calls for a buildable `gamemaster`
|
||||
process that loads its environment-driven configuration, opens
|
||||
PostgreSQL and Redis pools, installs the OpenTelemetry runtime, exposes
|
||||
`/healthz` and `/readyz` on the trusted internal HTTP listener, and
|
||||
exits cleanly on `SIGTERM` within `GAMEMASTER_SHUTDOWN_TIMEOUT`. No
|
||||
business endpoints, no workers, and no persistence stores yet.
|
||||
|
||||
The reference implementation is `rtmanager`, the most recently landed
|
||||
Galaxy service that follows the platform-wide skeleton conventions
|
||||
(layered `cmd / internal/{app, api, config, logging, telemetry}`,
|
||||
`app.Component` lifecycle, OpenTelemetry runtime with deferred
|
||||
observable gauges, fail-fast environment loader). Stage 08 mirrors that
|
||||
skeleton with two deliberate divergences described below.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. `go.mod` scope is minimal at Stage 08
|
||||
|
||||
Only modules actually imported by Stage 08 code land in
|
||||
[`../go.mod`](../go.mod):
|
||||
|
||||
- `galaxy/postgres`, `galaxy/redisconn`, `galaxy/notificationintent`
|
||||
(the last one was already present from Stage 07 freeze test);
|
||||
- the OpenTelemetry stack (`otel`, `metric`, `trace`, `sdk`,
|
||||
`sdk/metric`, OTLP exporters for traces and metrics over gRPC and
|
||||
HTTP, stdout exporters);
|
||||
- `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`;
|
||||
- `github.com/redis/go-redis/v9` (promoted from indirect to direct);
|
||||
- `github.com/jackc/pgx/v5` (transitive via `pkg/postgres`).
|
||||
|
||||
PLAN-listed modules that arrive with later consumers (`go-jet/jet/v2`,
|
||||
`pressly/goose/v3`, the testcontainers modules, `go.uber.org/mock`,
|
||||
`galaxy/cronutil`, `galaxy/error`, `galaxy/util`) are deliberately left
|
||||
out of Stage 08's `go.mod`. They join the module together with their
|
||||
first consumers in Stages 09 / 10 / 11 / 12.
|
||||
|
||||
Reasoning: keeping `go mod tidy` honest at every stage is cheaper than
|
||||
pre-declaring blank-import stubs. The PLAN's full list is the eventual
|
||||
shape of the module across the series, not a Stage 08 contract.
|
||||
|
||||
### 2. `ShutdownTimeout` lives at the top level of `Config`
|
||||
|
||||
The README §Configuration groups one variable —
|
||||
`GAMEMASTER_SHUTDOWN_TIMEOUT` — under a documentation group called
|
||||
"Lifecycle". The Go struct does not split that single field into a
|
||||
substruct: `Config.ShutdownTimeout` mirrors the
|
||||
`rtmanager.Config.ShutdownTimeout` shape so the two services stay
|
||||
isomorphic. The "Lifecycle" group remains a documentation grouping in
|
||||
[`../README.md`](../README.md) only.
|
||||
|
||||
### 3. Telemetry — counters and histograms now, observable gauges later
|
||||
|
||||
`internal/telemetry/runtime.go` registers every counter and histogram
|
||||
listed under [`../README.md` §Observability](../README.md) at process
|
||||
start (`buildRuntime`). The three observable gauges
|
||||
(`gamemaster.runtime_records_by_status`,
|
||||
`gamemaster.scheduler.due_games`, `gamemaster.engine_versions_total`)
|
||||
are declared up front but their callbacks are installed via a deferred
|
||||
`Runtime.RegisterGauges(deps)` call. The wiring layer at Stages 11 / 14
|
||||
/ 15 supplies the probes (per-status row count, due-now scheduler
|
||||
count, registered engine versions) once the persistence stores and the
|
||||
scheduler exist.
|
||||
|
||||
This matches the `rtmanager` pattern where
|
||||
`runtime_records_by_status` is registered through an analogous
|
||||
`RegisterGauges` plumbing.
|
||||
|
||||
### 4. PostgreSQL migrations are deferred to Stage 09
|
||||
|
||||
The README §Startup dependencies states "Embedded goose migrations
|
||||
apply synchronously before any listener opens." Stage 08 opens,
|
||||
instruments, and pings the PostgreSQL pool but **does not** call
|
||||
`postgres.RunMigrations`. The migrations package
|
||||
(`internal/adapters/postgres/migrations/`) is shipped by Stage 09; the
|
||||
runtime adds the one-line `RunMigrations` call at that stage.
|
||||
|
||||
Until then, the runtime is buildable, listener-ready, and serves
|
||||
`/healthz` + `/readyz` against a fresh PostgreSQL pool with no schema
|
||||
applied. This is acceptable because Stage 08 ships no business handlers
|
||||
and no workers; nothing reads or writes `gamemaster.*` tables yet.
|
||||
|
||||
### 5. Makefile mirrors `rtmanager`
|
||||
|
||||
[`../Makefile`](../Makefile) declares `jet`, `mocks`, `integration`
|
||||
targets identical in shape to `rtmanager/Makefile`. The `jet` target
|
||||
runs `go run ./cmd/jetgen`; the binary lands in Stage 09. The `mocks`
|
||||
target runs `go generate ./internal/ports/...
|
||||
./internal/api/internalhttp/handlers/...`; the `//go:generate`
|
||||
directives land in Stages 10 / 12 / 19. Both targets fail until their
|
||||
prerequisites land — accepted because Stage 08 does not require either
|
||||
to succeed; only `go build` and `go test ./gamemaster/...` matter.
|
||||
|
||||
### 6. No Docker dependency
|
||||
|
||||
`Game Master` is forbidden from importing the Docker SDK
|
||||
([`../README.md` §Non-Goals](../README.md)). The skeleton therefore
|
||||
drops the `newDockerClient` / `pingDocker` helpers from
|
||||
`internal/app/bootstrap.go` and the Docker-related fields from
|
||||
`internal/app/wiring.go`. The readiness probe pings PostgreSQL and
|
||||
Redis only.
|
||||
|
||||
## Files landed
|
||||
|
||||
- `cmd/gamemaster/main.go` — process entrypoint.
|
||||
- `internal/config/{config.go, env.go, validation.go, config_test.go}` —
|
||||
GAMEMASTER-prefixed env loader plus required-vars fail-fast.
|
||||
- `internal/logging/{logger.go, context.go}` — slog JSON-stdout logger
|
||||
with request id and span id helpers.
|
||||
- `internal/telemetry/{runtime.go, runtime_test.go}` — OpenTelemetry
|
||||
runtime, instruments listed in §Observability, deferred gauge
|
||||
plumbing.
|
||||
- `internal/api/internalhttp/{server.go, server_test.go}` — `/healthz`
|
||||
and `/readyz` listener with observability middleware.
|
||||
- `internal/app/{app.go, app_test.go, bootstrap.go, runtime.go,
|
||||
wiring.go}` — process lifecycle (component supervisor + reverse-order
|
||||
cleanup), Redis bootstrap helpers, minimal placeholder wiring.
|
||||
- `Makefile` — `jet`, `mocks`, `integration` target stubs.
|
||||
- Updated `go.mod` / `go.sum` with the dependencies and replace
|
||||
directives for `galaxy/postgres` and `galaxy/redisconn`.
|
||||
|
||||
## Verification
|
||||
|
||||
- `go build ./gamemaster/...` succeeds.
|
||||
- `go test ./gamemaster/...` passes (existing contract / freeze tests
|
||||
plus the four new test files).
|
||||
- Manual smoke against a local Postgres + Redis confirms:
|
||||
`/healthz` returns `200 ok`, `/readyz` returns `200 ready` while both
|
||||
dependencies respond, and `503 service_unavailable` once one of them
|
||||
is brought down.
|
||||
- `SIGTERM` ends the process within `GAMEMASTER_SHUTDOWN_TIMEOUT`,
|
||||
releasing PostgreSQL pool, Redis client, and telemetry providers in
|
||||
reverse construction order.
|
||||
Reference in New Issue
Block a user