@malloy-publisher/server 0.0.198-dev4 → 0.0.198-dev6
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/build.ts +17 -13
- package/dist/instrumentation.mjs +21 -0
- package/dist/package_load_worker.mjs +12213 -0
- package/dist/server.mjs +2026 -2622
- package/package.json +2 -3
- package/src/health.ts +5 -3
- package/src/instrumentation.ts +50 -0
- package/src/package_load/package_load_pool.spec.ts +252 -0
- package/src/package_load/package_load_pool.ts +920 -0
- package/src/{compile/compile_worker.ts → package_load/package_load_worker.ts} +505 -246
- package/src/package_load/protocol.ts +336 -0
- package/src/server.ts +12 -0
- package/src/service/environment_store.ts +24 -3
- package/src/service/given.ts +80 -0
- package/src/service/model.ts +255 -291
- package/src/service/package.spec.ts +10 -0
- package/src/service/package.ts +268 -259
- package/src/service/package_worker_path.spec.ts +196 -0
- package/dist/compile_worker.mjs +0 -633
- package/src/compile/compile_pool.spec.ts +0 -292
- package/src/compile/compile_pool.ts +0 -796
- package/src/compile/protocol.ts +0 -270
- package/src/service/model_worker_path.spec.ts +0 -133
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: exercise `Package.create` with the package-load
|
|
3
|
+
* worker pool enabled (PACKAGE_LOAD_WORKERS=1).
|
|
4
|
+
*
|
|
5
|
+
* Validates that the worker-load path:
|
|
6
|
+
* - reads the manifest, probes embedded databases, and compiles
|
|
7
|
+
* every model in a single off-thread job
|
|
8
|
+
* - produces a live `Package` whose `Model`s have populated
|
|
9
|
+
* `modelDef` / `sources` / `queries`
|
|
10
|
+
* - hydrates the `ModelMaterializer` from `modelDef` on first
|
|
11
|
+
* query (no recompile) — verified end-to-end by running a
|
|
12
|
+
* query through the resulting Model and getting a result
|
|
13
|
+
*
|
|
14
|
+
* Kept separate from `package.spec.ts` so the existing tests keep
|
|
15
|
+
* running on the in-process path without paying worker startup cost.
|
|
16
|
+
*
|
|
17
|
+
* Pool reuse strategy: one `PackageLoadPool` shared across all
|
|
18
|
+
* cases in this file. Spawning a fresh worker per test crashes Bun
|
|
19
|
+
* (segfault) because DuckDB's native bindings don't tolerate being
|
|
20
|
+
* loaded concurrently into multiple worker isolates of the same Bun
|
|
21
|
+
* process. Production uses one pool; this matches.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
afterAll,
|
|
25
|
+
afterEach,
|
|
26
|
+
beforeAll,
|
|
27
|
+
beforeEach,
|
|
28
|
+
describe,
|
|
29
|
+
expect,
|
|
30
|
+
it,
|
|
31
|
+
} from "bun:test";
|
|
32
|
+
import * as fs from "fs";
|
|
33
|
+
import * as os from "os";
|
|
34
|
+
import * as path from "path";
|
|
35
|
+
import {
|
|
36
|
+
PackageLoadPool,
|
|
37
|
+
__setPackageLoadPoolForTests,
|
|
38
|
+
} from "../package_load/package_load_pool";
|
|
39
|
+
import { Package } from "./package";
|
|
40
|
+
|
|
41
|
+
const ORIGINAL_ENV = process.env.PACKAGE_LOAD_WORKERS;
|
|
42
|
+
|
|
43
|
+
describe("Package.create via worker pool", () => {
|
|
44
|
+
let tempDir: string;
|
|
45
|
+
let pool: PackageLoadPool;
|
|
46
|
+
|
|
47
|
+
beforeAll(async () => {
|
|
48
|
+
process.env.PACKAGE_LOAD_WORKERS = "1";
|
|
49
|
+
pool = new PackageLoadPool(1);
|
|
50
|
+
// Wire our pool into the module-level singleton so Package.create
|
|
51
|
+
// picks it up via getPackageLoadPool().
|
|
52
|
+
await __setPackageLoadPoolForTests(pool);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterAll(async () => {
|
|
56
|
+
await __setPackageLoadPoolForTests(null);
|
|
57
|
+
if (ORIGINAL_ENV === undefined) {
|
|
58
|
+
delete process.env.PACKAGE_LOAD_WORKERS;
|
|
59
|
+
} else {
|
|
60
|
+
process.env.PACKAGE_LOAD_WORKERS = ORIGINAL_ENV;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "publisher-pkg-worker-"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
if (tempDir) {
|
|
70
|
+
// tempDir gets wiped by Package.create on failure (it's the
|
|
71
|
+
// staging-cleanup path); ignore ENOENT here.
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
/* already gone */
|
|
76
|
+
}
|
|
77
|
+
tempDir = "";
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
async function makeMalloyConfig(): Promise<{
|
|
82
|
+
malloyConfig: import("@malloydata/malloy").MalloyConfig;
|
|
83
|
+
duckdb: { close: () => Promise<void> };
|
|
84
|
+
}> {
|
|
85
|
+
const { MalloyConfig, FixedConnectionMap } = await import(
|
|
86
|
+
"@malloydata/malloy"
|
|
87
|
+
);
|
|
88
|
+
const { DuckDBConnection } = await import("@malloydata/db-duckdb");
|
|
89
|
+
const duckdb = new DuckDBConnection("duckdb", ":memory:");
|
|
90
|
+
const connections = new FixedConnectionMap(
|
|
91
|
+
new Map([["duckdb", duckdb]]),
|
|
92
|
+
"duckdb",
|
|
93
|
+
);
|
|
94
|
+
const malloyConfig = new MalloyConfig({ connections: {} });
|
|
95
|
+
malloyConfig.wrapConnections(() => connections);
|
|
96
|
+
return { malloyConfig, duckdb };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function writeManifest(): void {
|
|
100
|
+
fs.writeFileSync(
|
|
101
|
+
path.join(tempDir, "publisher.json"),
|
|
102
|
+
JSON.stringify({ name: "pkg", description: "test package" }),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
it("loads a package end-to-end and serves a query through the hydrated materializer", async () => {
|
|
107
|
+
writeManifest();
|
|
108
|
+
// Define `total_v` as a *view* on the source so the query
|
|
109
|
+
// builder's `run: nums -> total_v` form resolves. (Top-level
|
|
110
|
+
// queries take the `run: total_q` form — orthogonal path that
|
|
111
|
+
// the in-process tests already cover.)
|
|
112
|
+
fs.writeFileSync(
|
|
113
|
+
path.join(tempDir, "trivial.malloy"),
|
|
114
|
+
`source: nums is duckdb.sql("select 1 as a, 2 as b") extend {
|
|
115
|
+
measure: total is a.sum()
|
|
116
|
+
view: total_v is { aggregate: total }
|
|
117
|
+
}`,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const { malloyConfig, duckdb } = await makeMalloyConfig();
|
|
121
|
+
try {
|
|
122
|
+
const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
|
|
123
|
+
expect(pkg).toBeInstanceOf(Package);
|
|
124
|
+
expect(pkg.getModelPaths()).toEqual(["trivial.malloy"]);
|
|
125
|
+
|
|
126
|
+
const model = pkg.getModel("trivial.malloy");
|
|
127
|
+
expect(model).toBeDefined();
|
|
128
|
+
const apiModel = (await model!.getModel()) as {
|
|
129
|
+
modelDef?: string;
|
|
130
|
+
sources?: { name?: string }[];
|
|
131
|
+
};
|
|
132
|
+
expect(apiModel.modelDef).toBeDefined();
|
|
133
|
+
expect(apiModel.sources?.[0]?.name).toBe("nums");
|
|
134
|
+
|
|
135
|
+
// First query against the package — hydrates the
|
|
136
|
+
// ModelMaterializer from the worker's modelDef without a
|
|
137
|
+
// recompile, then runs the SQL against the *main thread's*
|
|
138
|
+
// DuckDB connection (the only one with the in-memory `nums`
|
|
139
|
+
// source loaded via duckdb.sql()).
|
|
140
|
+
const { result } = await model!.getQueryResults(
|
|
141
|
+
"nums",
|
|
142
|
+
"total_v",
|
|
143
|
+
undefined,
|
|
144
|
+
);
|
|
145
|
+
expect(result.data).toBeDefined();
|
|
146
|
+
} finally {
|
|
147
|
+
await duckdb.close();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("propagates a per-model compile failure as a thrown error from Package.create", async () => {
|
|
152
|
+
writeManifest();
|
|
153
|
+
fs.writeFileSync(
|
|
154
|
+
path.join(tempDir, "broken.malloy"),
|
|
155
|
+
`source: bad is duckdb.sql("select 1 as a") extend {
|
|
156
|
+
measure: oops is THIS_FUNC_DOES_NOT_EXIST(a)
|
|
157
|
+
}`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const { malloyConfig, duckdb } = await makeMalloyConfig();
|
|
161
|
+
try {
|
|
162
|
+
await expect(
|
|
163
|
+
Package.create("env", "pkg", tempDir, malloyConfig),
|
|
164
|
+
).rejects.toBeInstanceOf(Error);
|
|
165
|
+
} finally {
|
|
166
|
+
await duckdb.close();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// NB: kept last in this describe — swapping the singleton for a
|
|
171
|
+
// pre-shutdown pool also tears down the shared `pool` (the swap
|
|
172
|
+
// implementation shuts down the outgoing singleton). Subsequent
|
|
173
|
+
// tests in this describe would see a dead pool. afterAll only
|
|
174
|
+
// resets the singleton to null, so this is safe at the tail.
|
|
175
|
+
it("rewraps pool-infrastructure failures as ServiceUnavailableError (HTTP 503)", async () => {
|
|
176
|
+
writeManifest();
|
|
177
|
+
fs.writeFileSync(
|
|
178
|
+
path.join(tempDir, "trivial.malloy"),
|
|
179
|
+
`source: nums is duckdb.sql("select 1 as a")`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const deadPool = new PackageLoadPool(1);
|
|
183
|
+
await deadPool.shutdown();
|
|
184
|
+
await __setPackageLoadPoolForTests(deadPool);
|
|
185
|
+
|
|
186
|
+
const { ServiceUnavailableError } = await import("../errors");
|
|
187
|
+
const { malloyConfig, duckdb } = await makeMalloyConfig();
|
|
188
|
+
try {
|
|
189
|
+
await expect(
|
|
190
|
+
Package.create("env", "pkg", tempDir, malloyConfig),
|
|
191
|
+
).rejects.toBeInstanceOf(ServiceUnavailableError);
|
|
192
|
+
} finally {
|
|
193
|
+
await duckdb.close();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|