@powerhousedao/switchboard 6.2.3-dev.43 → 6.2.3-dev.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tsbuild/test/durable-node-fs-pg16.test.d.ts +2 -0
- package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
- package/.tsbuild/test/observability-signals.test.d.ts +2 -0
- package/.tsbuild/test/observability-signals.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
- package/.tsbuild/test/pglite-preflight.test.d.ts.map +1 -0
- package/.tsbuild/test/snapshot-writer.d.ts +18 -0
- package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
- package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
- package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
- package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +43 -0
- package/dist/index.mjs +42 -9
- package/dist/index.mjs.map +1 -1
- package/dist/{server-CMAxYG6D.mjs → server-Dvw8_NeM.mjs} +271 -79
- package/dist/server-Dvw8_NeM.mjs.map +1 -0
- package/dist/server.d.mts +3 -1
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +15 -16
- package/test/boot-unsupported-stored-documents.test.ts +5 -5
- package/test/durable-node-fs-pg16.test.ts +80 -0
- package/test/fatal-shutdown.test.ts +33 -0
- package/test/observability-signals.test.ts +131 -0
- package/test/pglite-dialect.test.ts +28 -0
- package/test/pglite-preflight.test.ts +313 -0
- package/test/pglite-version.test.ts +9 -1
- package/test/snapshot-writer.ts +94 -0
- package/test/worker-pool.test.ts +27 -1
- package/test/workflow/resolvers.test.ts +26 -0
- package/test/workflow-reactor-access.test.ts +384 -0
- package/test/workflow-reactor-enforcement.test.ts +805 -0
- package/test/workflow-runtime.test.ts +53 -0
- package/tsconfig.json +0 -3
- package/dist/server-CMAxYG6D.mjs.map +0 -1
package/dist/server.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ILogger } from "document-model";
|
|
2
|
-
import { ChannelScheme, IDocumentModelLoader, IReactorClient, InProcessReactorClientModule, JwtHandler, ReactorBuilder, ReactorClientBuilder, ReactorFeatureFlags, SignerConfig, UnsupportedStoredDocuments, UnsupportedStoredProtocolError } from "@powerhousedao/reactor";
|
|
2
|
+
import { ChannelScheme, IDocumentModelLoader, IReactorClient, InProcessReactorClientModule, JwtHandler, ModelManifestEntry, ReactorBuilder, ReactorClientBuilder, ReactorFeatureFlags, SignerConfig, UnsupportedStoredDocuments, UnsupportedStoredProtocolError } from "@powerhousedao/reactor";
|
|
3
3
|
import { AttachmentReferenceProjectionCapability } from "@powerhousedao/reactor-api";
|
|
4
4
|
import { IAttachmentService } from "@powerhousedao/reactor-attachments";
|
|
5
5
|
import { IRenown } from "@renown/sdk/node";
|
|
@@ -206,6 +206,8 @@ export type SwitchboardReactor = {
|
|
|
206
206
|
workflowTriggers?: WorkflowTriggersCapability;
|
|
207
207
|
/** Whether workflows are on, after the option, PH_WORKFLOWS_ENABLED and the config file. */
|
|
208
208
|
workflowsEnabled: boolean;
|
|
209
|
+
/** The boot list of importable models workers and workflow pieces load; empty when none are resolved. */
|
|
210
|
+
modelManifest(): ModelManifestEntry[];
|
|
209
211
|
/** Present when the privacy add-on runs: its erasure service. */
|
|
210
212
|
privacy?: {
|
|
211
213
|
erasure: IErasureService;
|
package/dist/server.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/privacy.mts","../src/workflow-runtime.mts","../src/types.ts","../src/builder-defaults.mts","../src/unsupported-stored-documents.mts","../src/server.mts"],"mappings":";;;;;;;;;;;;KAyBY;;EAEV;;EAEA;;EAEA,SAAS;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/privacy.mts","../src/workflow-runtime.mts","../src/types.ts","../src/builder-defaults.mts","../src/unsupported-stored-documents.mts","../src/server.mts"],"mappings":";;;;;;;;;;;;KAyBY;;EAEV;;EAEA;;EAEA,SAAS;;;;;;KC8BC;EACN;;EAEA;EACA;;;;;;;;;;YC3CM;;YAKA,wBAAwB;EAClC,eAAe;;YAGL;EACV;EACA;EACA;;YAGU;;EAEV;;;;;EAKA;;EAGA;;YAGU;;;;;;;;;;;;;;;;;;;;;EAqBV,UAAU;;;;;;;;EAQV;;;;;;;EAOA;EACA;EACA;;;;;;EAMA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;IAEM;IACA;;;;;;;EASN,WAAW;;EAEX;;;EAGA;;;EAGA;IACE;;;EAGF,UAAU;EACV,kBAAkB;EAClB;EACA;;;;;;EAMA;EACA,SAAS;;;;;;;;;;EAUT;;;;;;;;;;EAUA;;;;;;;;;EASA;IACE;IACA;IACA;;;;;;;;;;;EAWF;IACE;IACA;;;;;;;;;EASF,6BAA6B;;YAGnB;EACV;EACA,SAAS;;EAET,mBAAmB;;EAEnB,+BAA+B;;;EAG/B,mBAAmB;;EAEnB;;EAEA,iBAAiB;;EAEjB;IAAY,SAAS;;;EAErB;;EAEA,QAAQ;;;;;EAKR;;;;;;;;;;;;;EAaA,gBAAgB;;;;KC/MN;EAIV,iBAAiB;;EAEjB,mBAAmB;;EAEnB;;;;;;EAMA,gBAAgB;;EAEhB;;EAEA;IACE;IACA,eAAe,QAAQ;;;EAGzB,sBAAsB;EACtB,SAAS;;;;;;EAMT,SAAS;;;;;EAKT,cAAc,KAAK;;EAEnB,wBAAwB;;wBAsBV,gCACd,gBAAgB,gBAChB,eAAe,sBACf,UAAS;;;;wBC3EK,kCACd,OAAO,wCACP,KAAK,OAAO,aACX;;qBAaU,oCAAoC;WACtC;IAAqB;IAAkB;;WACvC;EAET,YAAY,OAAO;SAcZ,QAAQ,iBAAiB,SAAS;;;;;;;;;wBCoK3B,gBAAgB,eAAe;;wBAoI/B,8BACd,SAAS,KAAK,uDACd,oBACA,QAAQ;EACL;EAAmB,YAAY;;;;;;;;;;;;;;;;;;;qBAm7BvB,mBAAgB,UAClB,uBACR,QAAQ"}
|
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as resolveUnsupportedStoredDocuments, i as StoredDocumentsRefusedError, n as isPortAvailable, o as applySwitchboardReactorDefaults, r as startSwitchboard, t as deriveAttachmentServiceConfig } from "./server-
|
|
2
|
+
import { a as resolveUnsupportedStoredDocuments, i as StoredDocumentsRefusedError, n as isPortAvailable, o as applySwitchboardReactorDefaults, r as startSwitchboard, t as deriveAttachmentServiceConfig } from "./server-Dvw8_NeM.mjs";
|
|
3
3
|
export { StoredDocumentsRefusedError, applySwitchboardReactorDefaults, deriveAttachmentServiceConfig, isPortAvailable, resolveUnsupportedStoredDocuments, startSwitchboard };
|
|
4
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
5
|
-
//# debugId=
|
|
4
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b5bdc179-e9e6-50c3-a581-a82e554e7130")}catch(e){}}();
|
|
5
|
+
//# debugId=b5bdc179-e9e6-50c3-a581-a82e554e7130
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/switchboard",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.2.3-dev.
|
|
4
|
+
"version": "6.2.3-dev.45",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@openfeature/core": "1.9.1",
|
|
42
42
|
"@openfeature/env-var-provider": "0.3.1",
|
|
43
43
|
"@openfeature/server-sdk": "1.19.0",
|
|
44
|
-
"@powerhousedao/config": "6.2.3-dev.
|
|
44
|
+
"@powerhousedao/config": "6.2.3-dev.45",
|
|
45
45
|
"@opentelemetry/api": "^1.9.0",
|
|
46
46
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
|
|
47
47
|
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
|
|
@@ -55,23 +55,22 @@
|
|
|
55
55
|
"@opentelemetry/sdk-node": "^0.218.0",
|
|
56
56
|
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
57
57
|
"@opentelemetry/semantic-conventions": "^1.41.1",
|
|
58
|
-
"@powerhousedao/opentelemetry-instrumentation-reactor": "6.2.3-dev.
|
|
59
|
-
"@powerhousedao/reactor": "6.2.3-dev.
|
|
60
|
-
"@powerhousedao/shared": "6.2.3-dev.
|
|
61
|
-
"@powerhousedao/vetra": "6.2.3-dev.
|
|
62
|
-
"@powerhousedao/reactor-api": "6.2.3-dev.
|
|
63
|
-
"@powerhousedao/reactor-attachments": "6.2.3-dev.
|
|
64
|
-
"@powerhousedao/reactor-drive": "6.2.3-dev.
|
|
65
|
-
"@powerhousedao/reactor-group": "6.2.3-dev.
|
|
66
|
-
"@powerhousedao/reactor-privacy": "6.2.3-dev.
|
|
67
|
-
"@powerhousedao/reactor-workflow": "6.2.3-dev.
|
|
68
|
-
"@powerhousedao/workflow": "6.2.3-dev.
|
|
69
|
-
"@powerhousedao/pglite-fs": "6.2.3-dev.43",
|
|
58
|
+
"@powerhousedao/opentelemetry-instrumentation-reactor": "6.2.3-dev.45",
|
|
59
|
+
"@powerhousedao/reactor": "6.2.3-dev.45",
|
|
60
|
+
"@powerhousedao/shared": "6.2.3-dev.45",
|
|
61
|
+
"@powerhousedao/vetra": "6.2.3-dev.45",
|
|
62
|
+
"@powerhousedao/reactor-api": "6.2.3-dev.45",
|
|
63
|
+
"@powerhousedao/reactor-attachments": "6.2.3-dev.45",
|
|
64
|
+
"@powerhousedao/reactor-drive": "6.2.3-dev.45",
|
|
65
|
+
"@powerhousedao/reactor-group": "6.2.3-dev.45",
|
|
66
|
+
"@powerhousedao/reactor-privacy": "6.2.3-dev.45",
|
|
67
|
+
"@powerhousedao/reactor-workflow": "6.2.3-dev.45",
|
|
68
|
+
"@powerhousedao/workflow": "6.2.3-dev.45",
|
|
70
69
|
"@pyroscope/nodejs": "^0.4.5",
|
|
71
|
-
"@renown/sdk": "6.2.3-dev.
|
|
70
|
+
"@renown/sdk": "6.2.3-dev.45",
|
|
72
71
|
"@sentry/node": "^10.53.1",
|
|
73
72
|
"@sentry/opentelemetry": "^10.53.1",
|
|
74
|
-
"document-model": "6.2.3-dev.
|
|
73
|
+
"document-model": "6.2.3-dev.45",
|
|
75
74
|
"dotenv": "^16.4.7",
|
|
76
75
|
"express": "^4.22.1",
|
|
77
76
|
"graphql": "^16",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import {
|
|
2
|
+
import { NodeFS } from "@electric-sql/pglite/nodefs";
|
|
3
3
|
import {
|
|
4
4
|
JobStatus,
|
|
5
5
|
ReactorBuilder,
|
|
@@ -65,9 +65,7 @@ function stubLogger(): StubLogger {
|
|
|
65
65
|
/** A reactor store holding one drive created at base-reducer 7. */
|
|
66
66
|
async function seedStore(dir: string): Promise<void> {
|
|
67
67
|
const db = new Kysely<Database>({
|
|
68
|
-
dialect: new ClosablePGliteDialect(
|
|
69
|
-
new PGlite({ fs: new AtomicNodeFs(dir) }),
|
|
70
|
-
),
|
|
68
|
+
dialect: new ClosablePGliteDialect(new PGlite({ fs: new NodeFS(dir) })),
|
|
71
69
|
});
|
|
72
70
|
const module = await new ReactorBuilder()
|
|
73
71
|
.withKysely(db)
|
|
@@ -89,7 +87,7 @@ async function seedStore(dir: string): Promise<void> {
|
|
|
89
87
|
const { status } = await module.reactor.getJobStatus(created.id);
|
|
90
88
|
return status === JobStatus.READ_READY || status === JobStatus.FAILED;
|
|
91
89
|
},
|
|
92
|
-
//
|
|
90
|
+
// PGlite is several times slower on the Windows runner.
|
|
93
91
|
{ timeout: BOOT_TIMEOUT, interval: 5 },
|
|
94
92
|
);
|
|
95
93
|
expect((await module.reactor.getJobStatus(created.id)).status).toBe(
|
|
@@ -97,6 +95,8 @@ async function seedStore(dir: string): Promise<void> {
|
|
|
97
95
|
);
|
|
98
96
|
await module.reactor.kill().completed;
|
|
99
97
|
await db.destroy();
|
|
98
|
+
// PGlite leaves its lockfile behind after a clean close.
|
|
99
|
+
await rm(join(dir, "postmaster.pid"), { force: true });
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// PGlite boots are several times slower on Windows runners.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createDurableNodeFs } from "@powerhousedao/reactor-api/pglite-node";
|
|
2
|
+
import nodeFs, { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { loadNodeFsClass, loadPGliteModule } from "../src/pglite-version.js";
|
|
7
|
+
|
|
8
|
+
// A fresh initdb takes ~10 s under suite load; the Windows runner is slower.
|
|
9
|
+
const BOOT = 180_000;
|
|
10
|
+
const COMMITS = 10;
|
|
11
|
+
const RM = { recursive: true, force: true, maxRetries: 10 } as const;
|
|
12
|
+
|
|
13
|
+
describe("durable NodeFS over pglite-legacy-02 (PG16)", () => {
|
|
14
|
+
const tempDirs: string[] = [];
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
for (const dir of tempDirs.splice(0)) {
|
|
18
|
+
await fs.rm(dir, RM);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it(
|
|
23
|
+
"opens a PG16 dir over pglite-legacy-02 with the durable fs",
|
|
24
|
+
async () => {
|
|
25
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "durable-pg16-"));
|
|
26
|
+
tempDirs.push(dir);
|
|
27
|
+
const counts = { fsync: 0, fdatasync: 0 };
|
|
28
|
+
const hostFs = {
|
|
29
|
+
fsyncSync: (fd: number) => {
|
|
30
|
+
counts.fsync++;
|
|
31
|
+
nodeFs.fsyncSync(fd);
|
|
32
|
+
},
|
|
33
|
+
fdatasyncSync: (fd: number) => {
|
|
34
|
+
counts.fdatasync++;
|
|
35
|
+
nodeFs.fdatasyncSync(fd);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const [{ PGlite }, NodeFS] = await Promise.all([
|
|
39
|
+
loadPGliteModule(16),
|
|
40
|
+
loadNodeFsClass(16),
|
|
41
|
+
]);
|
|
42
|
+
const pg = new PGlite({
|
|
43
|
+
fs: createDurableNodeFs(NodeFS, dir, {
|
|
44
|
+
hostFs,
|
|
45
|
+
maintenanceIntervalMs: 0,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
await pg.waitReady;
|
|
50
|
+
const version = await pg.query<{ v: string }>(
|
|
51
|
+
"SELECT current_setting('server_version_num') AS v",
|
|
52
|
+
);
|
|
53
|
+
expect(version.rows[0].v.startsWith("16")).toBe(true);
|
|
54
|
+
await pg.exec("CREATE TABLE t (v int)");
|
|
55
|
+
const before = counts.fdatasync;
|
|
56
|
+
for (let i = 0; i < COMMITS; i++) {
|
|
57
|
+
await pg.exec(`INSERT INTO t VALUES (${i})`);
|
|
58
|
+
}
|
|
59
|
+
const perCommit = counts.fdatasync - before;
|
|
60
|
+
const fsyncBefore = counts.fsync;
|
|
61
|
+
await pg.exec("CHECKPOINT");
|
|
62
|
+
const checkpointSyncs = counts.fsync - fsyncBefore;
|
|
63
|
+
console.info(
|
|
64
|
+
`pglite-legacy-02: ${perCommit} host fdatasync over ${COMMITS} commits, ${checkpointSyncs} host fsync on CHECKPOINT, ${fsyncBefore} host fsync since open`,
|
|
65
|
+
);
|
|
66
|
+
// Pins decision 5's outcome on 0.2.17: WAL goes through
|
|
67
|
+
// open_datasync, which never reaches the fdatasync hook, so commits
|
|
68
|
+
// are not synced; the stream-op patch does take (CHECKPOINT syncs).
|
|
69
|
+
expect(perCommit).toBe(0);
|
|
70
|
+
expect(checkpointSyncs).toBeGreaterThan(0);
|
|
71
|
+
} finally {
|
|
72
|
+
await pg.close();
|
|
73
|
+
}
|
|
74
|
+
expect(
|
|
75
|
+
(await fs.readFile(path.join(dir, "PG_VERSION"), "utf8")).trim(),
|
|
76
|
+
).toBe("16");
|
|
77
|
+
},
|
|
78
|
+
BOOT,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
@@ -94,6 +94,39 @@ describe("installFatalErrorShutdown", () => {
|
|
|
94
94
|
expect(proc.exit).not.toHaveBeenCalled();
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it("still reaches exit when the logger itself throws EPIPE inside onFatal", () => {
|
|
98
|
+
vi.useFakeTimers();
|
|
99
|
+
const proc = makeProc();
|
|
100
|
+
const onSigterm = vi.fn();
|
|
101
|
+
proc.on("SIGTERM", onSigterm);
|
|
102
|
+
const error = vi.fn(() => {
|
|
103
|
+
throw Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
|
|
104
|
+
});
|
|
105
|
+
const throwingLogger = { error } as unknown as Parameters<
|
|
106
|
+
typeof installFatalErrorShutdown
|
|
107
|
+
>[0];
|
|
108
|
+
installFatalErrorShutdown(throwingLogger, proc as never);
|
|
109
|
+
|
|
110
|
+
// Without the guard the EPIPE propagates out of the uncaughtException
|
|
111
|
+
// listener, which in a real process re-enters fatal handling instead of
|
|
112
|
+
// running the shutdown.
|
|
113
|
+
expect(() =>
|
|
114
|
+
proc.emit("uncaughtException", new Error("boom")),
|
|
115
|
+
).not.toThrow();
|
|
116
|
+
|
|
117
|
+
expect(proc.kill).toHaveBeenCalledOnce();
|
|
118
|
+
expect(onSigterm).toHaveBeenCalledOnce();
|
|
119
|
+
expect(proc.exitCode).toBe(1);
|
|
120
|
+
|
|
121
|
+
vi.advanceTimersByTime(15_000);
|
|
122
|
+
|
|
123
|
+
expect(proc.exit).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(proc.exit).toHaveBeenCalledWith(1);
|
|
125
|
+
// Exactly the two guarded log attempts: the fatal report and the
|
|
126
|
+
// forced-exit notice. A re-entered handler would log again.
|
|
127
|
+
expect(error).toHaveBeenCalledTimes(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
97
130
|
it("routes a triggered fatal error through SIGTERM with exit code 1", () => {
|
|
98
131
|
const proc = makeProc();
|
|
99
132
|
const onSigterm = vi.fn();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { installFatalErrorShutdown } from "../src/fatal-shutdown.mjs";
|
|
4
|
+
import { installObservabilitySignalHandlers } from "../src/observability-signals.mjs";
|
|
5
|
+
|
|
6
|
+
function makeProc() {
|
|
7
|
+
const emitter = new EventEmitter();
|
|
8
|
+
const proc = Object.assign(emitter, {
|
|
9
|
+
pid: 1234,
|
|
10
|
+
exitCode: undefined as number | undefined,
|
|
11
|
+
kill: vi.fn((_pid: number, signal: string) => {
|
|
12
|
+
emitter.emit(signal);
|
|
13
|
+
return true;
|
|
14
|
+
}),
|
|
15
|
+
exit: vi.fn(),
|
|
16
|
+
});
|
|
17
|
+
return proc;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const logger = {
|
|
21
|
+
error: vi.fn(),
|
|
22
|
+
} as unknown as Parameters<typeof installFatalErrorShutdown>[0];
|
|
23
|
+
|
|
24
|
+
/** Lets the finally callback behind the flush promise run. */
|
|
25
|
+
async function settleMicrotasks(): Promise<void> {
|
|
26
|
+
await Promise.resolve();
|
|
27
|
+
await Promise.resolve();
|
|
28
|
+
await Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("installObservabilitySignalHandlers", () => {
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.useRealTimers();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each(["SIGINT", "SIGTERM"])(
|
|
37
|
+
"exits 0 after the flush on an ordinary %s",
|
|
38
|
+
async (signal) => {
|
|
39
|
+
const proc = makeProc();
|
|
40
|
+
const flush = vi.fn(() => Promise.resolve());
|
|
41
|
+
installObservabilitySignalHandlers(flush, proc as never);
|
|
42
|
+
|
|
43
|
+
proc.emit(signal);
|
|
44
|
+
await settleMicrotasks();
|
|
45
|
+
|
|
46
|
+
expect(flush).toHaveBeenCalledOnce();
|
|
47
|
+
expect(proc.exit).toHaveBeenCalledExactlyOnceWith(0);
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
it("does not exit before the flush settles", async () => {
|
|
52
|
+
const proc = makeProc();
|
|
53
|
+
let finish: () => void = () => undefined;
|
|
54
|
+
const flush = vi.fn(
|
|
55
|
+
() => new Promise<void>((resolve) => (finish = resolve)),
|
|
56
|
+
);
|
|
57
|
+
installObservabilitySignalHandlers(flush, proc as never);
|
|
58
|
+
|
|
59
|
+
proc.emit("SIGTERM");
|
|
60
|
+
await settleMicrotasks();
|
|
61
|
+
expect(proc.exit).not.toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
finish();
|
|
64
|
+
await settleMicrotasks();
|
|
65
|
+
expect(proc.exit).toHaveBeenCalledExactlyOnceWith(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps exit code 1 when a fatal error raised the SIGTERM", async () => {
|
|
69
|
+
vi.useFakeTimers();
|
|
70
|
+
const proc = makeProc();
|
|
71
|
+
const flush = vi.fn(() => Promise.resolve());
|
|
72
|
+
// Production order: observability's handlers install at module load,
|
|
73
|
+
// before the fatal handlers.
|
|
74
|
+
installObservabilitySignalHandlers(flush, proc as never);
|
|
75
|
+
installFatalErrorShutdown(logger, proc as never);
|
|
76
|
+
|
|
77
|
+
proc.emit("uncaughtException", new Error("boom"));
|
|
78
|
+
expect(proc.exitCode).toBe(1);
|
|
79
|
+
await settleMicrotasks();
|
|
80
|
+
|
|
81
|
+
// The flush ran, but observability never lowered the code to 0; the
|
|
82
|
+
// process is still alive inside the fatal path's drain budget.
|
|
83
|
+
expect(flush).toHaveBeenCalledOnce();
|
|
84
|
+
expect(proc.exit).not.toHaveBeenCalled();
|
|
85
|
+
|
|
86
|
+
// Fatal-shutdown's backstop owns the exit and preserves code 1.
|
|
87
|
+
vi.advanceTimersByTime(15_000);
|
|
88
|
+
expect(proc.exit).toHaveBeenCalledExactlyOnceWith(1);
|
|
89
|
+
expect(proc.exit).not.toHaveBeenCalledWith(0);
|
|
90
|
+
expect(proc.exitCode).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("leaves the fatal drain budget intact even when the flush is slow", async () => {
|
|
94
|
+
vi.useFakeTimers();
|
|
95
|
+
const proc = makeProc();
|
|
96
|
+
let finish: () => void = () => undefined;
|
|
97
|
+
const flush = vi.fn(
|
|
98
|
+
() => new Promise<void>((resolve) => (finish = resolve)),
|
|
99
|
+
);
|
|
100
|
+
installObservabilitySignalHandlers(flush, proc as never);
|
|
101
|
+
installFatalErrorShutdown(logger, proc as never);
|
|
102
|
+
|
|
103
|
+
proc.emit("uncaughtException", new Error("boom"));
|
|
104
|
+
|
|
105
|
+
// The flush gives up at its own ~5s budget (simulated by resolving it
|
|
106
|
+
// here); that settling must not force an exit ahead of the drain.
|
|
107
|
+
vi.advanceTimersByTime(5_000);
|
|
108
|
+
finish();
|
|
109
|
+
await settleMicrotasks();
|
|
110
|
+
expect(proc.exit).not.toHaveBeenCalled();
|
|
111
|
+
|
|
112
|
+
// Only the 15s fatal backstop ends the process, with code 1.
|
|
113
|
+
vi.advanceTimersByTime(9_999);
|
|
114
|
+
expect(proc.exit).not.toHaveBeenCalled();
|
|
115
|
+
vi.advanceTimersByTime(1);
|
|
116
|
+
expect(proc.exit).toHaveBeenCalledExactlyOnceWith(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("respects a non-zero exit code set by any other module", async () => {
|
|
120
|
+
const proc = makeProc();
|
|
121
|
+
proc.exitCode = 1;
|
|
122
|
+
const flush = vi.fn(() => Promise.resolve());
|
|
123
|
+
installObservabilitySignalHandlers(flush, proc as never);
|
|
124
|
+
|
|
125
|
+
proc.emit("SIGTERM");
|
|
126
|
+
await settleMicrotasks();
|
|
127
|
+
|
|
128
|
+
expect(flush).toHaveBeenCalledOnce();
|
|
129
|
+
expect(proc.exit).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -26,6 +26,34 @@ describe("ClosablePGliteDialect", () => {
|
|
|
26
26
|
expect(pglite.closed).toBe(true);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
it("tolerates a close that rejects because the runtime aborted", async () => {
|
|
30
|
+
const pglite = new PGlite();
|
|
31
|
+
created.push(pglite);
|
|
32
|
+
const db = new Kysely({ dialect: new ClosablePGliteDialect(pglite) });
|
|
33
|
+
await sql`select 1`.execute(db);
|
|
34
|
+
const realClose = pglite.close.bind(pglite);
|
|
35
|
+
pglite.close = () => {
|
|
36
|
+
pglite.close = realClose;
|
|
37
|
+
return Promise.reject(new Error("PGlite aborted: ENOSPC"));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
await expect(db.destroy()).resolves.toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rethrows a close that fails for any other reason", async () => {
|
|
44
|
+
const pglite = new PGlite();
|
|
45
|
+
created.push(pglite);
|
|
46
|
+
const db = new Kysely({ dialect: new ClosablePGliteDialect(pglite) });
|
|
47
|
+
await sql`select 1`.execute(db);
|
|
48
|
+
const realClose = pglite.close.bind(pglite);
|
|
49
|
+
pglite.close = () => {
|
|
50
|
+
pglite.close = realClose;
|
|
51
|
+
return Promise.reject(new Error("disk gone"));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await expect(db.destroy()).rejects.toThrow("disk gone");
|
|
55
|
+
});
|
|
56
|
+
|
|
29
57
|
it("is idempotent if the PGlite is already closed", async () => {
|
|
30
58
|
const pglite = new PGlite();
|
|
31
59
|
created.push(pglite);
|