Skip to content

Commit 11eb627

Browse files
committed
fix(cloudflare): Route DO teardown through original waitUntil
Avoid a flush-lock deadlock that prevents WebSocket hibernation. fix: #22328 fix: JS-3067
1 parent e36268b commit 11eb627

3 files changed

Lines changed: 58 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
6+
- fix(cloudflare): Route Durable Object teardown through the original `waitUntil` to avoid a flush-lock deadlock that prevented WebSocket hibernation ([#22328](https://github.com/getsentry/sentry-javascript/issues/22328))
67

78
## 10.66.0
89

packages/cloudflare/src/wrapMethodWithSentry.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
withScope,
1616
} from '@sentry/core';
1717
import type { CloudflareOptions } from './client';
18-
import { flushAndDispose } from './flush';
18+
import type { ExecutionContextCompat } from './executionContext';
19+
import { flushAndDispose, getOriginalWaitUntil } from './flush';
1920
import { ensureInstrumented } from './instrument';
2021
import { init } from './sdk';
2122
import { extractRpcMeta } from './utils/rpcMeta';
@@ -117,7 +118,10 @@ export function wrapMethodWithSentry<T extends OriginalMethod>(
117118
// see: https://github.com/getsentry/sentry-javascript/issues/13217
118119
const context: typeof wrapperOptions.context | undefined = wrapperOptions.context;
119120

120-
const waitUntil = context?.waitUntil?.bind?.(context);
121+
// see: https://github.com/getsentry/sentry-javascript/issues/22328
122+
const waitUntil = context
123+
? getOriginalWaitUntil(context as ExecutionContextCompat)?.bind(context)
124+
: undefined;
121125
const storage = resolveOriginalStorage(context, thisArg);
122126

123127
let scopeClient = scope.getClient();

packages/cloudflare/test/wrapMethodWithSentry.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as sentryCore from '@sentry/core';
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { makeFlushLock } from '../src/flush';
34
import { getInstrumented } from '../src/instrument';
45
import * as sdk from '../src/sdk';
56
import { wrapMethodWithSentry } from '../src/wrapMethodWithSentry';
@@ -694,3 +695,53 @@ describe('wrapMethodWithSentry', () => {
694695
});
695696
});
696697
});
698+
699+
describe('wrapMethodWithSentry waitUntil teardown (hibernation regression)', () => {
700+
beforeEach(() => {
701+
vi.clearAllMocks();
702+
});
703+
704+
afterEach(() => {
705+
vi.restoreAllMocks();
706+
});
707+
708+
// Regression for #22328
709+
it('does not deadlock teardown against a concurrent waitUntil task', async () => {
710+
const waitUntilPromises: Array<Promise<unknown>> = [];
711+
const context = {
712+
waitUntil: vi.fn((promise: Promise<unknown>) => {
713+
waitUntilPromises.push(promise);
714+
}),
715+
} as unknown as ExecutionContext;
716+
717+
// A prior invocation instrumented context.waitUntil
718+
// (installs the flush lock)
719+
const lock = makeFlushLock(context);
720+
721+
// A concurrent, in-flight waitUntil task holds the flush lock
722+
let resolveUserTask!: () => void;
723+
const userTask = new Promise<void>(resolve => {
724+
resolveUserTask = resolve;
725+
});
726+
context.waitUntil(userTask);
727+
728+
// flush waits for the flush lock to drain.
729+
mocks.flush.mockImplementationOnce(async () => {
730+
await lock.finalize();
731+
return true;
732+
});
733+
734+
const wrapped = wrapMethodWithSentry(
735+
{ options: { dsn: 'https://test@sentry.io/123' }, context, spanName: 'webSocketMessage' },
736+
vi.fn().mockResolvedValue('ok'),
737+
);
738+
739+
await wrapped();
740+
741+
// Releasing the concurrent task drains the lock
742+
resolveUserTask();
743+
744+
await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined();
745+
expect(mocks.flush).toHaveBeenCalled();
746+
});
747+
});

0 commit comments

Comments
 (0)