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:
# 3000
One Rust binary, one set of tests, every language reaps the benefit.
Install
Or use the prebuilt container image once it's published.
Run
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 \
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
# HTTP/1.1 200 OK
# X-Ferra-Index: 43
# Content-Type: application/json
#
# 3000
Reacting to changes (long-poll)
= 0
=
=
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:
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 persetevent.
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-serverunreachable on startup: snapshot load fails, agent retries with exponential backoff./readyzstays at 503 until a snapshot loads./cfg/{key}returns 404 in the meantime.ferra-serverunreachable after startup: cached values keep serving (last-known-good). The watch loop reconnects in the background./readyzstays at 200 because the snapshot was loaded successfully — disconnected doesn't mean unhealthy.reloadevent: 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