Health & readiness#

Two probes, and the important thing about them is that they are allowed to disagree.

TypeScript
import { health } from '@sigx/actors/host';

export const app = defineActorApp({ actors, storage })
    .use(cluster({ providers, advertise, secret }))
    .use(health());
RouteAnswers
GET /_sigx/healthliveness — is this process alive?
GET /_sigx/health/readyreadiness — should it receive traffic?

The three states#

StateLivenessReadinessWhat to do
active200200normal
leaving / draining200503drain it; do not restart
fenced503 {status:'fatal'}503restart the pod

A graceful host.stop() announces leaving before handoff — it is alive and finishing work, so restarting it is exactly wrong.

The fenced state is the one nothing else catches. A host that loses its membership heartbeat refuses every activation while its published status still says active. Only readiness sees it.

fatal forces a restart#

A readiness check can declare the process unrecoverable:

TypeScript
registry.reportHealth('membership', () => ({ ready: false, fatal: true, detail: 'fenced' }));

fatal implies not-ready, and the liveness route then answers 503 {status:'fatal'} so the orchestrator restarts the pod. cluster() marks fenced fatal for you.

Why fenced must restart rather than recover: host identities are minted per start, and a cluster host id is gone once its membership entry is. The fence is permanent for the process.

The readiness seam#

TypeScript
registry.reportHealth('db', () => ({ ready: await pingOk(), detail: 'primary' }));
registry.health();   // the aggregate

Every check must pass. All of them are evaluated, so a failing probe names every reason rather than the first. A throwing check reads as not-ready rather than a 500 — a health endpoint that 500s is a second incident on top of the first.

cluster() registers its own, which is why health() needs no wiring.

Before start()#

The whole mount answers 503, including liveness. Kubernetes therefore needs a startupProbe, or the liveness probe will kill the pod during a slow boot:

YAML
startupProbe:
  httpGet: { path: /_sigx/health, port: 7311 }
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet: { path: /_sigx/health, port: 7311 }
readinessProbe:
  httpGet: { path: /_sigx/health/ready, port: 7311 }

Probes cannot be authenticated#

A kubelet cannot sign the cluster HMAC, so these routes are unauthenticated by necessity — which is why they must be internal-only, and why they deliberately withhold perType detail.

TypeScript
health({ detail: false });   // reduce the body to the status code

The asymmetry is the design: health() answers may I take traffic? in a status code and cannot be authenticated. ops() answers what is going on in here? in a body, is authenticated, and may therefore carry actor type names and cluster topology.

Next steps#