# `ReloadDriver` _(no submodules)_ ## Purpose `ReloadDriver` implements the §8.1 blue-green hot-reload primitive plus the §8.1a `RollbackJournal` integration. Each `reload(plugin, opts)` call captures the live module + manifest into a per-plugin ring buffer, imports the new module under a cache-busted ESM specifier via the `@suaveplan/api-loader` `ModuleResolver`, refuses to proceed if native bindings (`*.node`) are present in the dep graph, stages the new module in a shadow router slot, drives the lifecycle FSM through `stop → start` against the new module, runs the health probe, atomically flips the router's per-plugin version pointer to the new slot, and drains the old slot. On any failure the driver discards the shadow + auto-transitions the plugin to `rolled_back` by re-activating the most-recent journal entry. The driver is paired with `LifecycleDriver` (see `../lifecycle/`): the lifecycle driver owns the FSM transitions; the reload driver orchestrates the blue-green plumbing around them. Both share the same `EventBus` so the `plugin.rolled_back` event is the public boundary between them. ## Features - Blue-green primitive with atomic shadow → live flip + draining slot. - 503-window opt-in via `blueGreen: false` for environments that cannot host two module instances concurrently. - `RollbackJournal` ring buffer (default depth 3) layered on the in-memory `createRollbackJournal` from `@suaveplan/api-loader`; persisted subset written to the plugin ledger via the `RollbackPersistence` adapter. - Auto-rollback on `start_failed` during reload — the lifecycle driver is fed `rolled_back` without operator intervention. - Manual rollback via `rollback(plugin, toIndex)` — Stage 4's admin route `POST /admin/plugins/{name}/rollback?to=N` calls into this. - Native-binding guard via `walkNativeBindings()` + `assertNoNativeBindings()`. - Hydration from the persisted journal store after host restart — `hydrateFromPersistence(plugin)` re-populates the in-memory ring buffer from the ledger (the `moduleRef` is intentionally left `null` until a rollback re-imports). - `FileWatcher` (sibling feature) supplies dev-mode `--watch` triggers with a 500ms debounce, gated off in production. - OTel: `plugin.reload_total{plugin, trigger, outcome}` counter + `plugin.reload_duration_ms{plugin, trigger}` histogram on every reload. ## Usage: Basic ```ts import { EventBus } from "@suaveplan/events"; import { createHostInstruments, LifecycleDriver, ReloadDriver, } from "@suaveplan/api-host"; const reload = new ReloadDriver({ adapters: { blueGreen, persistence, resolver, health: { async probe() {} }, }, lifecycle, events, instruments, }); reload.recordInitialActivation(plugin, registered, initialHandle); await reload.reload(plugin, { manifest: nextManifest, module: nextModule, manifestHash: nextHash, trigger: "admin", }); ``` ## Usage: Advanced Wire the dev-mode file watcher and a custom drain timeout: ```ts import { FileWatcher, ReloadDriver } from "@suaveplan/api-host/hot-reload"; const reload = new ReloadDriver({ adapters, lifecycle, events, instruments, drainTimeoutMs: 5_000, forceTimeoutMs: 10_000, blueGreen: false, // 503-window mode for native-locks journalDepth: 5, }); const watcher = new FileWatcher({ fs: nodeFsWatch }); watcher.register("@acme/billing", "/opt/plugins/billing/dist", async (pluginName) => { await reload.reload(pluginName, await loadPluginFromDisk(pluginName, "filewatch")); }); ``` Operator-initiated rollback: ```ts const snap = await reload.rollback("@acme/billing", 0); // snap.journal[0] is now the active version; emits plugin.rolled_back. ``` ## API Reference - `class ReloadDriver` - `constructor(options: ReloadDriverOptions)` - `recordInitialActivation(plugin, registered, handle): void` - `reload(plugin, opts: ReloadOptions): Promise` - `rollback(plugin, toIndex: number): Promise` - `hydrateFromPersistence(plugin): Promise` - `snapshot(plugin): ReloadSnapshot` - `interface ReloadDriverOptions` — `adapters`, `lifecycle`, `events`, `instruments`, `journalDepth?`, `drainTimeoutMs?`, `forceTimeoutMs?`, `blueGreen?`, `now?`. - `interface ReloadAdapters` — `blueGreen`, `persistence`, `resolver`, `health`. - `interface ReloadOptions` — `manifest`, `module`, `manifestHash`, `trigger`. - `interface ReloadSnapshot` — `plugin`, `journal`, `inFlight`, `lastReloadAt`, `lastReloadTrigger`. - `class FileWatcher` — dev-mode watch primitive with debounced fire. ## Implementation Notes The `BlueGreenAdapter` interface is intentionally narrow — `stageShadow`, `flip`, `discardShadow`, `releaseDraining`. The production impl (Stage 4's `api-router`) backs these with `Atomics.store` on a SharedArrayBuffer; the Chunk-B reference impl used in tests is a plain map of `{ live, draining }` records. The flip is "atomic" in the design because at the router level a single integer write decides which handler runs — but `ReloadDriver` does not care which mechanism the adapter chooses. Reinstall criteria (`requiresReinstall(prev, next)`) raises `LIFECYCLE.REINSTALL_REQUIRED_VERSION_DOWNGRADE` on a downgrade — the caller propagates that as the reload's failure code. Forward-compatible upgrades that cross schemaVersion or add new scopes/events/jobs/secrets intentionally fall through to the lifecycle driver's normal start path so the host's audit log shows one reload event, not two. Persistence holds only the serialisable subset; `moduleRef` is a `WeakRef` that cannot be serialised. On restart the host re-imports the persisted specifier on demand — the first rollback after a restart pays one extra `import()` call, every subsequent one is free. The drain loop currently consults its own `AbortController` but Chunk D wires the lifecycle driver's `InflightTracker` into the resolver-dispose path so requests actually settle against the right module — Chunk B captures the contract + emits the OTel metric so downstream wiring is mechanical.