Skip to content

Commit 2043804

Browse files
chore: add orphan exports cleanup
1 parent 69a3d45 commit 2043804

File tree

10 files changed

+368
-295
lines changed

10 files changed

+368
-295
lines changed

src/common/exportsManager.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,23 @@ type AvailableExport = Pick<StoredExport, "exportName" | "exportURI" | "exportPa
5858
export type ExportsManagerConfig = Pick<UserConfig, "exportsPath" | "exportTimeoutMs" | "exportCleanupIntervalMs">;
5959

6060
type ExportsManagerEvents = {
61+
initialized: [];
6162
"export-expired": [string];
6263
"export-available": [string];
6364
};
6465

6566
export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
67+
private wasInitialized: boolean = false;
6668
private storedExports: Record<StoredExport["exportName"], StoredExport> = {};
6769
private exportsCleanupInProgress: boolean = false;
68-
private exportsCleanupInterval: NodeJS.Timeout;
69-
private exportsDirectoryPath: string;
70+
private exportsCleanupInterval?: NodeJS.Timeout;
7071

71-
constructor(
72-
sessionId: string,
72+
private constructor(
73+
private readonly exportsDirectoryPath: string,
7374
private readonly config: ExportsManagerConfig,
7475
private readonly logger: LoggerBase
7576
) {
7677
super();
77-
this.exportsDirectoryPath = path.join(this.config.exportsPath, sessionId);
78-
this.exportsCleanupInterval = setInterval(
79-
() => void this.cleanupExpiredExports(),
80-
this.config.exportCleanupIntervalMs
81-
);
8278
}
8379

8480
public get availableExports(): AvailableExport[] {
@@ -96,6 +92,24 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
9692
}));
9793
}
9894

95+
protected async init(): Promise<void> {
96+
if (!this.wasInitialized) {
97+
// If a server is killed, the exports from the active session on
98+
// that server won't be cleaned up. We tackle that by attempting to
99+
// clean any directory on the exportsPath when the server starts and
100+
// initializes the ExportsManager.
101+
await this.cleanupOrphanExportsDirectories();
102+
103+
this.exportsCleanupInterval = setInterval(
104+
() => void this.cleanupExpiredExports(),
105+
this.config.exportCleanupIntervalMs
106+
);
107+
108+
this.wasInitialized = true;
109+
this.emit("initialized");
110+
}
111+
}
112+
99113
public async close(): Promise<void> {
100114
try {
101115
clearInterval(this.exportsCleanupInterval);
@@ -302,6 +316,25 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
302316
}
303317
}
304318
}
319+
320+
private async cleanupOrphanExportsDirectories(): Promise<void> {
321+
try {
322+
await fs.rm(this.exportsDirectoryPath, { force: true, recursive: true });
323+
} catch (error) {
324+
this.logger.error({
325+
id: LogId.exportInitError,
326+
context: "Error while cleaning orphan exports",
327+
message: error instanceof Error ? error.message : String(error),
328+
});
329+
}
330+
}
331+
332+
static init(sessionId: string, config: ExportsManagerConfig, logger: LoggerBase): ExportsManager {
333+
const exportsDirectoryPath = path.join(config.exportsPath, sessionId);
334+
const exportsManager = new ExportsManager(exportsDirectoryPath, config, logger);
335+
void exportsManager.init();
336+
return exportsManager;
337+
}
305338
}
306339

307340
/**

src/common/logger.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ export const LogId = {
5454
exportCreationError: mongoLogId(1_007_002),
5555
exportCreationCleanupError: mongoLogId(1_007_003),
5656
exportReadError: mongoLogId(1_007_004),
57-
exportCloseError: mongoLogId(1_007_005),
58-
exportedDataListError: mongoLogId(1_007_006),
59-
exportedDataAutoCompleteError: mongoLogId(1_007_007),
57+
exportInitError: mongoLogId(1_007_005),
58+
exportCloseError: mongoLogId(1_007_006),
59+
exportedDataListError: mongoLogId(1_007_007),
60+
exportedDataAutoCompleteError: mongoLogId(1_007_008),
6061
} as const;
6162

6263
interface LogPayload {

src/transports/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export abstract class TransportRunnerBase {
4444

4545
const logger = new CompositeLogger(...loggers);
4646
const sessionId = new ObjectId().toString();
47-
const exportsManager = new ExportsManager(sessionId, userConfig, logger);
47+
const exportsManager = ExportsManager.init(sessionId, userConfig, logger);
4848
const connectionManager = new ConnectionManager();
4949

5050
const session = new Session({

tests/integration/helpers.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { Telemetry } from "../../src/telemetry/telemetry.js";
1010
import { config } from "../../src/common/config.js";
1111
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
1212
import { ConnectionManager } from "../../src/common/connectionManager.js";
13-
import { CompositeLogger } from "../../src/common/logger.js";
14-
import { ExportsManager } from "../../src/common/exportsManager.js";
13+
import { CompositeLogger, LoggerBase } from "../../src/common/logger.js";
14+
import { ExportsManager, ExportsManagerConfig } from "../../src/common/exportsManager.js";
1515

1616
interface ParameterInfo {
1717
name: string;
@@ -59,7 +59,7 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
5959

6060
const logger = new CompositeLogger();
6161
const sessionId = new ObjectId().toString();
62-
const exportsManager = new ExportsManager(sessionId, userConfig, logger);
62+
const exportsManager = await exportsManagerForTests(sessionId, userConfig, logger);
6363
const connectionManager = new ConnectionManager();
6464

6565
const session = new Session({
@@ -294,3 +294,14 @@ export function resourceChangedNotification(client: Client, uri: string): Promis
294294
});
295295
});
296296
}
297+
298+
export async function exportsManagerForTests(
299+
sessionId: string,
300+
exportsConfig: ExportsManagerConfig,
301+
logger: LoggerBase
302+
): Promise<ExportsManager> {
303+
return new Promise((resolve) => {
304+
const manager = ExportsManager.init(sessionId, exportsConfig, logger);
305+
manager.once("initialized", () => resolve(manager));
306+
});
307+
}

tests/integration/resources/exportedData.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
import path from "path";
2+
import fs from "fs/promises";
13
import { Long } from "bson";
2-
import { describe, expect, it, beforeEach } from "vitest";
4+
import { describe, expect, it, beforeEach, afterAll } from "vitest";
35
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
46
import { defaultTestConfig, resourceChangedNotification, timeout } from "../helpers.js";
57
import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js";
68
import { contentWithResourceURILink } from "../tools/mongodb/read/export.test.js";
9+
import { UserConfig } from "../../../src/lib.js";
10+
11+
const userConfig: UserConfig = {
12+
...defaultTestConfig,
13+
exportsPath: path.join(path.dirname(defaultTestConfig.exportsPath), `exports-${Date.now()}`),
14+
exportTimeoutMs: 200,
15+
exportCleanupIntervalMs: 300,
16+
};
717

818
describeWithMongoDB(
919
"exported-data resource",
@@ -19,6 +29,10 @@ describeWithMongoDB(
1929
]);
2030
});
2131

32+
afterAll(async () => {
33+
await fs.rm(userConfig.exportsPath, { recursive: true, force: true });
34+
});
35+
2236
it("should be able to list resource template", async () => {
2337
await integration.connectMcpClient();
2438
const response = await integration.mcpClient().listResourceTemplates();
@@ -123,11 +137,5 @@ describeWithMongoDB(
123137
});
124138
});
125139
},
126-
() => {
127-
return {
128-
...defaultTestConfig,
129-
exportTimeoutMs: 200,
130-
exportCleanupIntervalMs: 300,
131-
};
132-
}
140+
() => userConfig
133141
);

tests/integration/telemetry.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import nodeMachineId from "node-machine-id";
66
import { describe, expect, it } from "vitest";
77
import { CompositeLogger } from "../../src/common/logger.js";
88
import { ConnectionManager } from "../../src/common/connectionManager.js";
9-
import { ExportsManager } from "../../src/common/exportsManager.js";
9+
import { exportsManagerForTests } from "./helpers.js";
1010

1111
describe("Telemetry", () => {
1212
it("should resolve the actual machine ID", async () => {
@@ -20,7 +20,7 @@ describe("Telemetry", () => {
2020
apiBaseUrl: "",
2121
logger: new CompositeLogger(),
2222
sessionId: "1FOO",
23-
exportsManager: new ExportsManager("1FOO", config, logger),
23+
exportsManager: await exportsManagerForTests("1FOO", config, logger),
2424
connectionManager: new ConnectionManager(),
2525
}),
2626
config

0 commit comments

Comments
 (0)