automc: schema-fit query + CI + FORK doc
- pgsync.go: filter rows where enabled IS NOT FALSE and domain IS NOT NULL, matching the existing automc servers table (domain nullable, enabled defaults true). Trigger doc now includes UPDATE OF enabled. - .gitea/workflows/ci.yaml: build/test on push to automc branch + tags, publish container as git.timemachine.center/timemachine/mc-router:automc. - FORK.md: soft-fork relationship doc at repo root. - docs/AUTOMC.md: quick-start recipe, troubleshooting table, sync-lifecycle and waker-dispatch sequence diagrams.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [automc]
|
||||
tags: ["automc-v*"]
|
||||
pull_request:
|
||||
branches: [automc]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
- name: Test automc package
|
||||
run: go test ./internal/automc/...
|
||||
- name: Test full suite
|
||||
run: go test ./...
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: validate
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
run: |
|
||||
if [[ "$GITEA_REF" == refs/tags/automc-v* ]]; then
|
||||
echo "tag=${GITEA_REF#refs/tags/}" >> "$GITEA_OUTPUT"
|
||||
else
|
||||
echo "tag=automc" >> "$GITEA_OUTPUT"
|
||||
fi
|
||||
echo "sha=sha-$(echo "$GITEA_SHA" | cut -c1-7)" >> "$GITEA_OUTPUT"
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.timemachine.center -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
IMG=git.timemachine.center/timemachine/${{ gitea.event.repository.name }}
|
||||
docker build -t "$IMG:${{ steps.meta.outputs.tag }}" -t "$IMG:${{ steps.meta.outputs.sha }}" .
|
||||
docker push "$IMG:${{ steps.meta.outputs.tag }}"
|
||||
docker push "$IMG:${{ steps.meta.outputs.sha }}"
|
||||
@@ -0,0 +1,51 @@
|
||||
# Fork status — Timemachine/mc-router
|
||||
|
||||
This is a **soft fork** of [`itzg/mc-router`](https://github.com/itzg/mc-router) maintained for the [automc](https://git.timemachine.center/Timemachine/automc) Minecraft management platform.
|
||||
|
||||
## Branch model
|
||||
|
||||
| Branch | Tracks | Contents |
|
||||
|---|---|---|
|
||||
| `main` | `upstream/main` (github.com/itzg/mc-router) | Verbatim mirror. **Do not commit here.** |
|
||||
| `automc` | branched from `v1.42.1` | Soft fork; carries the automc-specific patch. |
|
||||
|
||||
## Patch surface
|
||||
|
||||
Minimal — designed for low-friction rebases.
|
||||
|
||||
| Path | Type | Why |
|
||||
|---|---|---|
|
||||
| `cmd/mc-router/main.go` | edit | 1 import line + 4-line `automc.Wire(ctx)` call. Only upstream file modified. |
|
||||
| `internal/automc/` | new dir | Self-contained extension package — no upstream conflicts possible. |
|
||||
| `docs/AUTOMC.md` | new | Operator doc for the extensions. See [`docs/AUTOMC.md`](docs/AUTOMC.md). |
|
||||
| `Makefile` | edit | Appended `sync-upstream` + `automc-build` targets. |
|
||||
| `go.mod` / `go.sum` | edit | Added `github.com/jackc/pgx/v5 v5.8.0` for LISTEN/NOTIFY. |
|
||||
| `FORK.md` | new | This file. |
|
||||
|
||||
Everything else stays untouched. No edits to `server/`, `mcproto/`, `cmd/mc-router/` beyond the wire-call.
|
||||
|
||||
## What the fork adds
|
||||
|
||||
1. **Postgres LISTEN/NOTIFY route source** — drop-in alternative to `--routes-config`, `--api-binding`, K8s, and Docker route sources. Pulled in via `AUTOMC_DSN` env var.
|
||||
2. **HTTP waker integration** — registers a `WakerFunc` per route that POSTs to a control-plane (`AUTOMC_WAKER_URL`) and polls until `state=running`.
|
||||
|
||||
Both are opt-in. With `AUTOMC_DSN` unset, the binary behaves exactly like upstream `itzg/mc-router`.
|
||||
|
||||
See [`docs/AUTOMC.md`](docs/AUTOMC.md) for env vars, schema, troubleshooting.
|
||||
|
||||
## Sync from upstream
|
||||
|
||||
```
|
||||
make sync-upstream
|
||||
```
|
||||
|
||||
Rebases `automc` onto `upstream/main`, runs `go build ./...` and `go test ./internal/automc/...`. If `server.Routes.CreateMapping` signature changes, only [`internal/automc/pgsync.go`](internal/automc/pgsync.go) needs adjustment.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
| Where it goes | What |
|
||||
|---|---|
|
||||
| [`Timemachine/mc-router`](https://git.timemachine.center/Timemachine/mc-router/issues) | automc-specific bugs (pg sync, waker, fork mechanics) |
|
||||
| [`itzg/mc-router`](https://github.com/itzg/mc-router/issues) | Upstream core bugs (routing, mcproto, REST API) |
|
||||
|
||||
If unsure, file upstream first — most reports turn out to be upstream-side.
|
||||
+122
-1
@@ -38,7 +38,7 @@ $$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS automc_servers_route_notify ON servers;
|
||||
CREATE TRIGGER automc_servers_route_notify
|
||||
AFTER INSERT OR UPDATE OF domain, address, state OR DELETE ON servers
|
||||
AFTER INSERT OR UPDATE OF domain, address, state, enabled OR DELETE ON servers
|
||||
FOR EACH ROW EXECUTE FUNCTION automc_notify_routes_changed();
|
||||
```
|
||||
|
||||
@@ -46,6 +46,47 @@ The trigger fires on every mutation to a route-relevant column. mc-router holds
|
||||
|
||||
State column is not read by mc-router; it exists to drive the trigger and for the waker's own ready-check.
|
||||
|
||||
### Sync lifecycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Wire as automc.Wire
|
||||
participant Sync as syncer
|
||||
participant PG as Postgres
|
||||
participant R as server.Routes
|
||||
|
||||
Wire->>Sync: newSyncer(dsn, waker)
|
||||
Wire-)Sync: go run(ctx)
|
||||
Note over Sync: backoff = 1s
|
||||
|
||||
loop reconnect (until ctx cancelled)
|
||||
Sync->>PG: pgx.Connect(dsn)
|
||||
alt connect ok
|
||||
Sync->>PG: LISTEN automc_routes_changed
|
||||
Sync->>PG: SELECT name, domain, address FROM servers
|
||||
PG-->>Sync: initial rows
|
||||
Sync->>Sync: diff vs current (empty on first run)
|
||||
loop for each add
|
||||
Sync->>R: CreateMapping(host, addr, waker)
|
||||
end
|
||||
loop for each del
|
||||
Sync->>R: DeleteMapping(host)
|
||||
end
|
||||
Note over Sync: backoff reset
|
||||
|
||||
loop while connection healthy
|
||||
Sync->>PG: WaitForNotification(ctx)
|
||||
PG-->>Sync: NOTIFY automc_routes_changed
|
||||
Sync->>PG: SELECT all servers
|
||||
PG-->>Sync: fresh rows
|
||||
Sync->>R: CreateMapping / DeleteMapping (diff only)
|
||||
end
|
||||
else connect or notify error
|
||||
Note over Sync: warn log<br/>sleep backoff<br/>backoff = min(backoff*2, 30s)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Waker contract
|
||||
|
||||
When `AUTOMC_WAKER_URL` is set, every route is registered with a `WakerFunc` that the upstream connector calls only when a client tries to LOGIN (not on status pings — those are answered locally via `--use-asleep-motd`).
|
||||
@@ -58,6 +99,47 @@ The waker:
|
||||
|
||||
The polled address overrides the route's static address for that connection only — useful when the backend's IP is allocated lazily.
|
||||
|
||||
### Waker dispatch on stopped backend
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Client as MC Client
|
||||
participant MR as mc-router
|
||||
participant SM as server-manager (waker URL)
|
||||
participant MC as MC backend
|
||||
|
||||
Client->>MR: TCP + Handshake (next_state=login)
|
||||
Note over MR: Routes.FindBackendForServerAddress<br/>returns backend + WakerFunc
|
||||
MR->>MC: dial backend address
|
||||
MC--xMR: connection refused
|
||||
Note over MR: WakerFunc != nil →<br/>invoke it before kicking client
|
||||
|
||||
MR->>SM: POST /servers/test1/start<br/>X-API-Key: ...
|
||||
alt 202 Accepted
|
||||
SM-->>MR: 202
|
||||
else 409 Conflict (already starting)
|
||||
SM-->>MR: 409 — treat as success
|
||||
else 5xx
|
||||
SM-->>MR: error → WakerFunc returns err
|
||||
MR->>Client: kick (connection refused)
|
||||
end
|
||||
|
||||
loop every 2s, up to 90s
|
||||
MR->>SM: GET /servers/test1
|
||||
alt running
|
||||
SM-->>MR: 200 state=running address=10.0.0.5:25565
|
||||
else still starting
|
||||
SM-->>MR: 200 state=starting
|
||||
Note over MR: continue polling
|
||||
end
|
||||
end
|
||||
|
||||
Note over MR: WakerFunc returns address
|
||||
MR->>MC: dial polled address
|
||||
MC-->>MR: ok
|
||||
MR->>Client: splice handshake-and-onward
|
||||
```
|
||||
|
||||
## Upstream sync
|
||||
|
||||
```
|
||||
@@ -76,6 +158,43 @@ go.mod / go.sum — pgx dep added; mergeable
|
||||
|
||||
If upstream renames `server.Routes.CreateMapping` or changes its signature, only `pgsync.go:apply` needs adjustment.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# 1. Start a postgres reachable from mc-router
|
||||
podman run -d --name automc-pg \
|
||||
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=automc \
|
||||
-p 127.0.0.1:5432:5432 docker.io/postgres:16-alpine
|
||||
|
||||
# 2. Apply the schema + trigger (see "Postgres schema" above)
|
||||
podman exec -i automc-pg psql -U postgres -d automc < schema.sql
|
||||
|
||||
# 3. Run mc-router with automc enabled
|
||||
AUTOMC_DSN="postgres://postgres:test@127.0.0.1:5432/automc?sslmode=disable" \
|
||||
AUTOMC_WAKER_URL="http://server-manager:8080" \
|
||||
AUTOMC_WAKER_TOKEN="$SM_API_KEY" \
|
||||
mc-router --port 25565 --api-binding 127.0.0.1:25590 --use-asleep-motd
|
||||
|
||||
# 4. Add a route
|
||||
podman exec automc-pg psql -U postgres -d automc -c \
|
||||
"INSERT INTO servers (name, domain, address, state) \
|
||||
VALUES ('test1', 'test1.example.com', '127.0.0.1:25001', 'running');"
|
||||
|
||||
# Log should show: automc route +: test1.example.com → 127.0.0.1:25001 (test1)
|
||||
# REST API should show it: curl http://127.0.0.1:25590/routes
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Check |
|
||||
|---|---|---|
|
||||
| Binary starts but no `automc: pg route sync started` log | `AUTOMC_DSN` empty or unset | `env \| grep AUTOMC_DSN` |
|
||||
| `automc pgsync disconnected; reconnecting in 1s` repeating | pg unreachable / wrong DSN | Test with `psql "$AUTOMC_DSN" -c "SELECT 1"` |
|
||||
| INSERT into `servers` doesn't fire a route | Trigger missing or not on the right columns | `\d+ servers` in psql — confirm `automc_servers_route_notify` trigger exists |
|
||||
| Routes appear in REST `/routes` but client connect times out | Backend address wrong / unreachable from mc-router | `podman exec mc-router nc -zv <address>` |
|
||||
| WakerFunc never called for stopped backends | `AUTOMC_WAKER_URL` empty | Set it; without it stopped backends get connection refused on login |
|
||||
| `waker timeout for X after 1m30s` | Backend takes longer than 90 s to come up | Tune `wakerPollTimeout` (currently a const in `waker.go:18`); planned env var |
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
@@ -83,3 +202,5 @@ go build ./...
|
||||
go test ./internal/automc/...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
End-to-end smoke recipe verified 2026-05-27 on local podman: INSERT/UPDATE/DELETE in `servers` table propagated to `/routes` REST API within ~1 s; postgres restart triggered exponential backoff reconnect (1 s → 30 s cap) and full route re-sync on reconnect.
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *syncer) connectAndLoop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *syncer) refresh(ctx context.Context, conn *pgx.Conn) error {
|
||||
rows, err := conn.Query(ctx, `SELECT name, domain, address FROM servers WHERE domain != '' AND address != ''`)
|
||||
rows, err := conn.Query(ctx, `SELECT name, domain, address FROM servers WHERE domain IS NOT NULL AND domain != '' AND address != '' AND enabled IS NOT FALSE`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user