Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
- 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))

## 10.66.0

Expand Down
8 changes: 6 additions & 2 deletions packages/cloudflare/src/wrapMethodWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
withScope,
} from '@sentry/core';
import type { CloudflareOptions } from './client';
import { flushAndDispose } from './flush';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { ensureInstrumented } from './instrument';
import { init } from './sdk';
import { extractRpcMeta } from './utils/rpcMeta';
Expand Down Expand Up @@ -117,7 +118,10 @@ export function wrapMethodWithSentry<T extends OriginalMethod>(
// see: https://github.com/getsentry/sentry-javascript/issues/13217
const context: typeof wrapperOptions.context | undefined = wrapperOptions.context;

const waitUntil = context?.waitUntil?.bind?.(context);
// see: https://github.com/getsentry/sentry-javascript/issues/22328
const waitUntil = context
? getOriginalWaitUntil(context as ExecutionContextCompat)?.bind(context)
: undefined;
const storage = resolveOriginalStorage(context, thisArg);

let scopeClient = scope.getClient();
Expand Down
51 changes: 51 additions & 0 deletions packages/cloudflare/test/wrapMethodWithSentry.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as sentryCore from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { makeFlushLock } from '../src/flush';
import { getInstrumented } from '../src/instrument';
import * as sdk from '../src/sdk';
import { wrapMethodWithSentry } from '../src/wrapMethodWithSentry';
Expand Down Expand Up @@ -694,3 +695,53 @@ describe('wrapMethodWithSentry', () => {
});
});
});

describe('wrapMethodWithSentry waitUntil teardown (hibernation regression)', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

// Regression for #22328
it('does not deadlock teardown against a concurrent waitUntil task', async () => {
const waitUntilPromises: Array<Promise<unknown>> = [];
const context = {
waitUntil: vi.fn((promise: Promise<unknown>) => {
waitUntilPromises.push(promise);
}),
} as unknown as ExecutionContext;

// A prior invocation instrumented context.waitUntil
// (installs the flush lock)
const lock = makeFlushLock(context);

// A concurrent, in-flight waitUntil task holds the flush lock
let resolveUserTask!: () => void;
const userTask = new Promise<void>(resolve => {
resolveUserTask = resolve;
});
context.waitUntil(userTask);

// flush waits for the flush lock to drain.
mocks.flush.mockImplementationOnce(async () => {
await lock.finalize();
return true;
});

const wrapped = wrapMethodWithSentry(
{ options: { dsn: 'https://test@sentry.io/123' }, context, spanName: 'webSocketMessage' },
vi.fn().mockResolvedValue('ok'),
);

await wrapped();

// Releasing the concurrent task drains the lock
resolveUserTask();

await expect(Promise.all(waitUntilPromises)).resolves.toBeDefined();
expect(mocks.flush).toHaveBeenCalled();
});
});
Loading