ferra-agent 0.0.1

Sidecar agent for Ferra: holds an in-memory cache + SSE watch and exposes a localhost HTTP API to nearby service containers
Documentation

ferra-agent

Sidecar agent for Ferra. Holds an in-memory cache of every key under one or more configured prefixes, keeps it fresh via SSE watch against ferra-server, and exposes a tiny localhost HTTP API the service container hits for reads.

Why use it

Without the agent, every service that talks to Ferra has to implement the ~500-line cache + watch + reconnect loop itself (per language). With the agent, services do a 5-line localhost HTTP call:

curl http://localhost:9999/cfg/services/payment/timeout_ms
# 3000

One Rust binary, one set of tests, every language reaps the benefit.

Install

cargo install ferra-agent

Or use the prebuilt container image once it's published.

Run

ferra-agent \
  --server http://ferra-server.ferra.svc.cluster.local:8080 \
  --prefix services/payment/ \
  --prefix flags/global/ \
  --listen 127.0.0.1:9999

Or via env vars (more typical for K8s):

FERRA_SERVER=http://ferra-server.ferra.svc.cluster.local:8080 \
FERRA_PREFIX=services/payment/,flags/global/ \
FERRA_AGENT_LISTEN=127.0.0.1:9999 \
ferra-agent

Local API

Consumed by the service container via localhost:

GET /cfg/{key}                            # current value, ~1ms localhost RTT
GET /cfg/{key}?wait=30s&since=N           # long-poll, returns when key changes
GET /cfg?prefix=services/payment/         # list everything under a prefix
GET /healthz                              # always 200 (process alive)
GET /readyz                               # 200 when initial snapshots loaded

The wait/since parameters give you Consul-style blocking-query semantics: since is the cursor you last saw (header X-Ferra-Index), wait is the max hold time (capped at 5 minutes). Returns when the key's event_id > since or the wait elapses.

X-Ferra-Index is on every response; use it as your next since.

Reading a value

curl -i http://localhost:9999/cfg/services/payment/timeout_ms

# HTTP/1.1 200 OK
# X-Ferra-Index: 43
# Content-Type: application/json
#
# 3000

Reacting to changes (long-poll)

since = 0
while True:
    r = requests.get(
        f"http://localhost:9999/cfg/consumer/goroutines",
        params={"wait": "30s", "since": since},
    )
    pool.resize(int(r.json()))
    since = int(r.headers["X-Ferra-Index"])

The agent holds the connection until the value changes or 30s elapses, so the service code is a normal blocking HTTP call — no SSE parsing.

Prefix scoping

The agent only caches the prefixes you tell it to watch. A request for /cfg/services/auth/jwt from a service whose agent watches only services/payment/ returns:

{ "error": "key_not_in_watched_prefix", "message": "..." }

This is intentional — it forces explicit declaration of which config a service needs, instead of letting agents accidentally fetch the entire instance.

Resource footprint

Per Pod:

  • Cache memory: ~200 bytes × number of keys under the watched prefixes. 10–100 KB is typical; 1 MB is large.
  • Background memory: ~10 MB (Rust runtime + reqwest + axum).
  • CPU: idle.
  • Network: one persistent SSE connection to ferra-server, plus a brief GET per set event.

Configuration

Flags (env-var equivalents in parentheses):

Flag Env Default Purpose
--server FERRA_SERVER required URL of ferra-server
--prefix (repeatable) FERRA_PREFIX (comma-separated) required Prefix(es) to watch
--listen FERRA_AGENT_LISTEN 127.0.0.1:9999 Local listen address
--min-backoff FERRA_MIN_BACKOFF 500ms Reconnect backoff floor
--max-backoff FERRA_MAX_BACKOFF 30s Reconnect backoff ceiling

Failure model

  • ferra-server unreachable on startup: snapshot load fails, agent retries with exponential backoff. /readyz stays at 503 until a snapshot loads. /cfg/{key} returns 404 in the meantime.
  • ferra-server unreachable after startup: cached values keep serving (last-known-good). The watch loop reconnects in the background. /readyz stays at 200 because the snapshot was loaded successfully — disconnected doesn't mean unhealthy.
  • reload event: agent re-fetches the snapshot wholesale, replaces its cache, and resumes watching.

Kubernetes example

See deploy/kubernetes/40-example-consumer-with-sidecar.yaml in the main repo.

License

MIT