Skip to content

Health API

The health subsystem is a minimal, dependency-free HTTP/1.1 server that exposes Kubernetes-style probes (GET /livez, /readyz, /startupz) backed by a thread-safe readiness state, plus library-owned SIGTERM/SIGINT wiring that flips /readyz to 503 and then drains all subsystems. It is on by default only on the KUBERNETES platform and opt-in elsewhere.

This page is the API reference: exact types, signatures, config keys, and the per-language behavioral differences. For the narrative — how to drive the readiness gate and wire the probes into a Deployment — see the Health & graceful shutdown guide.

In a component you almost never construct the server yourself; the builder wires it for you, and you drive readiness through the runtime accessors (setReady / set_ready). The server, readiness, and config types below are documented because they are the public surface and are what the tests and builder use directly.

The server binds 0.0.0.0:<port> (default port 8081; port 0 selects an ephemeral port, used in tests). Bodies are tiny text/plain strings. These semantics are identical across all four SDKs:

Request Status Body Condition
GET <livenessPath> (default /livez) 200 ok Always while the process is alive. Never consults the broker — the handler running is itself the liveness proof, so a broker outage must not fail liveness.
GET <readinessPath> (default /readyz) 200 / 503 ok / not ready 200 only when messagingConnected && readyFlag && !shuttingDown; otherwise 503.
GET <startupPath> (default /startupz) 200 / 503 ok / not ready Reuses the readiness predicate exactly.
GET <unknown path> 404 not found Any path not matching the three above.
non-GET method diverges See non-GET method handling — the four SDKs differ.

The readyFlag defaults to true, so /readyz returns 200 as soon as messaging connects unless you call setReady(false). setReady(true) only lifts your gate — it cannot force readiness while disconnected or shutting down, because the connectivity and !shuttingDown parts of the predicate still apply.

Health lives under the health section of the component config (the single schema in schema/ggcommons-config-schema.json, shared by all four SDKs). Every key is optional; the values shown are the defaults.

{
"component": { "name": "com.example.MyComponent" },
"health": {
"enabled": true,
"port": 8081,
"livenessPath": "/livez",
"readinessPath": "/readyz",
"startupPath": "/startupz"
}
}

The enabled key is tri-state: absent means “use the platform default”, and an explicit value overrides in either direction (explicit false turns the server off even on KUBERNETES). See enablement precedence below.

Each SDK parses the health section into a config type. Java and Python carry a single HealthConfiguration (enabled + port + paths); Rust splits the on/off flag (HealthConfig.enabled, from the config model) from the server’s bind config (ServerConfig); TypeScript expresses the server’s inputs as HealthServerOptions / HealthPaths.

libs/java/.../config/HealthConfiguration.java
public HealthConfiguration(JsonObject jsonConfig); // accepts null
public boolean isEnabled(); // resolved on/off
public boolean isEnabledExplicitlySet(); // tri-state: was the "enabled" key present?
public int getPort(); // default 8081
public String getLivenessPath(); // default "/livez"
public String getReadinessPath(); // default "/readyz"
public String getStartupPath(); // default "/startupz"
// Constants: DEFAULT_PORT=8081, DEFAULT_LIVENESS_PATH="/livez",
// DEFAULT_READINESS_PATH="/readyz", DEFAULT_STARTUP_PATH="/startupz"

The readiness state holds the readyFlag and shuttingDown flags and answers the readiness predicate connected && ready && !shuttingDown. The connectivity check is supplied as a callback at construction so the state never depends on the messaging subsystem directly.

// Java has no ReadinessState class. The HealthServer takes BooleanSupplier callbacks
// (see "HealthServer" below), and you drive readiness via the runtime accessor:
gg.setReady(false); // hold the gate; gg.setReady(true) lifts it
// The runtime sets its internal `shuttingDown` flag during shutdown() — there is no
// public "begin shutdown" call; readiness flips to 503 automatically on drain.

The “begin shutdown” method name is one of the cross-language divergences: Python set_shutting_down(), Rust begin_shutdown(), TypeScript beginShutdown(), and Java has no separate object (it flips an internal volatile boolean shuttingDown field).

The server starts on a background thread and answers the three probe paths. Construction/startup and teardown differ by language idiom (see lifecycle & threading).

// libs/java/.../health/HealthServer.java — the constructor STARTS the server immediately.
public HealthServer(
int port, // 0 = ephemeral
String livenessPath,
String readinessPath,
String startupPath,
BooleanSupplier liveness, // () -> true: never checks the broker
BooleanSupplier readiness // () -> connected && readyFlag && !shuttingDown
) throws IOException;
public int getPort(); // resolves the actual (possibly ephemeral) port
public void close(); // idempotent; server.stop(0)

TypeScript is the only SDK that exports the router as a pure, public function — handy for unit testing routing without binding a socket. It takes the method, URL, paths, and readiness state and returns a { status, body } response with no I/O.

// Not available as a public function in Java. Routing happens inside the request handler
// (HealthServer); there is no standalone pure router to call.

In a component you interact with health through the built runtime instance (GGCommons / GgCommons), not the server directly. All four SDKs expose a readiness setter; beyond that the surface is intentionally asymmetric (see accessor asymmetry).

gg.setReady(false); // hold /readyz at 503 during startup
gg.setReady(true); // lift the gate (connectivity still required)
gg.shutdown(); // idempotent; flips readiness to 503 first, then drains
// Messaging accessor for this subsystem's connectivity is gg.getMessaging().

Enablement follows one rule in every language: explicit health.enabled wins; otherwise the platform default applies. The platform default is on only for KUBERNETES.

Platform Default Override
KUBERNETES on set health.enabled: false to turn it off
HOST off set health.enabled: true to turn it on
GREENGRASS off set health.enabled: true to turn it on
auto-detected as none of the above / unknown off set health.enabled: true to turn it on

The default lives in a per-language profileHealthEnabled(platform) helper (Java PlatformResolver.profileHealthEnabled, Python profile_health_enabled, Rust profile_health_enabled, TypeScript profileHealthEnabled) that returns true only for KUBERNETES (a null / None / unknown platform resolves to off). A bind failure is logged and swallowed in all four SDKs — a port clash never crashes your component.

The health subsystem is a deliberate four-way mirror, but a handful of behaviors differ. Document against the SDK you are using.

The probe handlers are GET-only, and Kubernetes only ever issues GET against probe paths, so this rarely matters in practice. The response to a non-GET method nonetheless diverges four ways:

SDK Behavior for a non-GET method
Java Path is checked first. non-GET on a known path (/livez//readyz//startupz) -> 405 “method not allowed”; non-GET on an unknown path -> 404 “not found”.
TypeScript method !== "GET" is checked first, before the path -> 405 “method not allowed” for any path, including unknown. So POST /nope -> 405 in TS but 404 in Java.
Rust The method is parsed then discarded — routing is by path regardless of method, so POST /livez -> 200. A malformed request line -> 400 “bad request”. There is no 405 path.
Python Only do_GET is defined; other verbs fall through to the stdlib BaseHTTPRequestHandler -> 501 Not Implemented.

All four SDKs expose setReady / set_ready. Beyond that:

  • Rust additionally exposes gg.is_shutting_down() (public, so a run loop can exit cooperatively).
  • TypeScript additionally exposes gg.ready() (a public boolean getter returning readiness.isReady()).
  • Java and Python expose neither readiness getter publicly — drive the gate through setReady / set_ready only. (Java’s isReadyz is package-private; Python’s readiness object is the private _readiness.)

Only TypeScript exports the pure evaluateHealth(...) router. Rust has an equivalent pure route(...) but it is private (#[cfg(test)]-tested in-module). Java and Python route inside the request handler with no standalone pure function.

SDK Server Teardown
Java JDK com.sun.net.httpserver.HttpServer on a single daemon thread close() = server.stop(0) (idempotent)
Python stdlib ThreadingHTTPServer on a daemon thread stop() joins with a 5s timeout (idempotent)
Rust hand-rolled TcpListener on a dedicated std::thread; non-blocking accept polled every 50ms; 2s per-connection read timeout RAII — dropping HealthServer (or GgCommons) stops it
TypeScript node http.Server with server.unref() so it never keeps the event loop alive async stop() returns a Promise; close-after-close is a no-op

Every SDK runs the same drain order on SIGTERM: readiness flips to 503 first, then subsystems drain, and the health server stops last — so /readyz keeps serving 503 throughout the drain and the orchestrator sees a clean “not ready -> gone” transition. The signal wiring itself differs by platform reality:

  • Java — a JVM shutdown hook (Runtime.addShutdownHook) catches SIGTERM/SIGINT; the JVM exits 0 after the hook. An app-driven shutdown() deregisters the hook; a shutdownComplete AtomicBoolean CAS makes it idempotent.
  • Pythonsignal.signal(SIGTERM, ...) is installed only on the main thread; the handler flips readiness, calls shutdown(), and sys.exit(0). SIGINT is not hooked by the library.
  • Rust — a tokio signal-watcher task calls state.begin_shutdown() on the first signal; teardown is RAII.
  • TypeScriptprocess.on("SIGTERM" | "SIGINT", ...) flips readiness, awaits close(), then process.exit(0); handlers are removed in close().