-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Labels
bugSomething that isn't workingSomething that isn't workingminiflareRelating to MiniflareRelating to MiniflarevitestRelating to the Workers Vitest integrationRelating to the Workers Vitest integration
Description
What versions & operating system are you using?
Node v22, Vitest v3, Miniflare v4.20251008.0
Please provide a link to a minimal reproduction
No response
Describe the Bug
Calling Durable Object method that never resolves hangs tests and can not be raced against using Promise.race()
It does however allow for racing when proxied via a regular worker.
Tests that have .skip() are the ones that hang
/** *
* Issue: Calling a Durable Object method that blocks internally (waiting on a promise)
* in test makes the test hang
*
* Expected: The blockedOp() call should allow Promise.race() to timeout first when release() is never called.
*
* Actual: Regardless of not being awaited the call to the .blockedOp hanges the test as well as prevents the vitest from timing out.
*/
import { it, expect, afterEach, beforeEach } from 'vitest';
import { Miniflare } from 'miniflare';
let mf: Miniflare;
beforeEach(async () => {
mf = new Miniflare({
workers: [
{
name: 'do-worker',
modules: true,
script: `
import { DurableObject } from 'cloudflare:workers';
export class BlockingDO extends DurableObject {
locks = new Map();
async blockedOp(n, lock) {
console.log('blockedOp called, waiting for lock:', lock);
await new Promise((resolve) => {
this.locks.set(lock, () => {
console.log('lock released:', lock);
resolve(lock);
});
});
console.log('blockedOp completing');
return n + 2;
}
async release(lock) {
console.log('release called for:', lock);
const releaseFn = this.locks.get(lock);
if (releaseFn) {
releaseFn();
this.locks.delete(lock);
}
}
}
`,
durableObjects: {
BLOCKING_DO: 'BlockingDO',
},
compatibilityDate: '2025-04-23',
},
{
name: 'worker',
modules: true,
bindings: { DO: { durableObjectNamespace: 'BLOCKING_DO' } },
durableObjects: { BLOCKING_DO: { className: 'BlockingDO', scriptName: 'do-worker' } },
compatibilityDate: '2025-04-23',
script: `
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/blockedOp') {
const value = parseInt(url.searchParams.get('value'), 10);
const lock = url.searchParams.get('lock');
const id = env.BLOCKING_DO.idFromName('test');
const stub = env.BLOCKING_DO.get(id);
const result = await stub.blockedOp(value, lock);
return new Response(JSON.stringify({ result }), {
headers: { 'Content-Type': 'application/json' }
});
} else if (url.pathname === '/release') {
const lock = url.searchParams.get('lock');
const id = env.BLOCKING_DO.idFromName('test');
const stub = env.BLOCKING_DO.get(id);
await stub.release(lock);
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
} else {
return new Response('Not Found', { status: 404 });
}
}
}
`,
},
],
});
});
afterEach(async () => {
await mf.dispose();
});
it.skip('should demonstrate that blockedOp hangs without release', async () => {
const ns = await mf.getDurableObjectNamespace('BLOCKING_DO');
const id = ns.idFromName('test');
const stub = ns.get(id);
// Call blockedOp but don't await it
const blockedPromise = stub.blockedOp(5, 'lock-1');
// Race the blocked operation against a timeout
// We expect the timeout to win since we never call release()
const raced = await Promise.race([
blockedPromise.then((result) => ({ type: 'resolved', result })),
new Promise((resolve) => setTimeout(() => resolve({ type: 'timeout' }), 100)),
]);
// Never runs
expect(raced).toEqual({ type: 'timeout' });
}, 5000);
it.skip('should demonstrate that release allows blockedOp to complete', async () => {
const ns = await mf.getDurableObjectNamespace('BLOCKING_DO');
const id = ns.idFromName('test');
const stub = ns.get(id);
const blockedPromise = stub.blockedOp(10, 'lock-2');
// Release the lock after 50ms
setTimeout(() => {
stub.release('lock-2');
}, 50);
const result = await blockedPromise;
// Never runs
expect(result).toBe(12);
}, 5000);
it('should demonstrate that blockedOp hangs without release (via HTTP)', async () => {
const worker = await mf.getWorker('worker');
// Call blockedOp via HTTP
const blockedPromise = worker.fetch('http://localhost/blockedOp?value=5&lock=http-lock-1').then((resp) => resp.json());
// Race against timeout
const raced = await Promise.race([
blockedPromise.then((result) => ({ type: 'resolved', result })),
new Promise((resolve) => setTimeout(() => resolve({ type: 'timeout' }), 100)),
]);
// Expected: timeout wins because we never call release
expect(raced).toEqual({ type: 'timeout' });
}, 5000);
it('should demonstrate that release allows blockedOp to complete (via HTTP)', async () => {
const worker = await mf.getWorker('worker');
const blockedPromise = worker.fetch('http://localhost/blockedOp?value=10&lock=http-lock-2').then((resp) => resp.json());
// Release the lock after 50ms
setTimeout(() => {
worker.fetch('http://localhost/release?lock=http-lock-2');
}, 50);
const data = await blockedPromise;
expect(data).toEqual({ result: 12 });
}, 5000);Please provide any relevant error logs
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething that isn't workingSomething that isn't workingminiflareRelating to MiniflareRelating to MiniflarevitestRelating to the Workers Vitest integrationRelating to the Workers Vitest integration
Type
Projects
Status
Done