This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 4097bce106 docs: /admin/ratelimit + RateLimitGuard topic-page updates
for snapshot SPI + 9.5.0 release-notes entry (TODO-89)
4097bce106 is described below
commit 4097bce10667ebf75820cffbd6c7b91f049ef591
Author: James Bognar <[email protected]>
AuthorDate: Tue May 26 13:53:19 2026 -0400
docs: /admin/ratelimit + RateLimitGuard topic-page updates for snapshot SPI
+ 9.5.0 release-notes entry (TODO-89)
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 60 ++++++++++++++++++---
pages/topics/10.14c.OpsIntrospectionMixins.md | 43 +++++++++++++--
.../10.20c.RestServerRateLimitAndRequestId.md | 61 ++++++++++++++++++++++
3 files changed, 151 insertions(+), 13 deletions(-)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 78c624ac21..4be1010299 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2664,14 +2664,15 @@ method on the host, leave the rest unmounted.
`@RestOp(method="*")` so every HTTP verb hits the same code path.
- **`BasicAdminResource`** — serves `/admin/threads` (JSON thread dump),
`/admin/heap`
(`Runtime` + `MemoryMXBean` stats), `POST /admin/cache/flush` (run
registered hooks),
- and `/admin/ratelimit` (registered `RateLimitGuard` beans). Default-deny via
the new
- `org.apache.juneau.rest.guard.DenyAllGuard` until the host registers a
`@Bean RestGuardList`
- factory; the framework's bean-store override seam **replaces** the entire
annotation-derived
- guard list (including the deny-all) with the user-supplied chain. Builder
methods:
- `cacheFlush(String, Runnable)`, `cacheFlushAll(Map)`,
`threadNamePrefixExclude(String...)`.
- Bucket-level rate-limit inspection (per-key counters) is reserved for a
follow-on once
- `RateLimitGuard.Storage` exposes a snapshot SPI; v1 emits configuration only
and returns
- `404 Not Found` when no `RateLimitGuard` bean is registered.
+ and `/admin/ratelimit` (registered `RateLimitGuard` beans — both static
configuration and
+ live per-key bucket snapshot; see the
+ [`RateLimitGuard.Storage.snapshot()` SPI
extension](#ratelimitguardstorage-snapshot-spi--basicadminresource-enrichment-work-item-89)
+ below). Default-deny via the new `org.apache.juneau.rest.guard.DenyAllGuard`
until the host
+ registers a `@Bean RestGuardList` factory; the framework's bean-store
override seam
+ **replaces** the entire annotation-derived guard list (including the
deny-all) with the
+ user-supplied chain. Builder methods: `cacheFlush(String, Runnable)`,
+ `cacheFlushAll(Map)`, `threadNamePrefixExclude(String...)`. Returns `404 Not
Found` when no
+ `RateLimitGuard` bean is registered.
- **`BasicRouteIndexResource`** — serves `/options` and `/routes` (synonyms)
returning a JSON
list of every `@RestOp`-annotated method on the host (and on every mixin on
the host),
ordered by path. Each entry has `path`, `methods` (single-element list of
the HTTP method),
@@ -2694,6 +2695,49 @@ tests under both Jetty microservice and Spring Boot.
`@Rest(guards=...)` or `@RestOp(guards=...)` site that wants the same
default-deny posture
with a `@Bean RestGuardList` override seam.
+#### `RateLimitGuard.Storage.snapshot()` SPI + `BasicAdminResource` enrichment
(work item 89)
+
+`org.apache.juneau.rest.guard.RateLimitGuard.Storage` gained an optional
read-side SPI for
+operator-facing tooling, and `BasicAdminResource`'s `/admin/ratelimit`
endpoint now uses it to
+emit live per-bucket state alongside the existing static configuration. Purely
additive — the
+new method is a `default` returning `Map.of()` so external storage backends
(Redis-backed,
+DynamoDB-backed, etc.) keep compiling unchanged.
+
+- **New `default Map<String, RateLimitGuard.BucketState> snapshot()` on
`RateLimitGuard.Storage`.**
+ Returns a point-in-time view of every per-key bucket the storage backend
currently tracks.
+ The default empty-map implementation is the backwards-compatible opt-out for
storages that
+ can't cheaply enumerate buckets. In-memory implementations SHOULD override.
+- **New `RateLimitGuard.BucketState` record.** Token-bucket-vocabulary
+ per-bucket snapshot: `String key`, `double tokens` (current fractional fill
level),
+ `int remaining` (`floor(tokens)` — mirrors the `X-RateLimit-Remaining`
advisory header),
+ `boolean throttled` (convenience flag for `tokens < 1.0`), `Instant
lastRequest`
+ (wall-clock timestamp of the bucket's last activity).
+- **Bundled `Storage.inMemory()` overrides `snapshot()`.** Walks the internal
+ `ConcurrentHashMap<String,Bucket>` and emits one `BucketState` per entry.
Each per-bucket
+ read is consistent (synchronized on the bucket); the snapshot as a whole is
not a global
+ point-in-time consistent view (consistent with the `ConcurrentHashMap`
weakly-consistent
+ iteration semantics).
+- **`Bucket` gained a `lastWallMillis` field** alongside the existing
`lastNanos` so
+ `BucketState.lastRequest` is wall-clock accurate (and not skewed by JVM
suspension /
+ container pause). Costs 8 bytes per bucket (~800 KB worst-case at the
default 100k-key cap).
+- **New public accessors on `RateLimitGuard`.** `getCapacity()`,
`getPermitsPerSecond()`,
+ `isXForwardedForAware()`, `getExemptPaths()`, `getStorage()`, and the
convenience
+ `snapshot()` that delegates to `getStorage().snapshot()`. Expose existing
builder state for
+ operator-facing dashboards.
+- **`/admin/ratelimit` response shape — `buckets` renamed to `snapshot`,
`config` enriched.**
+ The 9.5.0 ops-pack mixin (FINISHED-77) emitted
+ `{ "guards": { "<bean-name>": { "config": {"class": "..."}, "buckets": [] }
} }` with the
+ empty `buckets` array as an explicit placeholder. Now the response is
+ `{ "guards": { "<bean-name>": { "config": {"class": "...", "limit": N,
+ "permitsPerSecond": X, "xForwardedForAware": bool, "exemptPaths": [...] },
+ "snapshot": [ {"key": "...", "tokens": N.N, "remaining": N, "throttled":
bool,
+ "lastRequest": "..."} ] } } }` — `snapshot` entries are sorted ascending by
`key` for
+ stable operator-tooling output. Storage backends that don't override
+ `Storage.snapshot()` (e.g. Redis-backed impls) emit `"snapshot": []` for
their bean entry.
+ The `buckets` field name was an FINISHED-77 placeholder that only ever
emitted `[]`; the
+ rename to `snapshot` matches the SPI method name and is unlikely to affect
any real
+ consumer.
+
#### Server-side SSE Helpers (TODO-62)
`juneau-rest-server` now includes an SSE helper layer for streaming endpoints:
diff --git a/pages/topics/10.14c.OpsIntrospectionMixins.md
b/pages/topics/10.14c.OpsIntrospectionMixins.md
index d94132e393..17aa91a720 100644
--- a/pages/topics/10.14c.OpsIntrospectionMixins.md
+++ b/pages/topics/10.14c.OpsIntrospectionMixins.md
@@ -17,7 +17,7 @@ configure them via a `@Bean` factory, and leave the rest
unmounted.
| Mixin | Default `paths` | Default behavior | Why it exists |
|---|---|---|---|
|
[`BasicEchoResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicEchoResource.html)
| `/echo/*`, `/debug/echo/*` | `404 Not Found` until `Debug` is enabled. When
debug-on, returns a JSON body reflecting the inbound method, path, query
string, headers (sensitive ones redacted), query params, attributes, and
bounded body capture. | Round-trip request introspection — invaluable for
diagnosing proxy / mTLS / auth-header issues without spinning up a packet
capture. |
-|
[`BasicAdminResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicAdminResource.html)
| `/admin/threads`, `/admin/heap`, `/admin/cache/flush`, `/admin/ratelimit` |
`403 Forbidden` until the host registers a `@Bean RestGuardList`. Once
unlocked: `GET /admin/threads` (JSON thread dump), `GET /admin/heap` (Runtime +
MemoryMXBean stats), `POST /admin/cache/flush` (run registered hooks), `GET
/admin/ratelimit` (configured `RateLimitGuard` beans). | JVM operational
visibility on a deploye [...]
+|
[`BasicAdminResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicAdminResource.html)
| `/admin/threads`, `/admin/heap`, `/admin/cache/flush`, `/admin/ratelimit` |
`403 Forbidden` until the host registers a `@Bean RestGuardList`. Once
unlocked: `GET /admin/threads` (JSON thread dump), `GET /admin/heap` (Runtime +
MemoryMXBean stats), `POST /admin/cache/flush` (run registered hooks), `GET
/admin/ratelimit` (registered `RateLimitGuard` beans — config + live per-key
bucket snapshot). | [...]
|
[`BasicRouteIndexResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicRouteIndexResource.html)
| `/options`, `/routes` | JSON list of every `@RestOp`-annotated method on the
host (and its mixins), excluding `@OpSwagger(ignore=true)` ops and itself. Each
entry: `path`, `methods`, `summary`, `description`, `deprecated`. |
Machine-readable navigation index for tooling that needs a non-Swagger view of
the URL surface (smoke-test scripts, auto-generated nav, etc.). |
All endpoints across the pack carry
@@ -175,10 +175,43 @@ canonical auth chain when it ships will unlock the admin
paths automatically.
callers that want async semantics own the threading model. Unknown names are
silently
ignored (404-on-unknown would leak the registered hook set).
* `GET /admin/ratelimit` — JSON map keyed by bean name listing every registered
-
[`RateLimitGuard`](/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.html).
Returns
- `404 Not Found` when no `RateLimitGuard` bean is registered. Bucket-level
inspection
- (per-key counters) is reserved for a follow-on once `RateLimitGuard.Storage`
exposes a
- snapshot SPI; v1 emits configuration only.
+
[`RateLimitGuard`](/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.html).
Each
+ entry has two sub-fields: `config` (the guard's static configuration:
`class`, `limit`,
+ `permitsPerSecond`, `xForwardedForAware`, `exemptPaths`) and `snapshot` (a
sorted-by-key
+ array of
[`BucketState`](/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.BucketState.html)
+ entries describing every per-key bucket the storage backend currently
tracks). Each
+ `BucketState` carries `key`, `tokens` (fractional fill level), `remaining`
(integer tokens
+ available — mirrors the `X-RateLimit-Remaining` advisory header),
`throttled` (convenience
+ flag for `tokens < 1.0`), and `lastRequest` (wall-clock `Instant` of last
activity).
+ Returns `404 Not Found` when no `RateLimitGuard` bean is registered. Storage
backends that
+ don't override
[`Storage.snapshot()`](/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.Storage.html#snapshot())
+ (e.g. Redis-backed impls that can't cheaply enumerate buckets) emit
`"snapshot": []` for
+ their bean entry — the SPI method is `default` returning `Map.of()` so
external
+ `Storage` implementations stay backwards-compatible. See [Rate-Limiting and
Request-Id
+ Propagation § Inspecting bucket state at
runtime](/docs/topics/RestServerRateLimitAndRequestId#inspecting-bucket-state-at-runtime)
+ for the full snapshot SPI contract.
+
+ Example response shape:
+
+ ```json
+ {
+ "guards": {
+ "rateLimit": {
+ "config": {
+ "class": "org.apache.juneau.rest.guard.RateLimitGuard",
+ "limit": 100,
+ "permitsPerSecond": 1.67,
+ "xForwardedForAware": false,
+ "exemptPaths": ["/healthz", "/livez", "/readyz"]
+ },
+ "snapshot": [
+ {"key": "203.0.113.42", "tokens": 23.0, "remaining": 23,
"throttled": false, "lastRequest": "2026-05-26T14:00:42Z"},
+ {"key": "203.0.113.99", "tokens": 0.5, "remaining": 0, "throttled":
true, "lastRequest": "2026-05-26T14:00:58Z"}
+ ]
+ }
+ }
+ }
+ ```
### `BasicRouteIndexResource`
diff --git a/pages/topics/10.20c.RestServerRateLimitAndRequestId.md
b/pages/topics/10.20c.RestServerRateLimitAndRequestId.md
index 38d1653be1..4853f69ae6 100644
--- a/pages/topics/10.20c.RestServerRateLimitAndRequestId.md
+++ b/pages/topics/10.20c.RestServerRateLimitAndRequestId.md
@@ -142,6 +142,67 @@ The optional `whenLimitExceeded(BiConsumer<RestRequest,
RateLimitInfo>)` callbac
Use this hook for metrics (`statsd`, `micrometer`), structured logging, or
pushing throttle events onto a queue for downstream analysis.
+### Inspecting bucket state at runtime
+
+`RateLimitGuard.Storage` exposes an optional read-side SPI for operator-facing
tooling: <a
href="/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.Storage.html#snapshot()"
target="_blank">`default Map<String, BucketState> snapshot()`</a>. It returns
a point-in-time view of every per-key bucket the storage backend currently
tracks, so operators can answer not only "what is the rate-limit policy?" but
also "which clients are currently throttled and at what fill level?".
+
+The method is `default` returning `Map.of()`, which makes the addition
**purely additive and binary-compatible**: external storage backends
(Redis-backed, DynamoDB-backed, JDBC-backed, …) that don't enumerate buckets
cheaply keep compiling unchanged and inherit the empty-map default. The bundled
in-memory storage overrides `snapshot()` to walk its
`ConcurrentHashMap<String,Bucket>` and emit one `BucketState` per entry.
+
+#### `BucketState` record
+
+Each entry in the returned map is a <a
href="/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.BucketState.html"
target="_blank">`BucketState`</a> record describing a single per-key bucket:
+
+| Field | Meaning
|
+|------------------|----------------------------------------------------------------------------------------------|
+| `key()` | The per-request key the bucket is registered under (IP,
user id, custom). |
+| `tokens()` | The bucket's current fractional fill level (continuously
refilled at `permitsPerSecond`). |
+| `remaining()` | Integer tokens currently available — `floor(tokens)`.
Mirrors `X-RateLimit-Remaining`. |
+| `throttled()` | `true` when `tokens < 1.0` (the next request from this
key would be rejected). |
+| `lastRequest()` | Wall-clock
[`Instant`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Instant.html)
of the bucket's last activity. |
+
+Vocabulary is intentionally token-bucket-shaped: the underlying model is
continuous-refill, so there is no discrete "window start" to report. `tokens`
(the fill level) and `lastRequest` (the last activity) together fully describe
the bucket's state.
+
+The `lastRequest` field is wall-clock-accurate because each `Bucket` carries a
`lastWallMillis` field updated alongside the existing monotonic
`System.nanoTime()` value. The 8-byte-per-bucket overhead is trivial (~800 KB
worst-case at the default 100k-key cap) and avoids the drift you'd get from
mapping a `nanoTime` delta back to wall-clock under JVM suspension or container
pause.
+
+#### `RateLimitGuard` accessors
+
+For programmatic access — operator dashboards, custom admin endpoints,
integration tests — `RateLimitGuard` exposes the configured state plus a
snapshot convenience:
+
+| Method | Returns
|
+|------------------------------|------------------------------------------------------|
+| `getCapacity()` | The bucket capacity (burst).
|
+| `getPermitsPerSecond()` | The steady-state refill rate.
|
+| `isXForwardedForAware()` | Whether `X-Forwarded-For`-aware keying is
enabled. |
+| `getExemptPaths()` | The set of paths that bypass the guard.
|
+| `getStorage()` | The configured `Storage` backend.
|
+| `snapshot()` | Convenience for `getStorage().snapshot()`.
|
+
+```java
+var guard = RateLimitGuard.create()
+ .permitsPerSecond(100)
+ .burst(200)
+ .build();
+
+// ... requests flow through ...
+
+Map<String, BucketState> live = guard.snapshot();
+for (var b : live.values())
+ if (b.throttled())
+ log.warn("client {} is throttled (last seen {})", b.key(),
b.lastRequest());
+```
+
+#### Consistency contract
+
+Each per-bucket read inside `InMemoryStorage.snapshot()` is consistent (the
`Bucket` accessors are `synchronized` on the bucket monitor), but the snapshot
as a whole is **not** a global point-in-time consistent view — the iterator
walks `ConcurrentHashMap` weakly-consistent entries, and concurrent
`tryAcquire(...)` calls on other buckets may interleave with the walk. This is
the right trade-off for operator visibility: contention-free under load,
accurate per-key, and inconsistent only i [...]
+
+The returned `Map` is immutable (`Map.copyOf(...)`); callers can safely pass
it across threads.
+
+#### `/admin/ratelimit` endpoint
+
+The Juneau ops/introspection mixin pack ships
[`BasicAdminResource`](/docs/topics/OpsIntrospectionMixins#basicadminresource),
whose `/admin/ratelimit` endpoint already exposes `snapshot()` over HTTP for
every registered `RateLimitGuard` bean — alongside the enriched `config` block
(`class`, `limit`, `permitsPerSecond`, `xForwardedForAware`, `exemptPaths`).
See the [`BasicAdminResource`
section](/docs/topics/OpsIntrospectionMixins#basicadminresource) of the
ops-pack page for the full JSON [...]
+
+Storage backends that don't override `snapshot()` emit `"snapshot": []` for
their bean entry on `/admin/ratelimit` — the SPI's default-empty contract is
honored end-to-end.
+
## `RequestIdFilter` — `X-Request-Id` mint / honor / echo
<a href="/site/apidocs/org/apache/juneau/rest/filter/RequestIdFilter.html"
target="_blank">RequestIdFilter</a> is a thin pre-call filter that: