Skip to content

feat(servers): Phase 3 cutover (single listener, datastore type-swap, Keycloak removal)#206

Merged
SyniRon merged 8 commits into
developfrom
feat/issue-134-single-listener-cutover
Jun 20, 2026
Merged

feat(servers): Phase 3 cutover (single listener, datastore type-swap, Keycloak removal)#206
SyniRon merged 8 commits into
developfrom
feat/issue-134-single-listener-cutover

Conversation

@SyniRon

@SyniRon SyniRon commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Implements #134, the Phase 3 cutover of PRD #112 ("Goodbye gRPC"). This is one revertible change: the public surface moves onto the new net/http stack, the datastore returns the hand-written types instead of generated proto, and the gRPC server and grpc-gateway are removed.

What changed

Single listener. One public listener on :11000 serves the API under /api and the Swagger UI plus specs everywhere else. The spec's info.version is stamped in-process now, which replaces the CI proto-sed step. Prometheus moves to an internal-only :9090 listener. The gRPC server on :10000 and the grpc-gateway are gone.

Datastore type-swap. The Datastore interface, mysql.go, and tickets.go return types.* instead of proto.*, and the per-handler proto-to-wire mapping is deleted. A new types.RankType carries the rank-name table so RankShort stays byte-identical. The vestigial xf_user_connected_account JOIN is dropped from the roster, profile, position, s1, and gamertag queries; it stays only on the Discord lookup, where the WHERE filters its columns.

Keycloak removed. The lookup route now 404s, keycloakId is gone from both profile shapes, and the datastore method is deleted.

Old stack deleted. servers/grpc, servers/gateway, and servers/sentry.go are removed. The Sentry boot-lifecycle (dial-check, startup probe, SIGTERM flush) was ported into rest/. proto/, buf, and cmd/example.go stay for Phase 4 (#135); only cmd/example.go still imports proto.

Verification

  • go build, go vet, and the full test suite pass; buf lint and breaking-vs-develop pass.
  • The golden contract corpus replays against the new stack and against a real MariaDB. Two-way spec coverage passes.
  • Reviewed at the checkpoint by the pr-review-toolkit agents. Findings are fixed: the metrics listener is best-effort (a failure there no longer takes the public API down), the Sentry boot wiring is pinned with an AST guard, the public router is extracted and tested, and the post-cutover comment rot is swept.
  • Smoke-tested locally against a XenForo dev DB with the maintainer's Insomnia suite. Green.

Deploy notes

  • API-only, no coupled schema. The indexes landed in Phase 1 and Keycloak removal is read-side only, so this is a single revertible deploy.
  • The public port stays :11000, so the nginx upstream is unchanged. Confirm the internal :9090 metrics port is not published by compose or routed by nginx.
  • Pre-stage 7cav/api:rollback before publishing the release. Watchtower auto-rotates the container on publish.

Known follow-up (not blocking)

The served docs still render the generated swagger, which still documents the Keycloak route. The route itself 404s and the fields are absent from responses; only the docs lag. The generated specs are deleted in Phase 4 (#135), when the hand-written openapi.yaml becomes the served spec.

After deploy

Close #94 (gateway dial deprecations, superseded) and #100 (Keycloak removal, delivered).

Stats: 58 files, +1,522 / -4,367.

This was generated by AI

SyniRon added 8 commits June 20, 2026 10:07
Port the RANK_TYPE_* name table from proto so the datastore can derive
RankShort without importing proto. The gap at 24/25 and the decimal fallback
for uncataloged ids are kept, so RankShort stays byte-identical after the swap
to types. RankShort() does the prefix strip the datastore does today at four
call sites.

> *This was generated by AI*
The Datastore interface and the Mysql implementation now build types.* wire
structs directly. The rest handlers drop their *FromProto mappers and consume
the datastore results as-is.

- Delete FindProfileByKeycloakID and extractKeycloakID. The new profile types
  carry no keycloak field, so nothing populates one.
- Remove the vestigial xenforo.ConnectedAccountJoin from the roster, profile,
  position, s1-uniforms, and gamertag queries. It stays on FindProfileByDiscordID,
  whose WHERE filters the connected-account columns. DiscordId still populates
  from the Preload(clause.Associations) load, so the roster goldens hold.
- Derive RankShort through types.RankType, byte-identical to the old proto path.
- Move the ticket allocation discipline into the datastore: participants and
  categoryAncestorIds serialize as [] when empty, customFields as {}, matching
  the frozen tickets goldens.

The two xf_post aggregation SQL consts are untouched.

> *This was generated by AI*
Convert the rest fakes, the contract recording fake, and the datastore harness
tests to build types.* so they satisfy the swapped Datastore interface. Drop
the keycloak seed fields and the keycloak fake method along with them.

Retarget the contract golden harness at rest.New: the gRPC server and the
grpc-gateway are gone, so the corpus now replays directly against the stdlib
net/http stack instead of dialing a gRPC server behind a gateway. The same
goldens hold.

Guard the roster handlers so a nil Profiles map still serializes as
{"profiles":{}}, never null. The real datastore always allocates, but a nil map
slipping through used to be caught by the old per-handler mapper; the guard
keeps the frozen empty-result form.

> *This was generated by AI*
Bring the three boot-lifecycle pieces from servers/sentry.go into the rest
package so they survive the deletion of the old stack: sentryDialCheck (warn-
only DSN reachability check), sentryStartupProbe (one canary flushed at boot),
and FlushSentryOnShutdown (the SIGTERM flush handler). SetupSentry now drives
the dial-check and probe, matching what servers.Start did. The probe-stall and
dial-check tests come along, adapted to the rest seam (sentryTransport).

Add DocsHandler: it serves the embedded Swagger UI and OpenAPI specs at the
same URLs the old gateway used, and stamps the spec's info.version from the
build-time version. That replaces the release workflow's proto-sed step; a dev
build leaves the sentinel untouched.

> *This was generated by AI*
Rebuild servers.Start around one public listener and one internal metrics
listener. The public :11000 listener serves rest.New(ds, rc) under /api and
DocsHandler everywhere else, at the same URLs as before. The metrics handler
binds on its own :9090 listener. The :10000 gRPC listener and the grpc-gateway
translation layer are gone.

InitTrustedProxies stays first, fatal on error (ADR 0005). The reference cache
still warms before serving (New + Refresh + the refresh goroutine), since
rest.New panics on a cold cache. SetupSentry(version) replaces the old Phase 0
setupSentry, and FlushSentryOnShutdown installs only when capture is enabled.

Delete servers/grpc and servers/gateway whole, plus servers/sentry.go and its
test. PORT was only the gateway's gRPC dial target, so servers.New takes no
address and serve.go no longer reads it. cmd/example.go is the only remaining
proto importer, left for Phase 4.

> *This was generated by AI*
Review follow-up on the #134 cutover.

- The internal-only metrics listener no longer takes the public API down. A bind
  failure now logs and serves without metrics, and a runtime Serve error is
  logged instead of calling Error.Fatalf. The public listener stays fatal since
  it is the service. The old dual stack made every listener fatal because both
  served traffic, which the metrics listener does not.
- Extract buildPublicRouter from servPublic so the /api-vs-docs split is
  unit-testable without opening a listener.
- Pin the Sentry boot wiring so FlushSentryOnShutdown is installed only inside
  the SetupSentry gate, using the same AST guard idiom as the trusted-proxy
  wiring. Those functions just changed packages, which is exactly when a
  refactor drops the call.

> *This was generated by AI*
Match the sibling enums (rostertype and recordtype inline FormatInt/TrimPrefix)
and point the tests at the public String()/RankShort() methods instead of the
removed private helpers. Behavior is unchanged and RankShort stays
byte-identical.

> *This was generated by AI*
The cutover deleted servers/grpc, servers/gateway, and servers/sentry.go but
left comments describing the old dual-stack world. Fix the load-bearing ones:
a dangling reference to the deleted servers/gateway/gateway.go (rest/sentry.go),
the rest package doc (it is the live single listener now, not test-mounted), the
"legacy gateway reuses this until cutover" cluster across auth/gzip/metrics, and
the metrics deploy-check (public /metrics returns 404 now, not 401). Also two
test comments naming removed symbols. Comments only, no behavior change.

> *This was generated by AI*
@SyniRon
SyniRon merged commit f2cf00d into develop Jun 20, 2026
3 checks passed
@SyniRon
SyniRon deleted the feat/issue-134-single-listener-cutover branch June 20, 2026 15:29
SyniRon added a commit that referenced this pull request Jun 20, 2026
… second port (#135) (#207)

Phase 3 (#134/#206) collapsed serving to a single HTTP listener, leaving the
proto/buf machinery referenced only by generated code and the gRPC example
client. This removes all of it.

Deleted surfaces:
- proto/ in full: milpacs.proto, tickets.proto, every generated .pb.go,
  .pb.gw.go, and _grpc.pb.go, plus the proto-coupled deprecation_test.go.
- buf config: buf.gen.yaml, buf.lock, buf.yaml, and the codegen tool pins in
  tools.go.
- cmd/example.go, the only non-generated proto consumer (the gRPC sample
  client). serve still works; root's description no longer claims gRPC.
- Generated Swagger 2.0 assets (milpacs.swagger.json, tickets.swagger.json)
  and openapi/openapi_test.go, which asserted the 2.0 keycloak surface.

Docs serving now points at the 3.1 spec. Per the #117 ruling (no consumer
codegens from the served spec), the hand-owned OpenAPI 3.1 document
(openapi/openapi.yaml) replaces the retired 2.0 files at the docs URL.
DocsHandler serves it at /openapi.yaml with build-version stamping, and the
Swagger UI shell loads it. The golden corpus already validates this document,
so the public contract stays covered.

Build, CI, and deploy:
- Makefile and magefile drop install/generate and the buf lint/breaking
  targets. make lint is now go vet.
- go.yml drops the buf install and make generate steps; the lint job runs go
  vet.
- build_and_push.yml drops the release-time proto-version sed. The version
  reaches the spec at runtime via the build-time version var.
- Dockerfile drops make install, buf install, and make generate, and stops
  exposing the gRPC port. docker-compose and the nginx template drop the
  :10000 second port and its grpc_pass block, so there is one listener in code
  and in config.

go mod tidy shrinks the build graph from 209 to 90 modules (about 119 removed),
in line with the PRD's ~100-module estimate. grpc, grpc-gateway, buf, and
protoc are gone from the graph entirely.

The throwaway request-log analytics and the duration= obligation were already
retired with the old gateway in Phase 3, so there is nothing to remove here.

*This was generated by AI*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace deprecated grpc.DialContext / WithBlock / WithInsecure in the gateway

1 participant