Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 3f5ab99

Browse files
committed
Ensure Miniflare instances disposed
1 parent f6e1543 commit 3f5ab99

File tree

8 files changed

+72
-2
lines changed

8 files changed

+72
-2
lines changed

ava.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const rewritePaths = Object.fromEntries(
1111
export default {
1212
files: ["packages/*/test/**/*.spec.ts"],
1313
nodeArguments: ["--no-warnings", "--experimental-vm-modules"],
14+
require: ["./packages/miniflare/test/setup.mjs"],
1415
workerThreads: inspector.url() === undefined,
1516
typescript: {
1617
compile: false,

packages/miniflare/src/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,15 @@ function safeReadableStreamFrom(iterable: AsyncIterable<Uint8Array>) {
448448
);
449449
}
450450

451+
// Maps `Miniflare` instances to stack traces for thier construction. Used to identify un-`dispose()`d instances.
452+
let maybeInstanceRegistry:
453+
| Map<Miniflare, string /* constructionStack */>
454+
| undefined;
455+
/** @internal */
456+
export function _initialiseInstanceRegistry() {
457+
return (maybeInstanceRegistry = new Map());
458+
}
459+
451460
export class Miniflare {
452461
readonly #gatewayFactories: PluginGatewayFactories;
453462
readonly #routers: PluginRouters;
@@ -496,6 +505,15 @@ export class Miniflare {
496505
const [sharedOpts, workerOpts] = validateOptions(opts);
497506
this.#sharedOpts = sharedOpts;
498507
this.#workerOpts = workerOpts;
508+
509+
// Add to registry after initial options validation, before any servers/
510+
// child processes are started
511+
if (maybeInstanceRegistry !== undefined) {
512+
const object = { name: "Miniflare", stack: "" };
513+
Error.captureStackTrace(object, Miniflare);
514+
maybeInstanceRegistry.set(this, object.stack);
515+
}
516+
499517
this.#log = this.#sharedOpts.core.log ?? new NoOpLog();
500518
this.#timers = this.#sharedOpts.core.timers ?? defaultTimers;
501519
this.#host = this.#sharedOpts.core.host ?? "127.0.0.1";
@@ -1247,6 +1265,10 @@ export class Miniflare {
12471265
await this.#stopLoopbackServer();
12481266
// `rm -rf ${#tmpPath}`, this won't throw if `#tmpPath` doesn't exist
12491267
await fs.promises.rm(this.#tmpPath, { force: true, recursive: true });
1268+
1269+
// Remove from instance registry as last step in `finally`, to make sure
1270+
// all dispose steps complete
1271+
maybeInstanceRegistry?.delete(this);
12501272
}
12511273
}
12521274
}

packages/miniflare/test/index.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ test("Miniflare: accepts https requests", async (t) => {
528528
t.assert(log.logs[0][1].startsWith("Ready on https://"));
529529
});
530530

531-
test("Miniflare: Manually triggered scheduled events", async (t) => {
531+
test("Miniflare: manually triggered scheduled events", async (t) => {
532532
const log = new TestLog(t);
533533

534534
const mf = new Miniflare({
@@ -545,6 +545,7 @@ test("Miniflare: Manually triggered scheduled events", async (t) => {
545545
}
546546
}`,
547547
});
548+
t.teardown(() => mf.dispose());
548549

549550
let res = await mf.dispatchFetch("http://localhost");
550551
t.is(await res.text(), "false");

packages/miniflare/test/plugins/core/errors/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ addEventListener("fetch", (event) => {
133133
},
134134
],
135135
});
136+
t.teardown(() => mf.dispose());
136137

137138
// Check service-workers source mapped
138139
let error = await t.throwsAsync(mf.dispatchFetch("http://localhost"), {

packages/miniflare/test/plugins/core/proxy/client.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ test("ProxyClient: supports service bindings with WebSockets", async (t) => {
3535
},
3636
},
3737
});
38+
t.teardown(() => mf.dispose());
39+
3840
const { CUSTOM } = await mf.getBindings<{
3941
CUSTOM: ReplaceWorkersTypes<Fetcher>;
4042
}>();
@@ -53,6 +55,8 @@ test("ProxyClient: supports service bindings with WebSockets", async (t) => {
5355

5456
test("ProxyClient: supports serialising multiple ReadableStreams, Blobs and Files", async (t) => {
5557
const mf = new Miniflare({ script: nullScript });
58+
t.teardown(() => mf.dispose());
59+
5660
const client = await mf._getProxyClient();
5761
const IDENTITY = client.env.IDENTITY as {
5862
asyncIdentity<Args extends any[]>(...args: Args): Promise<Args>;
@@ -130,6 +134,8 @@ test("ProxyClient: poisons dependent proxies after setOptions()/dispose()", asyn
130134
});
131135
test("ProxyClient: logging proxies provides useful information", async (t) => {
132136
const mf = new Miniflare({ script: nullScript });
137+
t.teardown(() => mf.dispose());
138+
133139
const caches = await mf.getCaches();
134140
const inspectOpts: util.InspectOptions = { colors: false };
135141
t.is(
@@ -160,6 +166,7 @@ test("ProxyClient: stack traces don't include internal implementation", async (t
160166
// https://developers.cloudflare.com/workers/configuration/compatibility-dates/#do-not-throw-from-async-functions
161167
compatibilityFlags: ["capture_async_api_throws"],
162168
});
169+
t.teardown(() => mf.dispose());
163170

164171
const ns = await mf.getDurableObjectNamespace("OBJECT");
165172
const caches = await mf.getCaches();
@@ -189,6 +196,8 @@ test("ProxyClient: stack traces don't include internal implementation", async (t
189196
});
190197
test("ProxyClient: can access ReadableStream property multiple times", async (t) => {
191198
const mf = new Miniflare({ script: nullScript, r2Buckets: ["BUCKET"] });
199+
t.teardown(() => mf.dispose());
200+
192201
const bucket = await mf.getR2Bucket("BUCKET");
193202
await bucket.put("key", "value");
194203
const objectBody = await bucket.get("key");
@@ -198,6 +207,8 @@ test("ProxyClient: can access ReadableStream property multiple times", async (t)
198207
});
199208
test("ProxyClient: returns empty ReadableStream synchronously", async (t) => {
200209
const mf = new Miniflare({ script: nullScript, r2Buckets: ["BUCKET"] });
210+
t.teardown(() => mf.dispose());
211+
201212
const bucket = await mf.getR2Bucket("BUCKET");
202213
await bucket.put("key", "");
203214
const objectBody = await bucket.get("key");

packages/miniflare/test/plugins/queues/index.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ test("flushes partial and full batches", async (t) => {
6666
},
6767
],
6868
});
69+
t.teardown(() => mf.dispose());
70+
6971
async function send(message: unknown) {
7072
await mf.dispatchFetch("http://localhost/send", {
7173
method: "POST",
@@ -255,6 +257,7 @@ test("sends all structured cloneable types", async (t) => {
255257
},
256258
],
257259
});
260+
t.teardown(() => mf.dispose());
258261

259262
await mf.dispatchFetch("http://localhost");
260263
timers.timestamp += 1000;
@@ -326,6 +329,8 @@ test("retries messages", async (t) => {
326329
}
327330
}`,
328331
});
332+
t.teardown(() => mf.dispose());
333+
329334
async function sendBatch(...messages: string[]) {
330335
await mf.dispatchFetch("http://localhost", {
331336
method: "POST",
@@ -546,6 +551,8 @@ test("moves to dead letter queue", async (t) => {
546551
}
547552
}`,
548553
});
554+
t.teardown(() => mf.dispose());
555+
549556
async function sendBatch(...messages: string[]) {
550557
await mf.dispatchFetch("http://localhost", {
551558
method: "POST",
@@ -648,6 +655,8 @@ test("operations permit strange queue names", async (t) => {
648655
}
649656
}`,
650657
});
658+
t.teardown(() => mf.dispose());
659+
651660
await mf.dispatchFetch("http://localhost");
652661
timers.timestamp += 1000;
653662
await timers.waitForTasks();
@@ -718,6 +727,8 @@ test("supports message contentTypes", async (t) => {
718727
},
719728
};`,
720729
});
730+
t.teardown(() => mf.dispose());
731+
721732
const res = await mf.dispatchFetch("http://localhost");
722733
await res.arrayBuffer(); // (drain)
723734
timers.timestamp += 1000;

packages/miniflare/test/setup.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { _initialiseInstanceRegistry } from "miniflare";
2+
3+
const registry = _initialiseInstanceRegistry();
4+
const bigSeparator = "=".repeat(80);
5+
const separator = "-".repeat(80);
6+
7+
// `process.on("exit")` is more like `worker_thread.on(`exit`)` here. It will
8+
// be called once AVA's finished running tests and `after` hooks. Note we can't
9+
// use an `after` hook here, as that would run before `miniflareTest`'s
10+
// `after` hooks to dispose their `Miniflare` instances.
11+
process.on("exit", () => {
12+
if (registry.size === 0) return;
13+
14+
// If there are Miniflare instances that weren't disposed, throw
15+
const s = registry.size === 1 ? "" : "s";
16+
const was = registry.size === 1 ? "was" : "were";
17+
const message = `Found ${registry.size} Miniflare instance${s} that ${was} not dispose()d`;
18+
const stacks = Array.from(registry.values()).join(`\n${separator}\n`);
19+
console.log(
20+
[bigSeparator, message, separator, stacks, bigSeparator].join("\n")
21+
);
22+
throw new Error(message);
23+
});

packages/miniflare/test/test-shared/miniflare.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,6 @@ export function miniflareTest<
149149
t.context.mf.setOptions({ ...userOpts, ...opts } as MiniflareOptions);
150150
t.context.url = await t.context.mf.ready;
151151
});
152-
test.after((t) => t.context.mf.dispose());
152+
test.after.always((t) => t.context.mf.dispose());
153153
return test;
154154
}

0 commit comments

Comments
 (0)