KDCube
← Our Journal

A processor can be alive while one of its apps is still getting ready. Its event loop is running. Redis and PostgreSQL are connected. Other apps can answer. The slow app may be checking out source, rebuilding an index, waiting for a recoverable shared lock, or compiling a UI.

Previously, one registry-wide preload pass made those facts indistinguishable. The processor reported not ready until the slowest configured app finished. An orchestrator could then replace a healthy process because an app was legitimately preparing, interrupt the work, and make recovery take longer.

The correction was not to skip app initialization or hide it in an unowned background task. KDCube now gives every app a supervised lifecycle and separates four availability facts that used to collapse into one.

Preparation remains a first-class lifecycle

At processor startup, KDCube reads the authoritative app registry and computes one desired generation for every app. A generation is an internal fingerprint of the source, module, effective properties, and runtime resource schema that affect prepared behavior. It is not an app version shown to a caller.

The processor starts one strongly owned task per app, under bounded concurrency, and continues starting its core turn, event, job, and HTTP machinery.

One supervised preparation lane per application generation The processor starts core work immediately while an owned application task resolves source, runs process-local on_bundle_load, validates surfaces, runs generation-fenced shared deployment and UI publication, then marks that exact generation ready. Failure retries with bounded backoff and a newer generation supersedes only that lane. PROC STARTUP Load the registry, publish desired generations, start owned app tasks core processor work starts now OWNED TASK: ONE APP + ONE DESIRED GENERATION 1. Source Resolve Git/local source bundle_registry.py 2. Local load Import + on_bundle_load() bundle_loader.py 3. Validate Declared surfaces app_lifecycle/runtime.py 4. Shared deployment on_app_deploy() + UI publish app_deployment/coordinator.py 5. READY Exact generation only app_readiness.py Failure stays in this lane PREPARING -> RETRYING -> bounded backoff -> PREPARING The supervisor retains the task and observes every completion. A newer generation supersedes only this lane Cancel and reap old work; stale completion cannot publish READY. Other app tasks and processor work continue. STRONG OWNERSHIP · BOUNDED CONCURRENCY · REQUEST-INDEPENDENT LIFETIME
Preparation is asynchronous at the processor boundary, but strongly owned inside each app lane.

The ownership distinction inside that rail matters. on_bundle_load() runs in every processor process that loads the app. It prepares process-local clients, caches, repositories, or indexes. A shared completion marker cannot prove that another process ran its local hook.

on_app_deploy() has the shared scope. It publishes idempotent resources for one desired app generation, coordinated by signatures and distributed locks. Static UI compilation is one participant in that shared deployment phase, not the definition of readiness.

Only after both scopes complete does the local process mark that app generation ready. A late completion for an older generation cannot satisfy a newer desired state.

Four facts, four owners

Four separate availability facts Process liveness feeds health live, required application aggregate readiness feeds health, per-application generation state feeds diagnostics, and every application door checks that requested app before execution. Independent apps do not block aggregate health but remain admission-gated. ONE RUNTIME, FOUR QUESTIONS 1. Is the proc alive? Event loop running, not draining GET /health/live Used for process/container replacement. App preparation never changes this answer. 2. May proc receive traffic? All required apps are READY GET /health Only service.readiness: required participates. Independent apps remain visible but do not block. 3. Is this generation ready? pending / preparing / retrying / ready readiness registry operator diagnostics Tracks desired and ready generations. Late stale completion cannot satisfy new state. 4. May this call enter? Requested app generation is READY application door admit or defer REST · MCP · UI · chat · jobs · Data Bus The door reads state; it never starts repair. INDEPENDENT: DOES NOT BLOCK /health, STILL GATED AT ITS OWN DOOR · REQUIRED: BLOCKS /health AND ITS OWN DOOR
Liveness, aggregate readiness, per-app readiness, and admission answer different operational questions.

The descriptor chooses only the aggregate-readiness answer:

- id: reports@1-0
  path: /bundles/apps/reports@1-0
  module: entrypoint
  service:
    readiness: required

independent is the default. An independent app does not hold aggregate /health while it prepares. Its own doors are still closed until it is ready.

required means normal processor traffic depends on that app. While it prepares, /health returns 503; /health/live remains a liveness fact and stays healthy. The app's own doors are closed in both modes.

Independent describes aggregate health policy, not permission to call an unready app.

Doors read state; they do not repair it

After resolving an app id and before loading or invoking app code, each application surface reads the committed readiness state. The request never starts source resolution, lifecycle hooks, npm, Vite, or deployment repair.

{
  "type": "application_not_ready",
  "application_id": "reports@1-0",
  "state": "preparing",
  "retryable": true
}

The internal generation, source path, credentials, and preparation error stay out of the public response. Authenticated operator diagnostics retain the desired and ready generations, attempts, timestamps, retry timing, and a bounded error.

  • REST, UI, sites, and public content return app-scoped 503.
  • MCP carries the same fact in JSON-RPC error data.
  • Chat releases its unstarted queue claim before visible effects.
  • Scheduled work stays inactive until the READY callback reconciles it.
  • Data Bus work remains unacknowledged for a later claim.
  • Admin status, configuration, reload, and retry stay available.

This keeps inventory honest. The app is configured and discoverable; it is temporarily unavailable for execution. Nothing starts a second preparation task because a caller arrived at the wrong moment.

Failure belongs to the app that failed

The lifecycle supervisor retains every task reference and observes every completion. A failed app moves to retrying and uses bounded exponential backoff. Another app continues serving. A source or property update computes a new desired generation, invalidates only that app's local state, and supersedes only its old preparation task.

Request cancellation has no ownership relationship to preparation. A browser disconnect cannot cancel a shared UI build or app deployment. Explicit supersession and processor shutdown can, and both paths cancel and reap the owned work.

For UI work, the existing lower-level guarantees remain: worker-local source and node_modules, shared locks and output, lock heartbeat and TTL recovery, npm/Vite in a dedicated process group, atomic artifact publication, and cleanup after the signature is published and the lock is released.

One runtime contract, different deployment adapters

  • Process or container replacement probes /health/live.
  • Traffic readiness probes /health when required-app policy should control the processor as a whole.
  • App-specific calls rely on the application door.
  • Monitoring and authenticated diagnostics explain state without redefining it.

The same state model works under Docker Compose, Kubernetes, and ECS. The deployment platform consumes the runtime's answers; it does not decide what application readiness means.

Highlights

Every configured app still prepares; none is silently skipped.
Preparation is owned, bounded, retryable, and independent of request lifetime.
on_bundle_load() and on_app_deploy() keep distinct scopes.
service.readiness controls aggregate readiness only.
Public callers get a precise fact; operators retain diagnostics.
A slow app no longer makes a healthy processor look dead.
KDCube Journal · Entry № 24 · 18.08.2026