Drain document lifecycle events through a durable processor
Before this commit `createDocumentWithLifecycle` enqueued ledger rows
inside the save transaction but consumed them from an in-memory
`pendingSideEffects` array straight after commit. A crash between the
commit and the drain stranded rows as `pending` forever; a crash mid
handler left them as `running`; and there was no retry budget.
The new `DocumentLifecycleProcessor` is the single drainer. It:
- claims work via `claimPendingBatch`, an atomic `UPDATE … RETURNING`
that flips rows from `pending` to `running` while incrementing
`attempts`, so two drainers cannot race onto the same row;
- dispatches each row through a handler registered by `hookType`,
parsing the frozen `payload_json` back into the call arguments;
- records `succeeded` / `failed` / `retryable` outcomes through the
ledger, so a transient failure stays on the queue until the attempt
cap expires, after which it is marked `failed` for operators;
- reclaims rows stuck in `running` past `staleRunningMs` back to
`pending` on every sweep tick plus at startup, so a crashed process
never leaves a row orphaned;
- runs a periodic sweep so every document — including those whose
immediate post-commit drain failed — is eventually advanced.
`createDocumentWithLifecycle` now only enqueues and then calls
`lifecycleProcessor.drainForDocument` for the immediate happy-path pass.
Handlers live in `document-lifecycle-handlers.ts` so the processor stays
free of service-graph knowledge. The processor is constructed in
`bootstrap-application`, started there, and stopped on `before-quit`.
This commit is contained in:
1 parent
ac9be2b639
commit
b1d1fbc54f
11 files changed
+487
-99
No files matched your search
@@ -7,6 +7,11 @@ import type { createServiceGraph } from "./service-graph";
|
||||
import type { createIpcSupport } from "./ipc-support";
|
||||
import type { createThreadIntegration } from "./thread-integration";
|
||||
import type { createCheckpointEngine } from "./checkpoint-engine";
|
||||
import {
|
||||
DocumentLifecycleProcessor,
|
||||
makeLifecycleProcessorOptions,
|
||||
} from "./document-lifecycle-processor";
|
||||
import { registerLifecycleHandlers } from "./document-lifecycle-handlers";
|
||||
import { prepareLogger } from "./bootstrap/prepare-logger";
|
||||
import { prepareDatabase } from "./bootstrap/prepare-database";
|
||||
import { prepareServices } from "./bootstrap/prepare-services";
|
||||
@@ -39,6 +44,7 @@ interface BootstrapApplicationDependencies {
|
||||
ipcSupport: ReturnType<typeof createIpcSupport>;
|
||||
threadIntegration: ReturnType<typeof createThreadIntegration>;
|
||||
checkpointEngine: ReturnType<typeof createCheckpointEngine>;
|
||||
lifecycleProcessor: DocumentLifecycleProcessor;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
@@ -82,11 +88,24 @@ export async function bootstrapApplication(deps: BootstrapApplicationDependencie
|
||||
return orchestration.wakeAgentIfPossible(agentId, companyId, trigger);
|
||||
};
|
||||
|
||||
const lifecycleProcessor = new DocumentLifecycleProcessor(
|
||||
makeLifecycleProcessorOptions({ db, logger: log }),
|
||||
);
|
||||
registerLifecycleHandlers(lifecycleProcessor, {
|
||||
dispatchAutomation: (trigger, ctx) => {
|
||||
const automation = serviceGraph.getAutomationService();
|
||||
if (!automation) throw new Error("Automation service is not initialized.");
|
||||
automation.dispatchAutomation(trigger, ctx);
|
||||
},
|
||||
wakeAgentIfPossible,
|
||||
});
|
||||
|
||||
const apiServer = wireApiServer({
|
||||
db,
|
||||
serviceGraph,
|
||||
threadIntegration,
|
||||
checkpointEngine,
|
||||
lifecycleProcessor,
|
||||
emitEvent,
|
||||
notify,
|
||||
publishDomainChanged,
|
||||
@@ -97,7 +116,18 @@ export async function bootstrapApplication(deps: BootstrapApplicationDependencie
|
||||
|
||||
apiServerRef.current = apiServer;
|
||||
await apiServer.start();
|
||||
deps.onRuntimeReady?.({ db, apiServer, serviceGraph, ipcSupport, threadIntegration, checkpointEngine });
|
||||
deps.onRuntimeReady?.({
|
||||
db,
|
||||
apiServer,
|
||||
serviceGraph,
|
||||
ipcSupport,
|
||||
threadIntegration,
|
||||
checkpointEngine,
|
||||
lifecycleProcessor,
|
||||
});
|
||||
|
||||
lifecycleProcessor.start();
|
||||
app.on("before-quit", () => lifecycleProcessor.stop());
|
||||
|
||||
if (apiServer && serviceGraph.getBrowserActionService()) {
|
||||
apiServer.setBrowserActionHandler(async (actionId, companyId) => {
|
||||
|
||||
Reference in new issue
Block a user