Skip to content

Commit 8cfd205

Browse files
committed
docs: dynamic configuration reload
Signed-off-by: Shane Utt <shaneutt@linux.com>
1 parent c23892e commit 8cfd205

4 files changed

Lines changed: 162 additions & 3 deletions

File tree

docs/architecture.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,31 @@ Request conditions gate both `on_request` and body hooks.
305305
Response conditions gate only `on_response` and response
306306
body hooks.
307307

308+
## Dynamic Configuration Reload
309+
310+
Praxis swaps filter pipelines at runtime without
311+
restarting the server or disrupting in-flight requests.
312+
313+
Each handler holds an `Arc<ArcSwap<FilterPipeline>>`
314+
instead of a plain `Arc<FilterPipeline>`. On every
315+
request, the handler calls `pipeline.load()` to get a
316+
snapshot pinned for that request's lifetime. A reload
317+
stores a new pipeline into the `ArcSwap`; the next
318+
request loads the new pointer while in-flight requests
319+
drain on the old one.
320+
321+
A file watcher (`notify` crate, 500ms debounce) monitors
322+
the config file. On change it validates the new config,
323+
rebuilds all pipelines, and swaps them atomically. If
324+
validation fails, nothing changes. Health check tasks
325+
are cancelled and respawned with a fresh registry on
326+
each successful reload.
327+
328+
Changes that cannot be applied dynamically (listener
329+
topology, protocol type, compression module, TLS toggle)
330+
are detected by diffing old and new configs and logged
331+
as warnings.
332+
308333
## Crate Layout
309334

310335
### Workspace Crates
@@ -357,7 +382,9 @@ benchmarks Benchmark tool and library
357382
358383
praxis Binary entry point
359384
├── pipelines Pipeline resolution from config
360-
└── server Protocol registration, startup
385+
├── reload Config reload orchestration (validate, swap, health lifecycle)
386+
├── server Protocol registration, startup
387+
└── watcher File watcher with debounce for config hot-reload
361388
362389
praxis-core Configuration, errors, and server factory
363390
├── config/ YAML parsing, defaults, and validation
@@ -438,10 +465,12 @@ praxis-filter Filter pipeline engine
438465
│ │ └── json_body_field Extract JSON field, promote to header
439466
│ ├── security/
440467
│ │ ├── cors CORS preflight handling, origin validation
468+
│ │ ├── credential_injection Per-cluster API key injection
441469
│ │ ├── forwarded_headers X-Forwarded-For/Proto/Host injection
442470
│ │ ├── guardrails Reject requests matching string/regex rules
443471
│ │ └── ip_acl Allow/deny by source IP/CIDR
444472
│ ├── traffic_management/
473+
│ │ ├── circuit_breaker Per-cluster circuit breaking (closed/open/half-open)
445474
│ │ ├── rate_limit Token bucket rate limiting (per-IP, global)
446475
│ │ ├── router Path-prefix + host routing to clusters
447476
│ │ ├── redirect 3xx redirect without upstream

docs/configuration.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,43 @@ shutdown_timeout_secs: # Optional. Graceful drain time (default: 30).
1717
insecure_options: # Optional. Dev/test overrides. See development.md.
1818
```
1919
20+
## Dynamic Configuration Reload
21+
22+
Praxis watches the config file for changes and
23+
automatically reloads filter pipelines without restart
24+
or disruption. When the file is modified, the server
25+
validates the new config, rebuilds pipelines, and swaps
26+
them atomically. In-flight requests complete on the old
27+
pipeline; new requests pick up the new config.
28+
29+
If the new config is invalid (bad YAML, unknown filter,
30+
validation failure), the server logs the error and
31+
continues serving with the old config.
32+
33+
**Dynamically reloadable:**
34+
35+
- Filter pipeline configuration
36+
- Router routes and path mappings
37+
- Load balancer endpoints and weights
38+
- Rate limit and circuit breaker settings
39+
- Health check configuration
40+
41+
**Requires restart (logged as warning):**
42+
43+
- Listener add, remove, or address rebind
44+
- Protocol changes (HTTP to TCP)
45+
- Compression module addition
46+
- TLS enable/disable
47+
48+
Stateful filters (rate limiter, circuit breaker) reset
49+
their state on reload. Operators should expect a brief
50+
burst window for rate limiters and a closed circuit for
51+
circuit breakers immediately after reload.
52+
53+
See [hot-reload.yaml] for an example.
54+
55+
[hot-reload.yaml]: ../examples/configs/operations/hot-reload.yaml
56+
2057
## Admin
2158
2259
`admin.address` binds a separate HTTP listener that serves
@@ -268,13 +305,15 @@ supports TCP-level filters too.
268305
| `timeout` | Traffic Management | HTTP |
269306
| `static_response` | Traffic Management | HTTP |
270307
| `rate_limit` | Traffic Management | HTTP |
308+
| `circuit_breaker` | Traffic Management | HTTP |
271309
| `headers` | Transformation | HTTP |
272310
| `request_id` | Observability | HTTP |
273311
| `access_log` | Observability | HTTP |
274312
| `tcp_access_log` | Observability | TCP |
275313
| `forwarded_headers` | Security | HTTP |
276314
| `guardrails` | Security | HTTP |
277315
| `ip_acl` | Security | HTTP |
316+
| `credential_injection` | Security | HTTP |
278317
| `json_body_field` | Payload Processing | HTTP |
279318
| `compression` | Payload Processing | HTTP |
280319
| `cors` | Security | HTTP |
@@ -347,12 +386,24 @@ recover. See [health-checks.yaml].
347386
| `timeout_ms` | integer | 2000 | Per-probe timeout in ms |
348387
| `healthy_threshold` | integer | 2 | Consecutive successes to mark healthy |
349388
| `unhealthy_threshold` | integer | 3 | Consecutive failures to mark unhealthy |
389+
| `passive_unhealthy_threshold` | integer | none | Consecutive upstream failures (5xx or connect error) to mark unhealthy without probes |
390+
| `passive_healthy_threshold` | integer | none | Consecutive upstream successes to recover a passively-marked endpoint |
350391

351392
TCP health checks only verify a TCP connection can be
352393
established; `path` and `expected_status` are ignored.
353394
When active health checks are configured, the admin
354395
`/ready` endpoint reports per-cluster health counts.
355396

397+
Passive health checking tracks upstream request
398+
outcomes inline. When `passive_unhealthy_threshold` is
399+
set, endpoints that return consecutive 5xx responses or
400+
connect errors are marked unhealthy without dedicated
401+
probe traffic. Set `passive_healthy_threshold` to
402+
control how many consecutive successes are required to
403+
recover. Passive and active checks can be used together;
404+
either mechanism can mark an endpoint unhealthy, and
405+
either can recover it.
406+
356407
By default, health check endpoints that resolve to
357408
loopback or cloud metadata addresses are rejected
358409
(SSRF protection).
@@ -448,6 +499,36 @@ When `allow` is set, only matching IPs are permitted.
448499
`allow` takes precedence over `deny`. Denied requests
449500
receive a `403 Forbidden` response.
450501

502+
### Credential Injection
503+
504+
Injects per-cluster API credentials into upstream
505+
requests and strips client-provided credentials to
506+
prevent forwarding. Pair with a source discriminator
507+
(IP ACL, client authentication) to control which
508+
clients receive credential upgrades. See
509+
[credential-injection.yaml].
510+
511+
```yaml
512+
- filter: credential_injection
513+
clusters:
514+
- name: openai
515+
header: Authorization
516+
value: "sk-example-key"
517+
header_prefix: "Bearer "
518+
strip_client_credential: true
519+
```
520+
521+
| Field | Type | Required | Description |
522+
| ----- | ---- | -------- | ----------- |
523+
| `clusters[].name` | string | yes | Cluster to inject credentials for |
524+
| `clusters[].header` | string | yes | Header name to set |
525+
| `clusters[].value` | string | one of | Inline credential value |
526+
| `clusters[].env_var` | string | one of | Environment variable containing the credential |
527+
| `clusters[].header_prefix` | string | no | Prefix prepended to the value (e.g. `"Bearer "`) |
528+
| `clusters[].strip_client_credential` | bool | no | Remove client-sent value before injection (default: true) |
529+
530+
[credential-injection.yaml]: ../examples/configs/ai/credential-injection.yaml
531+
451532
### TCP Access Log
452533

453534
Structured JSON logging of TCP connections. Works on both
@@ -515,6 +596,31 @@ successful responses.
515596
| `rate` | float | yes | Tokens per second (must be > 0) |
516597
| `burst` | integer | yes | Max bucket capacity (must be >= rate) |
517598

599+
### Circuit Breaker
600+
601+
Per-cluster circuit breaker that prevents cascading
602+
failures. When consecutive upstream failures reach the
603+
threshold, the circuit opens and subsequent requests
604+
receive 503 immediately. After the recovery window, a
605+
single probe request is forwarded; if it succeeds the
606+
circuit closes. See [circuit-breaker.yaml].
607+
608+
```yaml
609+
- filter: circuit_breaker
610+
clusters:
611+
- name: backend
612+
consecutive_failures: 5
613+
recovery_window_secs: 30
614+
```
615+
616+
| Field | Type | Required | Description |
617+
| ----- | ---- | -------- | ----------- |
618+
| `clusters[].name` | string | yes | Cluster name to protect |
619+
| `clusters[].consecutive_failures` | integer | yes | Failures before opening |
620+
| `clusters[].recovery_window_secs` | integer | yes | Seconds before half-open probe |
621+
622+
[circuit-breaker.yaml]: ../examples/configs/traffic-management/circuit-breaker.yaml
623+
518624
### Guardrails
519625

520626
Rejects requests matching string or regex rules against

docs/features.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
- **Active health checks** - HTTP and TCP health check
3131
probes with configurable thresholds; unhealthy hosts
3232
are automatically removed from load balancer rotation
33+
- **Passive health checks** - track upstream failures
34+
inline; endpoints that exceed a consecutive failure
35+
threshold are marked unhealthy without dedicated
36+
probe traffic
37+
- **Circuit breaker** - per-cluster circuit breaker that
38+
short-circuits requests to failing upstreams with 503,
39+
then gradually recovers via a half-open probe window
3340
- **Redirect** - return 3xx redirects without upstream;
3441
supports `${path}` and `${query}` template placeholders
3542
- **Timeout enforcement** - 504 rejection when upstream
@@ -138,6 +145,14 @@ deployment guidance.
138145

139146
## Operations
140147

148+
- **Dynamic configuration reload** - filter pipelines,
149+
routes, endpoints, health checks, and rate limits
150+
are swapped atomically at runtime when the config
151+
file changes. In-flight requests complete on the old
152+
pipeline; invalid configs are rejected and logged.
153+
Changes that require a restart (listener topology,
154+
TLS toggle, protocol type) are detected and logged
155+
as warnings.
141156
- **Graceful shutdown** - configurable drain timeout
142157
- **Runtime tuning** - thread pool sizing and
143158
work-stealing toggle
@@ -182,6 +197,12 @@ rather than bolted-on external processors.
182197
header-based routing to provider-specific clusters.
183198
Uses StreamBuffer to inspect the body before upstream
184199
selection.
200+
- **Credential injection** (`credential_injection`):
201+
per-cluster API key injection with client credential
202+
stripping. Supports inline values and environment
203+
variable sources. Pair with a source discriminator
204+
(IP ACL, client auth) to control which clients get
205+
credential upgrades.
185206

186207
### Planned
187208

@@ -199,8 +220,6 @@ filter pipeline.
199220
with sliding window or token bucket
200221
- **Cost attribution**: token counting mapped to user,
201222
session, model, and endpoint
202-
- **Credential injection**: per-cluster API key
203-
injection with credential stripping
204223
- **SSE streaming inspection**: per-event filter hooks
205224
for streaming responses
206225
- **Semantic caching**: prompt deduplication via vector

docs/filters.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ hooks have default implementations that pass through.
249249
- `Release` : forward accumulated StreamBuffer data to
250250
upstream; behaves as `Continue` in non-StreamBuffer
251251
contexts
252+
- `BodyDone` : signal that this filter has finished body
253+
processing; subsequent body chunks skip this filter
254+
while other filters continue normally
252255

253256
```rust
254257
FilterAction::Reject(Rejection::status(429)
@@ -470,6 +473,7 @@ A filter can have both `conditions` (request phase) and
470473
| `timeout` | Traffic Management | HTTP | `timeout_ms` (504 on exceed) |
471474
| `static_response` | Traffic Management | HTTP | `status` (required), `headers`, `body` |
472475
| `rate_limit` | Traffic Management | HTTP | `mode`, `rate`, `burst`; token bucket with per-IP and global modes |
476+
| `circuit_breaker` | Traffic Management | HTTP | `clusters[].consecutive_failures`, `.recovery_window_secs`; per-cluster circuit breaking |
473477
| `headers` | Transformation | HTTP | `request_add`, `response_add/set/remove` |
474478
| `request_id` | Observability | HTTP | Propagates/generates `X-Request-ID` |
475479
| `access_log` | Observability | HTTP | Structured JSON logging; optional `sample_rate` |
@@ -479,6 +483,7 @@ A filter can have both `conditions` (request phase) and
479483
| `forwarded_headers` | Security | HTTP | `trusted_proxies` (CIDR list) |
480484
| `guardrails` | Security | HTTP | Reject requests matching header/body string or regex rules |
481485
| `ip_acl` | Security | HTTP | `allow` / `deny` (CIDR lists); 403 on denial |
486+
| `credential_injection` | Security | HTTP | Per-cluster API key injection with client credential stripping |
482487
| `json_body_field` | Payload Processing | HTTP | Extract a JSON body field and promote to header |
483488
| `json_rpc` | Payload Processing | HTTP | Parse JSON-RPC 2.0 envelopes and extract method/id/kind for routing |
484489
| `compression` | Payload Processing | HTTP | Gzip, brotli, and zstd response compression |

0 commit comments

Comments
 (0)