Initial Commit
This commit is contained in:
188
packages/api-stack/api/api-host/src/lifecycle/lifecycle.md
Normal file
188
packages/api-stack/api/api-host/src/lifecycle/lifecycle.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# `LifecycleDriver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`LifecycleDriver` is the runtime executor for the per-plugin lifecycle FSM
|
||||
defined in `@suaveplan/api-lifecycle`. It owns one record per registered
|
||||
plugin (manifest hash, plugin module from `definePlugin`, schemaVersion,
|
||||
current FSM state, retry trackers, hook log) and translates the
|
||||
author-facing verbs `install` / `start` / `stop` / `uninstall` / `crash` /
|
||||
`retryInstall` / `retryStart` / `clearRollback` / `forceUninstall` /
|
||||
`repair` into the trigger sequences that the `createPluginFsm` interpreter
|
||||
applies against the legal-transition table.
|
||||
|
||||
Each high-level operation dispatches the entry trigger, executes the
|
||||
side-effect descriptors captured on the FSM (the discriminated-union
|
||||
`SideEffectOp` from `@suaveplan/api-lifecycle/side-effects`), then
|
||||
dispatches the completion trigger. Failures along the way auto-dispatch
|
||||
the matching `*Failed` trigger and the FSM ends in the documented parked
|
||||
state (`install_failed`, `start_failed`, `uninstall_failed`). Auto-retry of
|
||||
`start_failed` follows the `@suaveplan/retry` exponential-backoff policy
|
||||
defined in `RETRY_POLICIES.startFailed`; crashed plugins auto-restart up
|
||||
to `RETRY_POLICIES.crashed.crashRestartMax` (default 3) with cooldown
|
||||
reset through `CrashBudgetTracker`.
|
||||
|
||||
## Features
|
||||
|
||||
- Full coverage of every `(state, trigger)` edge in
|
||||
`@suaveplan/api-lifecycle`'s `PLUGIN_TRANSITIONS` table.
|
||||
- Per-plugin mutex enforcement inherited from `createPluginFsm` — concurrent
|
||||
triggers queue FIFO; tests assert this dedup.
|
||||
- `SideEffectOp` execution against pluggable adapter surfaces
|
||||
(`RouterAdapter`, `JobSchedulerAdapter`, `TopicSubscriberAdapter`,
|
||||
`HealthMarkerAdapter`, `LedgerAdapter`, `MigrationRunner`, `RbacAdapter`,
|
||||
`CapabilityInjector`, `InflightTracker`).
|
||||
- Emits `plugin.lifecycle_transitions_total{plugin, from, to, trigger,
|
||||
outcome}` on every transition via Chunk-A's `HostInstruments`.
|
||||
- Emits typed events onto the host `EventBus<LifecycleEventMap>` —
|
||||
`plugin.installed`, `plugin.started`, `plugin.stopped`, `plugin.uninstalled`,
|
||||
`plugin.rolled_back`, `plugin.crashed`.
|
||||
- `snapshot(plugin)` renders an admin-friendly view with the FSM state, the
|
||||
most recent hook outcomes, and any captured `lastError`.
|
||||
- `rollback(plugin, cause)` is invoked by the `ReloadDriver` (see
|
||||
`../hot-reload/`) on a failed reload — drives `starting → rolled_back`
|
||||
without operator intervention.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import {
|
||||
createHostInstruments,
|
||||
LifecycleDriver,
|
||||
} from "@suaveplan/api-host";
|
||||
|
||||
const events = new EventBus();
|
||||
const instruments = createHostInstruments({
|
||||
hostId: "h1",
|
||||
hostVersion: "0.1.0",
|
||||
});
|
||||
|
||||
const driver = new LifecycleDriver({
|
||||
adapters: {
|
||||
router: { mount() {}, unmount() {} },
|
||||
jobs: { register() {}, pause() {}, unpause() {} },
|
||||
topics: { subscribe() {}, unsubscribe() {} },
|
||||
health: { mark() {} },
|
||||
ledger: {
|
||||
async write() {},
|
||||
async drop() {},
|
||||
async read() { return null; },
|
||||
},
|
||||
migrations: { async runUpTo() {} },
|
||||
rbac: { registerScopes() {}, dropScopes() {} },
|
||||
capabilities: {
|
||||
inject: () => ({}) as never,
|
||||
dispose: async () => {},
|
||||
},
|
||||
inflight: {
|
||||
async drain() { return 0; },
|
||||
forceCancel: () => 0,
|
||||
},
|
||||
},
|
||||
events,
|
||||
instruments,
|
||||
});
|
||||
|
||||
driver.register({
|
||||
module: pluginModule,
|
||||
manifest,
|
||||
manifestHash: "sha256:...",
|
||||
specifier: "file:///opt/plugin/dist/index.js?v=1",
|
||||
schemaVersion: 1,
|
||||
});
|
||||
|
||||
await driver.install(pluginModule.name);
|
||||
await driver.start(pluginModule.name);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Customise the per-plugin mutex (e.g. backed by a distributed lock) and the
|
||||
crash-restart cap:
|
||||
|
||||
```ts
|
||||
import { LifecycleDriver } from "@suaveplan/api-host/lifecycle";
|
||||
import type { PluginMutex } from "@suaveplan/api-lifecycle";
|
||||
|
||||
class RedisPluginMutex implements PluginMutex {
|
||||
async acquire(plugin: string) { /* … */ }
|
||||
tryAcquire(plugin: string) { /* … */ }
|
||||
}
|
||||
|
||||
const driver = new LifecycleDriver({
|
||||
adapters,
|
||||
events,
|
||||
instruments,
|
||||
mutex: new RedisPluginMutex(),
|
||||
crashRestartMax: 5,
|
||||
sleep: () => Promise.resolve(),
|
||||
});
|
||||
|
||||
driver.events.on("plugin.rolled_back", ({ plugin, from, to }) => {
|
||||
operator.notify(`${plugin} rolled back ${from} → ${to}`);
|
||||
});
|
||||
|
||||
await driver.crash(plugin.name, new Error("worker exit code 137"));
|
||||
const snap = driver.snapshot(plugin.name);
|
||||
if (snap.lastError !== null) operator.escalate(snap);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class LifecycleDriver`
|
||||
- `constructor(options: LifecycleDriverOptions)`
|
||||
- `register(record: RegisteredPlugin): void`
|
||||
- `update(record: RegisteredPlugin): void`
|
||||
- `install(plugin: string): Promise<PluginState>`
|
||||
- `start(plugin: string): Promise<PluginState>`
|
||||
- `stop(plugin: string): Promise<PluginState>`
|
||||
- `uninstall(plugin: string): Promise<PluginState>`
|
||||
- `crash(plugin: string, cause: unknown): Promise<PluginState>`
|
||||
- `retryInstall(plugin: string): Promise<PluginState>`
|
||||
- `retryStart(plugin: string): Promise<PluginState>`
|
||||
- `clearRollback(plugin: string): Promise<PluginState>`
|
||||
- `forceUninstall(plugin: string): Promise<PluginState>`
|
||||
- `repair(plugin: string): Promise<PluginState>`
|
||||
- `rollback(plugin: string, cause: unknown): Promise<PluginState>`
|
||||
- `snapshot(plugin: string): LifecycleSnapshot`
|
||||
- `names(): readonly string[]`
|
||||
- `interface LifecycleDriverOptions` — `adapters`, `events`, `instruments`,
|
||||
`mutex?`, `crashRestartMax?`, `sleep?`
|
||||
- `interface LifecycleEventMap` — `plugin.installed`, `plugin.started`,
|
||||
`plugin.stopped`, `plugin.uninstalled`, `plugin.rolled_back`,
|
||||
`plugin.crashed`.
|
||||
- Adapter surfaces from `lifecycle.types.ts` — `RouterAdapter`,
|
||||
`JobSchedulerAdapter`, `TopicSubscriberAdapter`, `HealthMarkerAdapter`,
|
||||
`LedgerAdapter`, `MigrationRunner`, `RbacAdapter`, `CapabilityInjector`,
|
||||
`InflightTracker`, `LifecycleAdapters`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The driver intentionally delegates illegal-transition detection to the
|
||||
underlying `PluginFsm` — every operator verb maps to one entry trigger;
|
||||
attempting `start()` from `crashed` therefore raises a
|
||||
`LifecycleError(ILLEGAL_TRANSITION)` from the FSM rather than a duplicate
|
||||
guard in the driver. This keeps the source-of-truth for legal transitions
|
||||
in The contract package.
|
||||
|
||||
Side-effect ops `validate-manifest`, `resolve-deps`, `acquire-db-connection`,
|
||||
`complete-otel-span`, `apply-data-policy`, and `rollback-to-prior-module`
|
||||
are documented but execute as no-ops inside the driver: manifest validation
|
||||
ran at registration time, dep-graph resolution is computed by
|
||||
`api-manifest/dep-graph` before registration, the ledger adapter holds
|
||||
the DB connection, OTel span completion is captured via the lifecycle
|
||||
counter, the data policy is applied by the next `drop-ledger` op, and the
|
||||
rollback module switch is performed by the `ReloadDriver` before the
|
||||
`rolled_back` transition fires. Keeping the no-op branches lets Stage-3
|
||||
wire concrete behaviour without revising the FSM definition.
|
||||
|
||||
Auto-retry of `start_failed` uses `backoffDelay(base, factor, attempt, max)`
|
||||
locally rather than `@suaveplan/retry`'s `retry()` engine because the
|
||||
driver needs to inter-leave the FSM transitions between attempts —
|
||||
`@suaveplan/retry`'s engine is single-call-with-retries semantics. The
|
||||
underlying delay strategy is identical to `RETRY_POLICIES.startFailed.delay`.
|
||||
Reference in New Issue
Block a user