@malloy-publisher/server 0.0.198-dev2 → 0.0.198-dev4
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 +22 -12
- package/dist/compile_worker.mjs +633 -0
- package/dist/instrumentation.mjs +36 -57
- package/dist/server.mjs +986 -650
- package/package.json +1 -1
- package/src/compile/compile_pool.spec.ts +292 -0
- package/src/compile/compile_pool.ts +796 -0
- package/src/compile/compile_worker.ts +721 -0
- package/src/compile/protocol.ts +270 -0
- package/src/health.ts +13 -0
- package/src/instrumentation.ts +0 -50
- package/src/server.ts +0 -5
- package/src/service/environment_store.ts +0 -9
- package/src/service/model.ts +226 -3
- package/src/service/model_worker_path.spec.ts +133 -0
- package/src/service/package.spec.ts +7 -11
- package/src/service/package.ts +156 -49
- package/dist/service/schema_worker.mjs +0 -61
- package/src/service/process_stats_reporter.ts +0 -169
- package/src/service/schema_worker.ts +0 -123
- package/src/service/schema_worker_pool.ts +0 -278
- package/tests/integration/concurrent_environment/concurrent_environment.integration.spec.ts +0 -235
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol between the main thread (CompileWorkerPool) and the
|
|
3
|
+
* compile worker threads. Messages flow in both directions over the
|
|
4
|
+
* worker_threads MessagePort:
|
|
5
|
+
*
|
|
6
|
+
* main ──▶ worker: CompileJobRequest (start a compile)
|
|
7
|
+
* worker ──▶ main: CompileJobResult (success)
|
|
8
|
+
* worker ──▶ main: CompileJobError (failure)
|
|
9
|
+
*
|
|
10
|
+
* worker ──▶ main: SchemaForTablesRequest (proxy schema fetch)
|
|
11
|
+
* worker ──▶ main: SchemaForSqlRequest (proxy SQL block schema)
|
|
12
|
+
* main ──▶ worker: SchemaForTablesResponse / SchemaForSqlResponse
|
|
13
|
+
*
|
|
14
|
+
* main ──▶ worker: ShutdownRequest (graceful drain & exit)
|
|
15
|
+
*
|
|
16
|
+
* The protocol intentionally uses plain structured-clonable POJOs so
|
|
17
|
+
* `parentPort.postMessage` and `worker.postMessage` can transfer them
|
|
18
|
+
* via V8's structured clone — much cheaper than JSON.stringify for
|
|
19
|
+
* the multi-MB `modelDef` payloads that come back from compile.
|
|
20
|
+
*
|
|
21
|
+
* All requests are correlated by an opaque `requestId` string so the
|
|
22
|
+
* receiver can match responses without relying on FIFO ordering.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
Annotation,
|
|
27
|
+
SQLSourceDef,
|
|
28
|
+
TableSourceDef,
|
|
29
|
+
} from "@malloydata/malloy";
|
|
30
|
+
|
|
31
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Direction: main ──▶ worker (compile job)
|
|
33
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Connection metadata the worker needs to construct a stub
|
|
37
|
+
* `InfoConnection`. Resolved lazily — the worker asks the main thread
|
|
38
|
+
* for these on the first `lookupConnection(name)` call (see
|
|
39
|
+
* {@link ConnectionMetadataRequest}). We don't ship the full list
|
|
40
|
+
* upfront because the caller layer doesn't always know it; Malloy
|
|
41
|
+
* sees connection names only as `connection.table('...')`
|
|
42
|
+
* references inside the model.
|
|
43
|
+
*/
|
|
44
|
+
export interface ConnectionMetadata {
|
|
45
|
+
name: string;
|
|
46
|
+
dialectName: string;
|
|
47
|
+
digest: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CompileJobRequest {
|
|
51
|
+
type: "compile";
|
|
52
|
+
requestId: string;
|
|
53
|
+
/** Absolute path to the package directory on disk. */
|
|
54
|
+
packagePath: string;
|
|
55
|
+
/**
|
|
56
|
+
* Path of the model file relative to `packagePath`. Required for
|
|
57
|
+
* file-backed compiles. Omit when supplying {@link inlineSource}.
|
|
58
|
+
*/
|
|
59
|
+
modelPath?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Inline Malloy source string to compile in place of reading a file.
|
|
62
|
+
* Used by call sites that synthesize Malloy on the fly (e.g. the
|
|
63
|
+
* per-database schema probe in {@link Package.getDatabaseInfo}). When
|
|
64
|
+
* set, the worker calls `runtime.loadModel(inlineSource, {…})` instead
|
|
65
|
+
* of resolving a file:// URL. `modelPath` should be omitted; the
|
|
66
|
+
* worker will use a synthetic in-memory model id derived from
|
|
67
|
+
* `requestId` for source-info display purposes.
|
|
68
|
+
*/
|
|
69
|
+
inlineSource?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Base URL used to resolve `import "…"` statements inside the model.
|
|
72
|
+
* Only meaningful with {@link inlineSource}. For file-backed compiles
|
|
73
|
+
* the worker derives the importBaseURL from the modelPath.
|
|
74
|
+
*/
|
|
75
|
+
importBaseURL?: string;
|
|
76
|
+
/** Name of the default connection (e.g. "duckdb"), or null. */
|
|
77
|
+
defaultConnectionName: string | null;
|
|
78
|
+
/** Optional row-build manifest passed through to the Runtime. */
|
|
79
|
+
buildManifest?: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
83
|
+
// Direction: worker ──▶ main (compile result)
|
|
84
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wire shape of a successful compile. Mirrors the fields the
|
|
88
|
+
* server's `Model` constructor needs to fully describe a `.malloy`
|
|
89
|
+
* file without holding a `ModelMaterializer` reference.
|
|
90
|
+
*
|
|
91
|
+
* The materializer itself is intentionally NOT shipped back — it
|
|
92
|
+
* binds to a Runtime that holds live native connection handles and
|
|
93
|
+
* cannot cross a worker_threads boundary. The main thread builds
|
|
94
|
+
* its own materializer lazily on the first query (see
|
|
95
|
+
* `Model.ensureMaterializer`).
|
|
96
|
+
*/
|
|
97
|
+
export interface CompileJobResult {
|
|
98
|
+
type: "compile-result";
|
|
99
|
+
requestId: string;
|
|
100
|
+
/** Whatever `await modelMaterializer.getModel()`._modelDef returned. */
|
|
101
|
+
modelDef: unknown;
|
|
102
|
+
/** Source-info entries (from imports + local sources). */
|
|
103
|
+
sourceInfos: unknown[];
|
|
104
|
+
/** Pre-extracted API source descriptors. */
|
|
105
|
+
sources: unknown[];
|
|
106
|
+
/** Pre-extracted API query descriptors. */
|
|
107
|
+
queries: unknown[];
|
|
108
|
+
/** Parsed `#(filter)` map, keyed by source name. */
|
|
109
|
+
filterMap: Array<[string, unknown[]]>;
|
|
110
|
+
/** Givens declared on the model, already in API shape so the main
|
|
111
|
+
* thread can stash them on the `Model` without further conversion. */
|
|
112
|
+
givens?: unknown[];
|
|
113
|
+
/** Accumulated dataStyles (from HackyDataStylesAccumulator). */
|
|
114
|
+
dataStyles: unknown;
|
|
115
|
+
/** Wall-clock ms inside the worker for the actual compile. */
|
|
116
|
+
compileDurationMs: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface CompileJobError {
|
|
120
|
+
type: "compile-error";
|
|
121
|
+
requestId: string;
|
|
122
|
+
/** Serialized error — the main thread reconstructs an Error. */
|
|
123
|
+
error: SerializedError;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Error wire-shape. We cannot transfer Error instances directly
|
|
128
|
+
* across postMessage cleanly (Bun/Node behaviour diverges on stack
|
|
129
|
+
* propagation), so we ship a structured payload and reconstitute on
|
|
130
|
+
* the main thread.
|
|
131
|
+
*/
|
|
132
|
+
export interface SerializedError {
|
|
133
|
+
name: string;
|
|
134
|
+
message: string;
|
|
135
|
+
stack?: string;
|
|
136
|
+
/** Set when the error originated as a Malloy `MalloyError`. */
|
|
137
|
+
malloyProblems?: unknown[];
|
|
138
|
+
/** Set when the error originated as `ModelCompilationError`. */
|
|
139
|
+
isCompilationError?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
143
|
+
// Direction: worker ──▶ main (proxy connection metadata)
|
|
144
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export interface ConnectionMetadataRequest {
|
|
147
|
+
type: "connection-metadata";
|
|
148
|
+
requestId: string;
|
|
149
|
+
jobId: string;
|
|
150
|
+
connectionName: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ConnectionMetadataResponse {
|
|
154
|
+
type: "connection-metadata-response";
|
|
155
|
+
requestId: string;
|
|
156
|
+
ok: true;
|
|
157
|
+
metadata: ConnectionMetadata;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
161
|
+
// Direction: worker ──▶ main (proxy schema fetches)
|
|
162
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export interface SchemaForTablesRequest {
|
|
165
|
+
type: "schema-for-tables";
|
|
166
|
+
requestId: string;
|
|
167
|
+
/** Job this RPC belongs to (so main routes to the right config). */
|
|
168
|
+
jobId: string;
|
|
169
|
+
connectionName: string;
|
|
170
|
+
tables: Record<string, string>;
|
|
171
|
+
options: {
|
|
172
|
+
refreshTimestamp?: number;
|
|
173
|
+
modelAnnotation?: Annotation;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface SchemaForTablesResponse {
|
|
178
|
+
type: "schema-for-tables-response";
|
|
179
|
+
requestId: string;
|
|
180
|
+
ok: true;
|
|
181
|
+
schemas: Record<string, TableSourceDef>;
|
|
182
|
+
errors: Record<string, string>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SchemaForSqlRequest {
|
|
186
|
+
type: "schema-for-sql";
|
|
187
|
+
requestId: string;
|
|
188
|
+
jobId: string;
|
|
189
|
+
connectionName: string;
|
|
190
|
+
sentence: unknown;
|
|
191
|
+
options: {
|
|
192
|
+
refreshTimestamp?: number;
|
|
193
|
+
modelAnnotation?: Annotation;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface SchemaForSqlResponse {
|
|
198
|
+
type: "schema-for-sql-response";
|
|
199
|
+
requestId: string;
|
|
200
|
+
ok: true;
|
|
201
|
+
structDef?: SQLSourceDef;
|
|
202
|
+
error?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface RpcErrorResponse {
|
|
206
|
+
type: "rpc-error";
|
|
207
|
+
requestId: string;
|
|
208
|
+
ok: false;
|
|
209
|
+
error: SerializedError;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
213
|
+
// Direction: worker ──▶ main (file read for imports)
|
|
214
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Workers read most files directly via fs (they run in the same
|
|
218
|
+
* filesystem namespace). This RPC exists for the rare case where the
|
|
219
|
+
* package URL reader has host-specific behaviour (e.g. virtual files,
|
|
220
|
+
* remote URLs) — we delegate back to the main thread's URL reader so
|
|
221
|
+
* compile semantics stay identical to the in-process path.
|
|
222
|
+
*/
|
|
223
|
+
export interface ReadUrlRequest {
|
|
224
|
+
type: "read-url";
|
|
225
|
+
requestId: string;
|
|
226
|
+
jobId: string;
|
|
227
|
+
url: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface ReadUrlResponse {
|
|
231
|
+
type: "read-url-response";
|
|
232
|
+
requestId: string;
|
|
233
|
+
ok: true;
|
|
234
|
+
contents: string;
|
|
235
|
+
invalidationKey?: string | number | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
239
|
+
// Lifecycle
|
|
240
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export interface ShutdownRequest {
|
|
243
|
+
type: "shutdown";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface ReadyMessage {
|
|
247
|
+
type: "ready";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
251
|
+
// Union types for routing
|
|
252
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export type MainToWorkerMessage =
|
|
255
|
+
| CompileJobRequest
|
|
256
|
+
| ConnectionMetadataResponse
|
|
257
|
+
| SchemaForTablesResponse
|
|
258
|
+
| SchemaForSqlResponse
|
|
259
|
+
| ReadUrlResponse
|
|
260
|
+
| RpcErrorResponse
|
|
261
|
+
| ShutdownRequest;
|
|
262
|
+
|
|
263
|
+
export type WorkerToMainMessage =
|
|
264
|
+
| CompileJobResult
|
|
265
|
+
| CompileJobError
|
|
266
|
+
| ConnectionMetadataRequest
|
|
267
|
+
| SchemaForTablesRequest
|
|
268
|
+
| SchemaForSqlRequest
|
|
269
|
+
| ReadUrlRequest
|
|
270
|
+
| ReadyMessage;
|
package/src/health.ts
CHANGED
|
@@ -143,6 +143,19 @@ export async function performGracefulShutdownAfterDrain(
|
|
|
143
143
|
/* do nothing */
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
try {
|
|
147
|
+
// Drain in-flight compiles and terminate worker_threads before
|
|
148
|
+
// we exit so a slow compile doesn't leave orphan worker
|
|
149
|
+
// processes. Lazy-imported to avoid pulling the pool module
|
|
150
|
+
// into the health.ts dep graph for tests that don't exercise
|
|
151
|
+
// the compile path.
|
|
152
|
+
const { getCompilePool } = await import("./compile/compile_pool");
|
|
153
|
+
await getCompilePool().shutdown();
|
|
154
|
+
logger.info("Malloy compile worker pool shut down");
|
|
155
|
+
} catch (_error) {
|
|
156
|
+
/* do nothing */
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
if (shutdownGracefulCloseTimeoutSeconds > 0) {
|
|
147
160
|
logger.info(
|
|
148
161
|
`Waiting ${shutdownGracefulCloseTimeoutSeconds} seconds after server close before exit...`,
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
2
1
|
import { metrics } from "@opentelemetry/api";
|
|
3
2
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
4
3
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
@@ -117,55 +116,6 @@ const httpRequestCount = meter.createCounter("http_server_requests_total", {
|
|
|
117
116
|
description: "Total number of HTTP requests",
|
|
118
117
|
});
|
|
119
118
|
|
|
120
|
-
// Event-loop-delay metrics. A blocked event loop is the only way the
|
|
121
|
-
// /health/liveness probe (a pure synchronous 200 handler) can fail under K8s,
|
|
122
|
-
// so we surface p50/p99/max so an operator can correlate liveness restarts
|
|
123
|
-
// with sustained event-loop pressure (large Malloy compiles, GC, etc.).
|
|
124
|
-
const eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });
|
|
125
|
-
eventLoopHistogram.enable();
|
|
126
|
-
|
|
127
|
-
const eventLoopLagP50 = meter.createObservableGauge(
|
|
128
|
-
"publisher_event_loop_lag_p50_ms",
|
|
129
|
-
{
|
|
130
|
-
description:
|
|
131
|
-
"Event loop delay p50 since the last scrape, in milliseconds",
|
|
132
|
-
unit: "ms",
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
const eventLoopLagP99 = meter.createObservableGauge(
|
|
136
|
-
"publisher_event_loop_lag_p99_ms",
|
|
137
|
-
{
|
|
138
|
-
description:
|
|
139
|
-
"Event loop delay p99 since the last scrape, in milliseconds",
|
|
140
|
-
unit: "ms",
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
const eventLoopLagMax = meter.createObservableGauge(
|
|
144
|
-
"publisher_event_loop_lag_max_ms",
|
|
145
|
-
{
|
|
146
|
-
description:
|
|
147
|
-
"Event loop delay max since the last scrape, in milliseconds",
|
|
148
|
-
unit: "ms",
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// Sample all three in one batch so the histogram reset can't race the reads.
|
|
153
|
-
meter.addBatchObservableCallback(
|
|
154
|
-
(observableResult) => {
|
|
155
|
-
observableResult.observe(
|
|
156
|
-
eventLoopLagP50,
|
|
157
|
-
eventLoopHistogram.percentile(50) / 1e6,
|
|
158
|
-
);
|
|
159
|
-
observableResult.observe(
|
|
160
|
-
eventLoopLagP99,
|
|
161
|
-
eventLoopHistogram.percentile(99) / 1e6,
|
|
162
|
-
);
|
|
163
|
-
observableResult.observe(eventLoopLagMax, eventLoopHistogram.max / 1e6);
|
|
164
|
-
eventLoopHistogram.reset();
|
|
165
|
-
},
|
|
166
|
-
[eventLoopLagP50, eventLoopLagP99, eventLoopLagMax],
|
|
167
|
-
);
|
|
168
|
-
|
|
169
119
|
const IGNORED_PATHS = new Set([
|
|
170
120
|
"/health",
|
|
171
121
|
"/health/liveness",
|
package/src/server.ts
CHANGED
|
@@ -43,7 +43,6 @@ import { EnvironmentStore } from "./service/environment_store";
|
|
|
43
43
|
import { ManifestService } from "./service/manifest_service";
|
|
44
44
|
import { MaterializationService } from "./service/materialization_service";
|
|
45
45
|
import { PackageMemoryGovernor } from "./service/package_memory_governor";
|
|
46
|
-
import { ProcessStatsReporter } from "./service/process_stats_reporter";
|
|
47
46
|
|
|
48
47
|
/** Normalize an Express query param into a string[] or undefined. */
|
|
49
48
|
export function normalizeQueryArray(value: unknown): string[] | undefined {
|
|
@@ -173,10 +172,6 @@ const memoryGovernor = memoryGovernorConfig
|
|
|
173
172
|
: null;
|
|
174
173
|
memoryGovernor?.start();
|
|
175
174
|
environmentStore.setMemoryGovernor(memoryGovernor);
|
|
176
|
-
// Always-on process-stats heartbeat so we can correlate RSS / thread
|
|
177
|
-
// counts / heap usage with traffic in prod. Logs every 30s at info.
|
|
178
|
-
const processStatsReporter = new ProcessStatsReporter(memoryGovernor);
|
|
179
|
-
processStatsReporter.start();
|
|
180
175
|
const packageController = new PackageController(
|
|
181
176
|
environmentStore,
|
|
182
177
|
manifestService,
|
|
@@ -703,15 +703,6 @@ export class EnvironmentStore {
|
|
|
703
703
|
}
|
|
704
704
|
|
|
705
705
|
public async getStatus() {
|
|
706
|
-
const memoryGovernorStatus = this.memoryGovernor?.getStatus() ?? null;
|
|
707
|
-
// Log every /status hit so we have a trace of RSS / back-pressure
|
|
708
|
-
// state to correlate against pod OOMs and request-driven leaks.
|
|
709
|
-
// Logged at info so it shows up in prod without LOG_LEVEL changes;
|
|
710
|
-
// the endpoint is low-frequency (monitoring/UI), so volume is fine.
|
|
711
|
-
logger.info("Memory governor status", {
|
|
712
|
-
memoryGovernor: memoryGovernorStatus,
|
|
713
|
-
});
|
|
714
|
-
|
|
715
706
|
const status = {
|
|
716
707
|
timestamp: Date.now(),
|
|
717
708
|
environments: [] as Array<components["schemas"]["Environment"]>,
|