Distributed mode & operations¶
Run the Control Plane and Data Plane as separate processes when you want config centralised and gateways at the edge (or multiple DPs). Concepts: Binaries & planes. First-time steps: First run — Path B. Auth: Authentication.
flowchart LR
pgctl[pgctl] -->|gRPC user token| CP[pgway-cp]
CP -->|BadgerDB| DB[(store)]
DP1[pgway-dp] -->|Register Heartbeat Watch| CP
DP2[pgway-dp] -->|Register Heartbeat Watch| CP
Client --> DP1
Client --> DP2
When to use it¶
| Mode | Use when |
|---|---|
All-in-one pgway |
Local dev, single host |
pgway-cp + pgway-dp |
Separate config host from traffic; multiple agents; clearer blast radius |
All-in-one still uses the same resource model and pgctl; it simply skips agent registration.
Topology¶
- CP listens on
grpc.listen_addr(and optional experimental REST). - Each DP dials that address, registers as an agent, heartbeats, and watches config.
- Entrypoints bind on the DP that received the config — clients hit the DP host:port, not the CP.
Bring-up checklist¶
# Terminal 1 — Control Plane
./build/pgway-cp --config ./cp.toml
# Terminal 2 — admin (once)
./build/pgctl init --bootstrap-token 'pgw_…'
./build/pgctl login --username admin
# Terminal 2 — agent registration token (once per new agent)
REG=$(./build/pgctl agent token create)
# Terminal 3 — Data Plane
PGWAY_AGENT_REGISTRATION_TOKEN="$REG" ./build/pgway-dp --config ./dp.toml
# Apply resources (served by DP after Watch/reload)
./build/pgctl apply -f stack.yaml
./build/pgctl agent list
Example configs¶
# cp.toml
log_level = "info"
[badger]
path = "./var/cp-lib"
[grpc]
listen_addr = ":9090"
[dashboard]
listen_addr = ":8081"
# dp.toml — grpc.listen_addr is the CP to dial
log_level = "info"
[grpc]
listen_addr = "127.0.0.1:9090"
[agent]
name = "edge-1"
labels = { zone = "edge" }
state_path = "./var/agent-edge-1.json"
heartbeat_interval = "10s"
Later DP starts reuse agent.state_path (no registration token).
Hot reload / Watch¶
- Applying or deleting resources on the CP emits change events.
- All-in-one: in-process consumer rebuilds entrypoints / balancers. A periodic Resync (default every
5m) also reloads cache + listeners so dropped in-process events heal without a restart (dataplane.event_resync_interval;0disables). - Distributed: agents subscribe over gRPC Watch. On each reconnect the DP runs a full Resync (Bootstrap + listener reconcile) so entrypoints added/removed while the CP was unreachable are healed.
Balancer internal state (RR cursor, least-bytes counters, weighted currents) resets when that balancer instance is rebuilt.
Control plane unreachable (pgway-dp)¶
Once the DP has bootstrapped and bound listeners, proxy traffic can continue on last-known config while the CP is down (fail-open by default — Envoy-style latch).
| Setting | Default | Meaning |
|---|---|---|
dataplane.cp_disconnect_strategy |
fail_open |
fail_open keeps serving stale config indefinitely; fail_closed rejects new requests with 503 (existing CONNECT tunnels are not cut). State transitions are logged (cp link state changed). |
dataplane.cp_disconnect_unreachable_threshold |
30s |
Both heartbeat and Watch proofs must be stale, then remain so for this long, before unreachable (worst case about 2× this value after CP death: wait for HB age-out, then this window) |
dataplane.cp_disconnect_recover_threshold |
0s |
Optional hysteresis before leaving unreachable |
Cold start with CP down no longer exits the process: the DP retries registration (or loading saved credentials) and the initial snapshot with capped backoff (1s → 30s) until the CP answers. With probes enabled, /healthz stays 200 and /readyz reports bootstrap_pending during the wait. Only unrecoverable setup errors still exit: no agent state and an empty PGWAY_AGENT_REGISTRATION_TOKEN, an unusable state file, or a rejected/expired registration token (re-register with a new registration token).
DP readiness has two gates. Until the first successful bootstrap (credentials + initial snapshot), /readyz reports bootstrap_pending. Afterwards it follows rejecting link state: not ready only when the DP is fail_closed and unreachable (cp_unreachable). Under fail_open, readiness stays ready so orchestrators keep routing while the DP serves stale config. Under fail_closed, rejected proxy requests include Retry-After set to cp_disconnect_unreachable_threshold (seconds).
Process probes (all binaries)¶
Opt-in dedicated listener (probes.enabled, default off). Prefer binding to loopback or a cluster-internal address (e.g. 127.0.0.1:8082 or a Service-only port) — do not expose probes on public interfaces. Paths are fixed:
| Path | Meaning |
|---|---|
GET /healthz |
Liveness — always 200 ok while the probe server answers |
GET /readyz |
Readiness — 200 ok or 503 with a short reason (bootstrap_pending, shutting_down, storage_unavailable, grpc_not_serving, cp_unreachable) |
Checks: CP / all-in-one → shutdown, Badger ping, gRPC serving. DP → shutdown, bootstrap latch (bootstrap_pending until the first successful bootstrap), and CPLinkStatus.Rejecting (fail_closed + unreachable). Empty entrypoint sets are still ready.
livenessProbe:
httpGet: { path: /healthz, port: 8082 }
periodSeconds: 10
readinessProbe:
httpGet: { path: /readyz, port: 8082 }
periodSeconds: 5
Outage behaviour (operator view)¶
| Situation | Default (fail_open) |
fail_closed |
|---|---|---|
| CP down < ~threshold window | Keep proxying on last-known config | Same |
CP down long enough to be unreachable |
Keep proxying (stale latch); logs cp link state changed |
New requests → 503 + Retry-After; open CONNECT tunnels keep running |
| CP returns, Watch reconnects | Full Resync (cache + listeners match CP again) | Same; 503 stops after recover |
| Cold start while CP is down | Process stays up and retries (readiness bootstrap_pending); becomes ready when CP answers |
Same |
All-in-one pgway |
Disconnect policy N/A; periodic Resync every event_resync_interval heals dropped in-process events |
Same |
Agent lifecycle¶
| Event | Effect |
|---|---|
| Successful Register | Credentials written to agent.state_path; status → active (with heartbeats) |
| Heartbeat | Extends sliding auth.agent_token_ttl; keeps active within agent.heartbeat_threshold |
| Graceful DP shutdown | Deregister → passive |
| Missed heartbeats | disconnected |
pgctl agent delete <name> |
Credentials revoked; row removed — need a new registration token to join again |
./build/pgctl agent list
./build/pgctl agent delete edge-1
Operations tips¶
- Give each agent a stable unique
agent.nameand its ownagent.state_path. - Point the DP (
grpc.dial_addr) andpgctl(-H/-Pflags) at the same CP (host reachable from that machine). - Prefer
PGWAY_AGENT_REGISTRATION_TOKENover committing secrets in TOML;pgctlcarries its token only in the--token-pathfile. - Entrypoint
host:portis local to the DP process — open firewalls accordingly. - Label-based DP placement (#46) and mTLS CP↔DP (#47) are still open.
Troubleshooting¶
| Symptom | Checks |
|---|---|
| DP exits: no agent state / empty registration token | First start needs PGWAY_AGENT_REGISTRATION_TOKEN or a populated agent.state_path |
DP not ready, logs agent bootstrap failed; retrying |
CP unreachable at startup — the DP is waiting (readiness bootstrap_pending); check CP uptime and network |
agent list shows disconnected |
Network to CP, agent.heartbeat_interval vs threshold, CP uptime |
| Apply OK but no listener | Confirm DP is running and Watch connected; look for entrypoint bind logs on DP, not CP |
pgctl auth errors |
pgctl login; PGWAY_GRPC_LISTEN_ADDR; not mixing agent token with user commands |
| Restarted CP before init | Bootstrap token rotated — use the new log value |