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 600ad0e635 Add graceful shutdown and readiness gating for Jetty/Tomcat
microservices; externalize remaining starter projects; document graceful
shutdown.
600ad0e635 is described below
commit 600ad0e635a3e37ed93c5a51d908db463f1f7c20
Author: James Bognar <[email protected]>
AuthorDate: Fri Jun 12 11:42:46 2026 -0400
Add graceful shutdown and readiness gating for Jetty/Tomcat microservices;
externalize remaining starter projects; document graceful shutdown.
---
pages/release-notes/10.0.0.md | 38 +++++++
pages/topics/16.11.GracefulShutdown.md | 191 +++++++++++++++++++++++++++++++++
sidebars.ts | 5 +
3 files changed, 234 insertions(+)
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index 8dfac36b13..73270b2af6 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -61,6 +61,30 @@ The `JettyMicroservice.run(...)` facade has three overloads:
The legacy `juneau-my-jetty-microservice` template module has been removed
from the reactor. Consumers should migrate to the new `JettyMicroservice`
facade in `juneau-microservice-jetty` — it delivers the same zero-config
developer experience without requiring the consumer to copy a template project.
+### juneau-microservice-jetty / juneau-microservice-tomcat
+
+#### Graceful shutdown + readiness gating (zero-downtime k8s rollouts)
+
+Both embedded-server microservice flavors now perform a zero-downtime graceful
shutdown suitable for rolling Kubernetes deployments, sharing one identical
shutdown contract.
+
+On shutdown, a shared `ReadinessState` flag is flipped **before** the
listening connector stops, so `GET /readyz` immediately returns `503`
(`state=OUT_OF_SERVICE`) while a load balancer / Kubernetes is still routing.
`GET /livez` and `GET /healthz` stay healthy, so the orchestrator does not kill
the pod mid-drain. The server then waits up to a bounded `stopTimeout` (default
`30s`) for in-flight requests to drain — Jetty via
`server.setStopTimeout(...)`, Tomcat by pausing the connector a [...]
+
+- New shared `org.apache.juneau.rest.server.health.ReadinessState` flag,
consulted by `HealthAggregator` to gate the `READY` probe (liveness and the
aggregate health surface are unaffected).
+- New `stopTimeout` / `shutdownSettleDelay` knobs on both `JettySettings` and
`TomcatSettings`, also configurable via `juneau.cfg` (`[Jetty]` / `[Tomcat]`
sections, in milliseconds). Precedence: programmatic settings > config-file
entry > (Jetty only: `jetty.xml` value) > 30s default.
+- Bundled `juneau.cfg` documents the knobs and the recommended Kubernetes
`preStop` hook + `terminationGracePeriodSeconds` pairing.
+
+```java
+@Bean
+JettySettings jettySettings() {
+ return JettySettings.create()
+ .stopTimeout(Duration.ofSeconds(45))
+ .shutdownSettleDelay(Duration.ofSeconds(2))
+ .build();
+}
+```
+
+See the new [Graceful Shutdown & Readiness
Gating](/docs/topics/GracefulShutdown) topic page for details, including the
Jetty/Tomcat parity table and Kubernetes deployment guidance.
+
### juneau-petstore (new module family)
Three new modules under a top-level `juneau-petstore/` aggregator together
form the canonical Juneau petstore showcase application. The legacy
`juneau-examples-rest{,-jetty,-springboot,-jetty-ftest}` family of modules has
been retired in this release in favor of the petstore family — see the
**Breaking Changes** section below.
@@ -107,6 +131,20 @@ Launcher: `python3 scripts/start-petstore-springboot.py`.
The core-hosted surface (CRUD, view demos, React SPA demo, auth-gated subset)
is identical across both deployments by construction — it's shared code, not
parallel copies. The deployment-specific surface (Jetty's microservice console
+ admin resources vs Spring Boot's `@Autowired` injection demo) is
intentionally non-parity and documented as such.
+### juneau-rest-common / juneau-rest-server
+
+#### Content Security Policy (CSP) support
+
+Juneau can now emit `Content-Security-Policy` response headers for the HTML it
serves, including dynamic per-request nonce support. New in this release:
+
+- A first-class immutable `ContentSecurityPolicy` HTTP header bean + fluent
builder in `juneau-rest-common` (`org.apache.juneau.http.header`), with typed
directive setters (`defaultSrc`, `scriptSrc`, `styleSrc`, `imgSrc`,
`connectSrc`, `fontSrc`, `objectSrc`, `baseUri`, `frameAncestors`,
`reportUri`/`reportTo`), a generic `directive(name, sources...)` escape hatch,
source-expression helpers, report-only mode, and a ready-made nonce-based
"strict starter" preset.
+- Per-response `SecureRandom` nonce generation: the same nonce is injected
into the emitted `Content-Security-Policy` header and stamped onto every inline
`<script>`/`<style>` Juneau emits — the HtmlDoc template, the Swagger UI, the
OpenAPI UI, and menu-item widgets.
+- Opt-in by design: turning CSP on is a deliberate, one-line choice; existing
apps are unaffected by default.
+
+See the new [Content Security Policy](/docs/topics/ContentSecurityPolicy)
topic page for details.
+
+This feature was inspired by an earlier proposal from **Gary Gregory**
([@garydgregory](https://github.com/garydgregory), PR #57); the implementation
shipped here is a fresh one built against the current 10.0 APIs.
+
### Bug Fixes
_TBD — to be filled in as development continues._
diff --git a/pages/topics/16.11.GracefulShutdown.md
b/pages/topics/16.11.GracefulShutdown.md
new file mode 100644
index 0000000000..7e76448fe9
--- /dev/null
+++ b/pages/topics/16.11.GracefulShutdown.md
@@ -0,0 +1,191 @@
+---
+title: "Graceful Shutdown & Readiness Gating"
+slug: GracefulShutdown
+---
+
+> **See also:** [Health / Readiness / Liveness
Probes](/docs/topics/HealthProbes) — the probe surface (`/healthz`, `/readyz`,
`/livez`) this page gates during shutdown.
+
+Starting with **10.0.0**, both embedded-server microservice flavors —
`juneau-microservice-jetty` and `juneau-microservice-tomcat` — perform a
**zero-downtime graceful shutdown** suitable for rolling Kubernetes
deployments. The two servers share one identical shutdown contract.
+
+When a microservice begins shutting down it:
+
+1. **Flips readiness out of service first.** A shared `ReadinessState` flag is
flipped before the listening connector stops, so `GET /readyz` immediately
returns `503 Service Unavailable` (`state=OUT_OF_SERVICE`) while a load
balancer / Kubernetes is still routing. `GET /livez` and `GET /healthz` stay
healthy so the orchestrator does **not** kill the pod mid-drain.
+2. **Optionally settles.** An optional `shutdownSettleDelay` pause between the
readiness flip and the connector stop gives the load balancer a window to
observe the `503` before draining begins.
+3. **Drains in-flight requests.** The connector stops accepting new
connections and the server waits up to a bounded `stopTimeout` (default `30s`)
for in-flight requests to complete before it fully stops.
+
+This sequencing is what lets Kubernetes shift traffic away from a terminating
pod before its in-flight work is interrupted.
+
+## Shutdown sequence
+
+```
+shutdown begins
+ │
+ ├─▶ ReadinessState.markOutOfService() /readyz → 503 (OUT_OF_SERVICE)
+ │ /livez → 200 (still LIVE)
+ │ /healthz→ 200 (still UP)
+ │
+ ├─▶ sleep(shutdownSettleDelay) LB observes the 503 (default 0ms)
+ │
+ ├─▶ drain in-flight requests up to stopTimeout (default 30s)
+ │ Jetty: server.setStopTimeout(...) then server.stop()
+ │ Tomcat: connector.pause() then wait for active count → 0, then stop()
+ │
+ └─▶ server fully stopped
+```
+
+## `/readyz` vs `/livez` vs `/healthz` during shutdown
+
+| Probe | Normal | During shutdown | Why |
+|---|---|---|---|
+| `GET /readyz` | `200` (when no component is `DOWN`) | **`503`
`OUT_OF_SERVICE`** | Tells the load balancer / k8s readiness gate to stop
routing new traffic to this pod. |
+| `GET /livez` | `200` | `200` (unaffected) | A `503` here would make k8s
**kill** the pod mid-drain — deliberately not gated. |
+| `GET /healthz` | `200` / `503` per indicators | unaffected | The aggregate
(null-probe) health surface is not gated by readiness. |
+
+The readiness gate is implemented in `HealthAggregator`: when the shared
`ReadinessState` is out of service it forces the `READY` probe `DOWN` and adds
a `readiness` component with `{"state": "OUT_OF_SERVICE"}` to the payload.
Liveness and the aggregate health surface are untouched.
+
+```json
+{
+ "status": "DOWN",
+ "components": {
+ "readiness": {
+ "status": "DOWN",
+ "details": {
+ "state": "OUT_OF_SERVICE"
+ }
+ }
+ }
+}
+```
+
+## Tuning knobs
+
+Two knobs control the drain behavior, both expressible programmatically
(preferred), via the config file, or left at their defaults.
+
+| Knob | Default | Meaning |
+|---|---|---|
+| `stopTimeout` | `30s` | Bounded time the server waits for in-flight requests
to drain before stopping the connector. |
+| `shutdownSettleDelay` | `0` (none) | In-process pause between the `/readyz`
503 flip and the connector stop. The recommended Kubernetes pattern uses a
`preStop` hook sleep instead (see below). |
+
+### Precedence
+
+For both servers the resolved value follows this precedence:
+
+**programmatic settings > config-file entry > (Jetty only: existing
`jetty.xml` value) > 30s default**
+
+### Builder examples
+
+Contribute a `@Bean JettySettings` (or `@Bean TomcatSettings`) from your own
`@Configuration` class to override the defaults:
+
+```java
+import java.time.Duration;
+import org.apache.juneau.commons.inject.Configuration;
+import org.apache.juneau.commons.inject.Bean;
+import org.apache.juneau.microservice.jetty.JettySettings;
+
+@Configuration
+public class MyAppConfig {
+
+ @Bean
+ JettySettings jettySettings() {
+ return JettySettings.create()
+ .stopTimeout(Duration.ofSeconds(45))
+ .shutdownSettleDelay(Duration.ofSeconds(2))
+ .build();
+ }
+}
+```
+
+The Tomcat flavor is identical — swap `JettySettings` for `TomcatSettings`:
+
+```java
+import java.time.Duration;
+import org.apache.juneau.commons.inject.Configuration;
+import org.apache.juneau.commons.inject.Bean;
+import org.apache.juneau.microservice.tomcat.TomcatSettings;
+
+@Configuration
+public class MyAppConfig {
+
+ @Bean
+ TomcatSettings tomcatSettings() {
+ return TomcatSettings.create()
+ .stopTimeout(Duration.ofSeconds(45))
+ .shutdownSettleDelay(Duration.ofSeconds(2))
+ .build();
+ }
+}
+```
+
+Any value left `null` on the settings bean falls back to the config-file
entry, then to the default.
+
+### `juneau.cfg` examples
+
+Both knobs are documented in the bundled `juneau.cfg` and read from the
server's section (`[Jetty]` or `[Tomcat]`) in **milliseconds**:
+
+```ini
+[Jetty]
+# Graceful-shutdown drain timeout in milliseconds. Defaults to 30000 (30s)
when unset.
+stopTimeout = 30000
+
+# Settle delay in milliseconds applied between the /readyz 503 flip and the
connector stop.
+# Defaults to 0 when unset.
+shutdownSettleDelay = 0
+```
+
+For the Tomcat flavor the keys live under the `[Tomcat]` section instead
(`Tomcat/stopTimeout`, `Tomcat/shutdownSettleDelay`).
+
+## Jetty vs Tomcat parity
+
+Both servers expose the same knobs, the same defaults, and the same observable
readiness contract. They differ only in the underlying mechanism used to drain:
+
+| | Jetty | Tomcat |
+|---|---|---|
+| Apply drain timeout | `server.setStopTimeout(...)` applied at startup;
`server.stop()` drains | connector `pause()`, then wait up to `stopTimeout` for
the protocol-handler executor's active count to reach zero, then `stop()` /
`destroy()` |
+| Config section | `[Jetty]` | `[Tomcat]` |
+| `jetty.xml` override | honored if set (precedence above the default) | n/a |
+| Default `stopTimeout` | `30s` | `30s` |
+| Readiness flip / settle | identical | identical |
+
+The bundled `jetty.xml` deliberately does **not** set `stopTimeout` — it is
applied in code so the 30s default and the precedence rules apply uniformly. A
comment in `jetty.xml` documents this.
+
+## Kubernetes deployment guidance
+
+The recommended production pattern pairs the in-process readiness flip with a
`preStop` hook and a generous `terminationGracePeriodSeconds`, rather than
relying on the in-process `shutdownSettleDelay`:
+
+- Point the **readiness probe** at `/readyz` so k8s stops routing to the pod
as soon as it flips to `503`.
+- Point the **liveness probe** at `/livez` so k8s does not kill the pod while
it is draining.
+- Add a **`preStop` hook sleep** to give the cluster's endpoint/Service
propagation time to remove the pod from rotation before the JVM begins draining.
+- Set **`terminationGracePeriodSeconds`** to comfortably exceed `(preStop
sleep + shutdownSettleDelay + stopTimeout)` so Kubernetes does not `SIGKILL`
the pod mid-drain.
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+spec:
+ template:
+ spec:
+ # Must exceed preStop sleep + shutdownSettleDelay + stopTimeout.
+ terminationGracePeriodSeconds: 60
+ containers:
+ - name: my-juneau-microservice
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 10000
+ periodSeconds: 5
+ livenessProbe:
+ httpGet:
+ path: /livez
+ port: 10000
+ periodSeconds: 10
+ lifecycle:
+ preStop:
+ exec:
+ # Give Service/endpoint propagation time to drop this pod
before draining.
+ command: ["sh", "-c", "sleep 10"]
+```
+
+With this setup: the `preStop` sleep + readiness `503` keep new traffic away,
`stopTimeout` drains the in-flight requests, and
`terminationGracePeriodSeconds` guarantees the JVM gets enough time to finish
before a hard kill.
+
+:::info See Also
+[Health / Readiness / Liveness Probes](/docs/topics/HealthProbes)
+:::
diff --git a/sidebars.ts b/sidebars.ts
index 2f9703dfcd..d1ffc445ea 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -2042,6 +2042,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/16.10.HealthProbes',
label: '16.10. Health /
Readiness / Liveness Probes',
},
+ {
+ type: 'doc',
+ id:
'topics/16.11.GracefulShutdown',
+ label: '16.11. Graceful
Shutdown & Readiness Gating',
+ },
],
},
{