@malloy-publisher/server 0.0.192 → 0.0.193
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/dist/app/api-doc.yaml +522 -1
- package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-Di9MU3lS.js} +1 -1
- package/dist/app/assets/{MainPage-GL06aMke.js → MainPage-yZQo2HSL.js} +1 -1
- package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Dx2mHWeT.js} +1 -1
- package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-Q386Py9t.js} +1 -1
- package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-WR7wPQB-.js} +1 -1
- package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-stRGU4aW.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-D3iX0djH.js} +1 -1
- package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
- package/dist/app/assets/{index-d5rvmoZ7.js → index-CVHzPJwN.js} +119 -119
- package/dist/app/assets/{index-CzjyS9cx.js → index-DavAceYD.js} +50 -50
- package/dist/app/assets/{index-HHdhLUpv.js → index-Y3Y-VRna.js} +1 -1
- package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-Bp8OIhfV.js} +46 -46
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1389 -984
- package/package.json +10 -10
- package/src/controller/connection.controller.ts +102 -27
- package/src/dto/connection.dto.spec.ts +4 -0
- package/src/dto/connection.dto.ts +46 -2
- package/src/server.ts +201 -2
- package/src/service/connection.spec.ts +250 -4
- package/src/service/connection.ts +326 -473
- package/src/service/connection_config.ts +514 -0
- package/src/service/connection_service.spec.ts +50 -0
- package/src/service/connection_service.ts +125 -32
- package/src/service/materialization_service.spec.ts +18 -12
- package/src/service/materialization_service.ts +54 -7
- package/src/service/model.ts +24 -27
- package/src/service/package.spec.ts +125 -1
- package/src/service/package.ts +86 -44
- package/src/service/project.ts +172 -94
- package/src/service/project_store.spec.ts +72 -0
- package/src/service/project_store.ts +98 -81
- package/tests/unit/duckdb/attached_databases.test.ts +1 -19
package/src/service/package.ts
CHANGED
|
@@ -2,10 +2,14 @@ import * as fs from "fs/promises";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
4
|
import { DuckDBConnection } from "@malloydata/db-duckdb";
|
|
5
|
+
import "@malloydata/db-duckdb/native";
|
|
5
6
|
import {
|
|
6
7
|
Connection,
|
|
7
8
|
ConnectionRuntime,
|
|
8
9
|
EmptyURLReader,
|
|
10
|
+
FixedConnectionMap,
|
|
11
|
+
contextOverlay,
|
|
12
|
+
MalloyConfig,
|
|
9
13
|
SourceDef,
|
|
10
14
|
} from "@malloydata/malloy";
|
|
11
15
|
import { metrics } from "@opentelemetry/api";
|
|
@@ -28,6 +32,14 @@ type ApiNotebook = components["schemas"]["Notebook"];
|
|
|
28
32
|
export type ApiPackage = components["schemas"]["Package"];
|
|
29
33
|
type ApiColumn = components["schemas"]["Column"];
|
|
30
34
|
type ApiTableDescription = components["schemas"]["TableDescription"];
|
|
35
|
+
// A thunk lets callers pass a live reference to the *current* project
|
|
36
|
+
// MalloyConfig so the package wrapper resolves project connections against the
|
|
37
|
+
// generation that's active at lookup time, not the one that was current when
|
|
38
|
+
// the package was first loaded.
|
|
39
|
+
type PackageConnectionInput =
|
|
40
|
+
| MalloyConfig
|
|
41
|
+
| Map<string, Connection>
|
|
42
|
+
| (() => MalloyConfig);
|
|
31
43
|
|
|
32
44
|
const ENABLE_LIST_MODEL_COMPILATION = true;
|
|
33
45
|
export class Package {
|
|
@@ -37,7 +49,7 @@ export class Package {
|
|
|
37
49
|
private databases: ApiDatabase[];
|
|
38
50
|
private models: Map<string, Model> = new Map();
|
|
39
51
|
private packagePath: string;
|
|
40
|
-
private
|
|
52
|
+
private malloyConfig: MalloyConfig;
|
|
41
53
|
private static meter = metrics.getMeter("publisher");
|
|
42
54
|
private static packageLoadHistogram = this.meter.createHistogram(
|
|
43
55
|
"malloy_package_load_duration",
|
|
@@ -54,7 +66,7 @@ export class Package {
|
|
|
54
66
|
packageMetadata: ApiPackage,
|
|
55
67
|
databases: ApiDatabase[],
|
|
56
68
|
models: Map<string, Model>,
|
|
57
|
-
|
|
69
|
+
malloyConfig: MalloyConfig = new MalloyConfig({ connections: {} }),
|
|
58
70
|
) {
|
|
59
71
|
this.projectName = projectName;
|
|
60
72
|
this.packageName = packageName;
|
|
@@ -62,14 +74,14 @@ export class Package {
|
|
|
62
74
|
this.packageMetadata = packageMetadata;
|
|
63
75
|
this.databases = databases;
|
|
64
76
|
this.models = models;
|
|
65
|
-
this.
|
|
77
|
+
this.malloyConfig = malloyConfig;
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
static async create(
|
|
69
81
|
projectName: string,
|
|
70
82
|
packageName: string,
|
|
71
83
|
packagePath: string,
|
|
72
|
-
|
|
84
|
+
projectMalloyConfig: PackageConnectionInput,
|
|
73
85
|
): Promise<Package> {
|
|
74
86
|
const startTime = performance.now();
|
|
75
87
|
await Package.validatePackageManifestExistsOrThrowError(packagePath);
|
|
@@ -97,20 +109,17 @@ export class Package {
|
|
|
97
109
|
databaseCount: databases.length,
|
|
98
110
|
duration: formatDuration(databasesTime - packageConfigTime),
|
|
99
111
|
});
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
// Add a duckdb connection for the package.
|
|
103
|
-
const duckdbConnection = new DuckDBConnection(
|
|
104
|
-
"duckdb",
|
|
105
|
-
":memory:",
|
|
112
|
+
const malloyConfig = Package.buildPackageMalloyConfig(
|
|
106
113
|
packagePath,
|
|
114
|
+
typeof projectMalloyConfig === "function"
|
|
115
|
+
? projectMalloyConfig
|
|
116
|
+
: () => Package.toMalloyConfig(projectMalloyConfig),
|
|
107
117
|
);
|
|
108
|
-
connections.set("duckdb", duckdbConnection);
|
|
109
118
|
|
|
110
119
|
const models = await Package.loadModels(
|
|
111
120
|
packageName,
|
|
112
121
|
packagePath,
|
|
113
|
-
|
|
122
|
+
malloyConfig,
|
|
114
123
|
);
|
|
115
124
|
const modelsTime = performance.now();
|
|
116
125
|
logger.info("Models loaded", {
|
|
@@ -159,7 +168,7 @@ export class Package {
|
|
|
159
168
|
packageConfig,
|
|
160
169
|
databases,
|
|
161
170
|
models,
|
|
162
|
-
|
|
171
|
+
malloyConfig,
|
|
163
172
|
);
|
|
164
173
|
} catch (error) {
|
|
165
174
|
logger.error(`Error loading package ${packageName}`, { error });
|
|
@@ -190,10 +199,6 @@ export class Package {
|
|
|
190
199
|
return this.packageName;
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
public getPackagePath(): string {
|
|
194
|
-
return this.packagePath;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
202
|
public getPackageMetadata(): ApiPackage {
|
|
198
203
|
return this.packageMetadata;
|
|
199
204
|
}
|
|
@@ -206,19 +211,24 @@ export class Package {
|
|
|
206
211
|
return this.models.get(modelPath);
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
public async getMalloyConnection(
|
|
215
|
+
connectionName: string,
|
|
216
|
+
): Promise<Connection> {
|
|
217
|
+
return this.malloyConfig.connections.lookupConnection(connectionName);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public getMalloyConfig(): MalloyConfig {
|
|
221
|
+
return this.malloyConfig;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public getPackagePath(): string {
|
|
225
|
+
return this.packagePath;
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
public getModelPaths(): string[] {
|
|
210
229
|
return Array.from(this.models.keys());
|
|
211
230
|
}
|
|
212
231
|
|
|
213
|
-
/**
|
|
214
|
-
* Recompile every model in the package with the given build manifest
|
|
215
|
-
* so queries resolve persist references to materialized tables.
|
|
216
|
-
*
|
|
217
|
-
* Builds a fresh map off to the side and swaps it in at the end. If any
|
|
218
|
-
* recompile fails the whole call rejects before the swap and the live
|
|
219
|
-
* `this.models` reference remains untouched — no half-loaded state is
|
|
220
|
-
* ever observable to concurrent readers.
|
|
221
|
-
*/
|
|
222
232
|
public async reloadAllModels(
|
|
223
233
|
buildManifest: BuildManifest["entries"],
|
|
224
234
|
): Promise<void> {
|
|
@@ -228,14 +238,13 @@ export class Package {
|
|
|
228
238
|
modelCount: modelPaths.length,
|
|
229
239
|
manifestEntryCount: Object.keys(buildManifest).length,
|
|
230
240
|
});
|
|
231
|
-
|
|
232
241
|
const reloaded = await Promise.all(
|
|
233
242
|
modelPaths.map((modelPath) =>
|
|
234
243
|
Model.create(
|
|
235
244
|
this.packageName,
|
|
236
245
|
this.packagePath,
|
|
237
246
|
modelPath,
|
|
238
|
-
this.
|
|
247
|
+
this.malloyConfig,
|
|
239
248
|
{ buildManifest },
|
|
240
249
|
),
|
|
241
250
|
),
|
|
@@ -247,20 +256,6 @@ export class Package {
|
|
|
247
256
|
this.models = nextModels;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
public getConnections(): Map<string, Connection> {
|
|
251
|
-
return this.connections;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
public getMalloyConnection(connectionName: string): Connection {
|
|
255
|
-
const connection = this.connections.get(connectionName);
|
|
256
|
-
if (!connection) {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Connection ${connectionName} not found in package ${this.packageName}`,
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
return connection;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
259
|
public async getModelFileText(modelPath: string): Promise<string> {
|
|
265
260
|
const model = this.getModel(modelPath);
|
|
266
261
|
if (!model) {
|
|
@@ -322,17 +317,64 @@ export class Package {
|
|
|
322
317
|
private static async loadModels(
|
|
323
318
|
packageName: string,
|
|
324
319
|
packagePath: string,
|
|
325
|
-
|
|
320
|
+
malloyConfig: MalloyConfig,
|
|
326
321
|
): Promise<Map<string, Model>> {
|
|
327
322
|
const modelPaths = await Package.getModelPaths(packagePath);
|
|
328
323
|
const models = await Promise.all(
|
|
329
324
|
modelPaths.map((modelPath) =>
|
|
330
|
-
Model.create(packageName, packagePath, modelPath,
|
|
325
|
+
Model.create(packageName, packagePath, modelPath, malloyConfig),
|
|
331
326
|
),
|
|
332
327
|
);
|
|
333
328
|
return new Map(models.map((model) => [model.getPath(), model]));
|
|
334
329
|
}
|
|
335
330
|
|
|
331
|
+
private static buildPackageMalloyConfig(
|
|
332
|
+
packagePath: string,
|
|
333
|
+
getProjectMalloyConfig: () => MalloyConfig,
|
|
334
|
+
): MalloyConfig {
|
|
335
|
+
const malloyConfig = new MalloyConfig(
|
|
336
|
+
{
|
|
337
|
+
connections: {
|
|
338
|
+
duckdb: {
|
|
339
|
+
is: "duckdb",
|
|
340
|
+
databasePath: ":memory:",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
config: contextOverlay({ rootDirectory: packagePath }),
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
malloyConfig.wrapConnections((base) => ({
|
|
350
|
+
lookupConnection: async (name?: string) => {
|
|
351
|
+
if (!name || name === "duckdb") {
|
|
352
|
+
return base.lookupConnection(name);
|
|
353
|
+
}
|
|
354
|
+
// Resolve against the *current* project MalloyConfig so a
|
|
355
|
+
// connection-generation swap on Project propagates without a
|
|
356
|
+
// package reload.
|
|
357
|
+
return getProjectMalloyConfig().connections.lookupConnection(name);
|
|
358
|
+
},
|
|
359
|
+
}));
|
|
360
|
+
|
|
361
|
+
return malloyConfig;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private static toMalloyConfig(
|
|
365
|
+
input: MalloyConfig | Map<string, Connection>,
|
|
366
|
+
): MalloyConfig {
|
|
367
|
+
if (input instanceof MalloyConfig) {
|
|
368
|
+
return input;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const malloyConfig = new MalloyConfig({ connections: {} });
|
|
372
|
+
malloyConfig.wrapConnections(
|
|
373
|
+
() => new FixedConnectionMap(input, "duckdb"),
|
|
374
|
+
);
|
|
375
|
+
return malloyConfig;
|
|
376
|
+
}
|
|
377
|
+
|
|
336
378
|
private static async getModelPaths(packagePath: string): Promise<string[]> {
|
|
337
379
|
let files = undefined;
|
|
338
380
|
try {
|
package/src/service/project.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { LogMessage } from "@malloydata/malloy";
|
|
2
|
-
import {
|
|
3
|
-
import { BaseConnection } from "@malloydata/malloy/connection";
|
|
2
|
+
import { MalloyError, Runtime } from "@malloydata/malloy";
|
|
4
3
|
import { Mutex } from "async-mutex";
|
|
5
4
|
import * as fs from "fs";
|
|
6
5
|
import * as path from "path";
|
|
@@ -14,9 +13,10 @@ import {
|
|
|
14
13
|
import { logger } from "../logger";
|
|
15
14
|
import { URL_READER } from "../utils";
|
|
16
15
|
import {
|
|
17
|
-
|
|
16
|
+
buildProjectMalloyConfig,
|
|
18
17
|
deleteDuckLakeConnectionFile,
|
|
19
18
|
InternalConnection,
|
|
19
|
+
ProjectMalloyConfig,
|
|
20
20
|
} from "./connection";
|
|
21
21
|
import { ApiConnection } from "./model";
|
|
22
22
|
import { Package } from "./package";
|
|
@@ -35,12 +35,27 @@ interface PackageInfo {
|
|
|
35
35
|
|
|
36
36
|
type ApiPackage = components["schemas"]["Package"];
|
|
37
37
|
type ApiProject = components["schemas"]["Project"];
|
|
38
|
+
type RetiredConnectionGeneration = {
|
|
39
|
+
label: string;
|
|
40
|
+
releaseConnections: () => Promise<void>;
|
|
41
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const RETIRED_CONNECTION_DRAIN_MS = 30_000;
|
|
38
45
|
|
|
39
46
|
export class Project {
|
|
40
47
|
private packages: Map<string, Package> = new Map();
|
|
48
|
+
// Lock ordering: connectionMutex (project) MUST be acquired before any
|
|
49
|
+
// packageMutex. Connection updates may invalidate cached package
|
|
50
|
+
// MalloyConfigs and force reloads, so the project lock is the outer one.
|
|
51
|
+
// Never acquire connectionMutex while holding a packageMutex — that's the
|
|
52
|
+
// AB/BA deadlock path.
|
|
41
53
|
private packageMutexes = new Map<string, Mutex>();
|
|
42
54
|
private packageStatuses: Map<string, PackageInfo> = new Map();
|
|
43
|
-
private
|
|
55
|
+
private malloyConfig: ProjectMalloyConfig;
|
|
56
|
+
private connectionMutex = new Mutex();
|
|
57
|
+
private retiredConnectionGenerations =
|
|
58
|
+
new Set<RetiredConnectionGeneration>();
|
|
44
59
|
private apiConnections: ApiConnection[];
|
|
45
60
|
private projectPath: string;
|
|
46
61
|
private projectName: string;
|
|
@@ -49,12 +64,12 @@ export class Project {
|
|
|
49
64
|
constructor(
|
|
50
65
|
projectName: string,
|
|
51
66
|
projectPath: string,
|
|
52
|
-
|
|
67
|
+
malloyConfig: ProjectMalloyConfig,
|
|
53
68
|
apiConnections: InternalConnection[],
|
|
54
69
|
) {
|
|
55
70
|
this.projectName = projectName;
|
|
56
71
|
this.projectPath = projectPath;
|
|
57
|
-
this.
|
|
72
|
+
this.malloyConfig = malloyConfig;
|
|
58
73
|
this.apiConnections = apiConnections;
|
|
59
74
|
this.metadata = {
|
|
60
75
|
resource: `${API_PREFIX}/projects/${this.projectName}`,
|
|
@@ -87,30 +102,28 @@ export class Project {
|
|
|
87
102
|
// Handle connections update
|
|
88
103
|
// TODO: Update project connections should have its own API endpoint
|
|
89
104
|
if (payload.connections) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const payloadConnections = payload.connections;
|
|
106
|
+
await this.runConnectionUpdateExclusive(async () => {
|
|
107
|
+
logger.info(
|
|
108
|
+
`Updating ${payloadConnections.length} connections for project ${this.projectName}`,
|
|
109
|
+
);
|
|
110
|
+
const isUpdateConnectionRequest = true;
|
|
111
|
+
const nextMalloyConfig = buildProjectMalloyConfig(
|
|
112
|
+
payloadConnections,
|
|
98
113
|
this.projectPath,
|
|
99
114
|
isUpdateConnectionRequest,
|
|
100
115
|
);
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
this.malloyConnections = malloyConnections;
|
|
104
|
-
this.apiConnections = apiConnections;
|
|
117
|
+
this.updateConnections(nextMalloyConfig);
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
119
|
+
logger.info(
|
|
120
|
+
`Successfully updated connections for project ${this.projectName}`,
|
|
121
|
+
{
|
|
122
|
+
apiConnections: this.apiConnections.length,
|
|
123
|
+
internalConnections: this.apiConnections.length,
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
});
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
return this;
|
|
@@ -128,22 +141,20 @@ export class Project {
|
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
logger.info(`Creating project with connection configuration`);
|
|
131
|
-
const
|
|
132
|
-
await createProjectConnections(connections, projectPath);
|
|
144
|
+
const malloyConfig = buildProjectMalloyConfig(connections, projectPath);
|
|
133
145
|
|
|
134
146
|
logger.info(
|
|
135
|
-
`Loaded ${
|
|
147
|
+
`Loaded ${malloyConfig.apiConnections.length} connections for project ${projectName}`,
|
|
136
148
|
{
|
|
137
|
-
|
|
138
|
-
apiConnections,
|
|
149
|
+
apiConnections: malloyConfig.apiConnections,
|
|
139
150
|
},
|
|
140
151
|
);
|
|
141
152
|
|
|
142
153
|
const project = new Project(
|
|
143
154
|
projectName,
|
|
144
155
|
projectPath,
|
|
145
|
-
|
|
146
|
-
apiConnections,
|
|
156
|
+
malloyConfig,
|
|
157
|
+
malloyConfig.apiConnections,
|
|
147
158
|
);
|
|
148
159
|
|
|
149
160
|
return project;
|
|
@@ -203,10 +214,14 @@ export class Project {
|
|
|
203
214
|
},
|
|
204
215
|
};
|
|
205
216
|
|
|
206
|
-
|
|
217
|
+
const pkg = await this.getPackage(packageName);
|
|
218
|
+
|
|
219
|
+
// Initialize Runtime with the package's active MalloyConfig so compile
|
|
220
|
+
// checks see the same package-scoped duckdb as execution. This runtime
|
|
221
|
+
// borrows the package config; the package/project lifecycle owns release.
|
|
207
222
|
const runtime = new Runtime({
|
|
208
223
|
urlReader: interceptingReader,
|
|
209
|
-
|
|
224
|
+
config: pkg.getMalloyConfig(),
|
|
210
225
|
});
|
|
211
226
|
|
|
212
227
|
// Attempt to compile
|
|
@@ -254,14 +269,66 @@ export class Project {
|
|
|
254
269
|
return connection;
|
|
255
270
|
}
|
|
256
271
|
|
|
257
|
-
public getMalloyConnection(connectionName: string)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
272
|
+
public async getMalloyConnection(connectionName: string) {
|
|
273
|
+
return this.malloyConfig.malloyConfig.connections.lookupConnection(
|
|
274
|
+
connectionName,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public getProjectMalloyConfig() {
|
|
279
|
+
return this.malloyConfig.malloyConfig;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public async runConnectionUpdateExclusive<T>(
|
|
283
|
+
fn: () => Promise<T>,
|
|
284
|
+
): Promise<T> {
|
|
285
|
+
return this.connectionMutex.runExclusive(fn);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private retireConnectionGeneration(
|
|
289
|
+
label: string,
|
|
290
|
+
releaseConnections: () => Promise<void>,
|
|
291
|
+
): void {
|
|
292
|
+
const generation: RetiredConnectionGeneration = {
|
|
293
|
+
label,
|
|
294
|
+
releaseConnections,
|
|
295
|
+
};
|
|
296
|
+
generation.timer = setTimeout(() => {
|
|
297
|
+
void this.releaseRetiredConnectionGeneration(generation);
|
|
298
|
+
}, RETIRED_CONNECTION_DRAIN_MS);
|
|
299
|
+
(
|
|
300
|
+
generation.timer as ReturnType<typeof setTimeout> & {
|
|
301
|
+
unref?: () => void;
|
|
302
|
+
}
|
|
303
|
+
).unref?.();
|
|
304
|
+
this.retiredConnectionGenerations.add(generation);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async releaseRetiredConnectionGeneration(
|
|
308
|
+
generation: RetiredConnectionGeneration,
|
|
309
|
+
): Promise<void> {
|
|
310
|
+
if (!this.retiredConnectionGenerations.delete(generation)) return;
|
|
311
|
+
|
|
312
|
+
if (generation.timer) {
|
|
313
|
+
clearTimeout(generation.timer);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await generation.releaseConnections();
|
|
318
|
+
} catch (error) {
|
|
319
|
+
logger.error(
|
|
320
|
+
`Error releasing retired connection generation ${generation.label}`,
|
|
321
|
+
{ error },
|
|
262
322
|
);
|
|
263
323
|
}
|
|
264
|
-
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async releaseAllRetiredConnectionGenerations(): Promise<void> {
|
|
327
|
+
await Promise.all(
|
|
328
|
+
[...this.retiredConnectionGenerations].map((generation) =>
|
|
329
|
+
this.releaseRetiredConnectionGeneration(generation),
|
|
330
|
+
),
|
|
331
|
+
);
|
|
265
332
|
}
|
|
266
333
|
|
|
267
334
|
public async listPackages(): Promise<ApiPackage[]> {
|
|
@@ -356,8 +423,13 @@ export class Project {
|
|
|
356
423
|
this.projectName,
|
|
357
424
|
packageName,
|
|
358
425
|
packagePath,
|
|
359
|
-
this.
|
|
426
|
+
() => this.malloyConfig.malloyConfig,
|
|
360
427
|
);
|
|
428
|
+
if (existingPackage !== undefined && reload) {
|
|
429
|
+
this.retireConnectionGeneration(`package ${packageName}`, () =>
|
|
430
|
+
existingPackage.getMalloyConfig().releaseConnections(),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
361
433
|
this.packages.set(packageName, _package);
|
|
362
434
|
|
|
363
435
|
// Set package status to serving
|
|
@@ -391,7 +463,7 @@ export class Project {
|
|
|
391
463
|
`Adding package ${packageName} to project ${this.projectName}`,
|
|
392
464
|
{
|
|
393
465
|
packagePath,
|
|
394
|
-
|
|
466
|
+
malloyConfig: this.malloyConfig.malloyConfig,
|
|
395
467
|
},
|
|
396
468
|
);
|
|
397
469
|
this.setPackageStatus(packageName, PackageStatus.LOADING);
|
|
@@ -402,7 +474,7 @@ export class Project {
|
|
|
402
474
|
this.projectName,
|
|
403
475
|
packageName,
|
|
404
476
|
packagePath,
|
|
405
|
-
this.
|
|
477
|
+
() => this.malloyConfig.malloyConfig,
|
|
406
478
|
),
|
|
407
479
|
);
|
|
408
480
|
} catch (error) {
|
|
@@ -514,6 +586,8 @@ export class Project {
|
|
|
514
586
|
this.setPackageStatus(packageName, PackageStatus.UNLOADING);
|
|
515
587
|
}
|
|
516
588
|
|
|
589
|
+
await _package.getMalloyConfig().releaseConnections();
|
|
590
|
+
|
|
517
591
|
try {
|
|
518
592
|
await fs.promises.rm(path.join(this.projectPath, packageName), {
|
|
519
593
|
recursive: true,
|
|
@@ -532,33 +606,37 @@ export class Project {
|
|
|
532
606
|
}
|
|
533
607
|
|
|
534
608
|
public updateConnections(
|
|
535
|
-
|
|
536
|
-
|
|
609
|
+
malloyConfig: ProjectMalloyConfig,
|
|
610
|
+
_apiConnections?: ApiConnection[],
|
|
611
|
+
afterPreviousRelease?: () => Promise<void>,
|
|
537
612
|
): void {
|
|
538
|
-
|
|
539
|
-
this.
|
|
613
|
+
const previousMalloyConfig = this.malloyConfig;
|
|
614
|
+
this.malloyConfig = malloyConfig;
|
|
615
|
+
this.apiConnections = malloyConfig.apiConnections;
|
|
616
|
+
|
|
617
|
+
if (previousMalloyConfig !== malloyConfig) {
|
|
618
|
+
this.retireConnectionGeneration(
|
|
619
|
+
`project ${this.projectName}`,
|
|
620
|
+
async () => {
|
|
621
|
+
await previousMalloyConfig.releaseConnections();
|
|
622
|
+
await afterPreviousRelease?.();
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
void afterPreviousRelease?.();
|
|
627
|
+
}
|
|
540
628
|
}
|
|
541
629
|
|
|
542
630
|
public async deleteConnection(connectionName: string): Promise<void> {
|
|
543
|
-
this.malloyConnections.get(connectionName)?.close();
|
|
544
|
-
const isDeleted = this.malloyConnections.delete(connectionName);
|
|
545
|
-
|
|
546
631
|
const index = this.apiConnections.findIndex(
|
|
547
632
|
(conn) => conn.name === connectionName,
|
|
548
633
|
);
|
|
549
634
|
|
|
550
|
-
const connectionType = this.apiConnections[index]?.type;
|
|
551
|
-
if (connectionType === "duckdb") {
|
|
552
|
-
await this.deleteDuckDBConnection(connectionName);
|
|
553
|
-
} else if (connectionType === "ducklake") {
|
|
554
|
-
await this.deleteDuckLakeConnection(connectionName);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
635
|
if (index !== -1) {
|
|
558
636
|
this.apiConnections.splice(index, 1);
|
|
559
637
|
}
|
|
560
638
|
|
|
561
|
-
if (
|
|
639
|
+
if (index !== -1) {
|
|
562
640
|
logger.info(
|
|
563
641
|
`Removed connection ${connectionName} from project ${this.projectName}`,
|
|
564
642
|
);
|
|
@@ -569,24 +647,36 @@ export class Project {
|
|
|
569
647
|
}
|
|
570
648
|
}
|
|
571
649
|
|
|
572
|
-
public closeAllConnections(): void {
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
650
|
+
public async closeAllConnections(): Promise<void> {
|
|
651
|
+
// Release the package-scoped MalloyConfigs (each holds the package's own
|
|
652
|
+
// sandbox `duckdb` connection) before tearing down the project config
|
|
653
|
+
// they wrap. Without this, hard unload leaks per-package DuckDB handles.
|
|
654
|
+
const packageReleases = await Promise.allSettled(
|
|
655
|
+
Array.from(this.packages.values(), (pkg) =>
|
|
656
|
+
pkg.getMalloyConfig().releaseConnections(),
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
for (const result of packageReleases) {
|
|
660
|
+
if (result.status === "rejected") {
|
|
581
661
|
logger.error(
|
|
582
|
-
`Error closing
|
|
583
|
-
{ error },
|
|
662
|
+
`Error closing package connections for project ${this.projectName}`,
|
|
663
|
+
{ error: result.reason },
|
|
584
664
|
);
|
|
585
665
|
}
|
|
586
666
|
}
|
|
667
|
+
this.packages.clear();
|
|
668
|
+
this.packageStatuses.clear();
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
await this.malloyConfig.releaseConnections();
|
|
672
|
+
} catch (error) {
|
|
673
|
+
logger.error(
|
|
674
|
+
`Error closing connections for project ${this.projectName}`,
|
|
675
|
+
{ error },
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
await this.releaseAllRetiredConnectionGenerations();
|
|
587
679
|
|
|
588
|
-
// Clear connection maps
|
|
589
|
-
this.malloyConnections.clear();
|
|
590
680
|
this.apiConnections = [];
|
|
591
681
|
|
|
592
682
|
logger.info(`Closed all connections for project ${this.projectName}`);
|
|
@@ -605,29 +695,17 @@ export class Project {
|
|
|
605
695
|
this.projectPath,
|
|
606
696
|
`${connectionName}.duckdb`,
|
|
607
697
|
);
|
|
608
|
-
|
|
609
|
-
.
|
|
610
|
-
.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
logger.error(
|
|
620
|
-
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
621
|
-
{ error },
|
|
622
|
-
);
|
|
623
|
-
});
|
|
624
|
-
})
|
|
625
|
-
.catch((error) => {
|
|
626
|
-
logger.error(
|
|
627
|
-
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
628
|
-
{ error },
|
|
629
|
-
);
|
|
630
|
-
});
|
|
698
|
+
try {
|
|
699
|
+
await fs.promises.rm(duckdbPath, { force: true });
|
|
700
|
+
logger.info(
|
|
701
|
+
`Removed DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
702
|
+
);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
logger.error(
|
|
705
|
+
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
706
|
+
{ error },
|
|
707
|
+
);
|
|
708
|
+
}
|
|
631
709
|
}
|
|
632
710
|
|
|
633
711
|
public async deleteDuckLakeConnection(
|