7.3 KiB
LifecycleDriver
(no submodules)
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'sPLUGIN_TRANSITIONStable. - Per-plugin mutex enforcement inherited from
createPluginFsm— concurrent triggers queue FIFO; tests assert this dedup. SideEffectOpexecution 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'sHostInstruments. - 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 capturedlastError.rollback(plugin, cause)is invoked by theReloadDriver(see../hot-reload/) on a failed reload — drivesstarting → rolled_backwithout operator intervention.
Usage: Basic
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:
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 LifecycleDriverconstructor(options: LifecycleDriverOptions)register(record: RegisteredPlugin): voidupdate(record: RegisteredPlugin): voidinstall(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): LifecycleSnapshotnames(): 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.