7884fb1c5f
- 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.
207 lines
8.1 KiB
Markdown
207 lines
8.1 KiB
Markdown
# automc extensions
|
|
|
|
Soft fork of `itzg/mc-router` that adds Postgres-driven route management and an HTTP waker, without touching upstream behavior by default.
|
|
|
|
The `internal/automc` package is opt-in via env vars: with `AUTOMC_DSN` unset, the binary behaves exactly like upstream.
|
|
|
|
## Environment variables
|
|
|
|
| Var | Required | Purpose |
|
|
|---|---|---|
|
|
| `AUTOMC_DSN` | yes (to enable) | Postgres DSN, e.g. `postgres://user:pass@host:5432/automc?sslmode=disable`. When unset, automc is a no-op. |
|
|
| `AUTOMC_WAKER_URL` | no | Base URL of the waker control plane (server-manager). When set, stopped backends are auto-started on login. |
|
|
| `AUTOMC_WAKER_TOKEN` | no | Sent as `X-API-Key` header on every waker request. |
|
|
|
|
Recommended companion upstream flags:
|
|
|
|
- `--use-asleep-motd` / `--use-loading-motd` — supplies friendly MOTD to clients during the wake window. Already implemented upstream; automc does not duplicate this.
|
|
|
|
## Postgres schema
|
|
|
|
Apply this once to the database referenced by `AUTOMC_DSN`. mc-router only reads; the trigger is what tells it to re-read.
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
name TEXT PRIMARY KEY,
|
|
domain TEXT NOT NULL,
|
|
address TEXT NOT NULL,
|
|
state TEXT NOT NULL DEFAULT 'stopped',
|
|
UNIQUE(domain)
|
|
);
|
|
|
|
CREATE OR REPLACE FUNCTION automc_notify_routes_changed() RETURNS trigger AS $$
|
|
BEGIN
|
|
PERFORM pg_notify('automc_routes_changed', '');
|
|
RETURN NULL;
|
|
END;
|
|
$$ 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, enabled OR DELETE ON servers
|
|
FOR EACH ROW EXECUTE FUNCTION automc_notify_routes_changed();
|
|
```
|
|
|
|
The trigger fires on every mutation to a route-relevant column. mc-router holds a persistent `LISTEN automc_routes_changed` and re-runs `SELECT name, domain, address FROM servers WHERE domain != '' AND address != ''`, diffing against its in-memory map. Adds/removes/changes call `server.Routes.CreateMapping` and `DeleteMapping` directly — no file I/O.
|
|
|
|
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`).
|
|
|
|
The waker:
|
|
|
|
1. `POST {AUTOMC_WAKER_URL}/servers/{name}/start` — fire-and-forget start signal. `409 Conflict` is treated as success (already starting).
|
|
2. Polls `GET {AUTOMC_WAKER_URL}/servers/{name}` every 2 s, expecting JSON `{"state":"running","address":"host:port"}`.
|
|
3. Returns the polled `address` once `state == "running"`, or errors after 90 s.
|
|
|
|
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
|
|
|
|
```
|
|
make sync-upstream
|
|
```
|
|
|
|
Fetches `upstream/main`, rebases the `automc` branch onto it, builds, and runs the automc tests. The patch surface is intentionally tiny so rebase conflicts are rare:
|
|
|
|
```
|
|
cmd/mc-router/main.go — 1 import line + 4-line Wire call
|
|
internal/automc/ — new directory; no upstream conflicts possible
|
|
docs/AUTOMC.md — new doc; no upstream conflicts
|
|
Makefile — appended targets only
|
|
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
|
|
|
|
```
|
|
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.
|