Skip to content

Memory leak with endless streaming (example: CCTV) #13806

Description

@soapproject

Is there an existing issue for this?

How do you use Sentry?

Self-hosted/on-premise

Which SDK are you using?

@sentry/nextjs

SDK Version

8.30.0

Framework Version

next 14.2.13, react 18.2.0, video.js 7.21.1

Link to Sentry event

No response

Reproduction Example/SDK Setup

// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig(bundledConfig, {
  org: 'xxx',
  project: 'xxx',
  sentryUrl: 'https://xxx/',
  silent: !process.env.CI,
  widenClientFileUpload: true,
  reactComponentAnnotation: {
    enabled: false,
  },
  tunnelRoute: '/monitoring',
  hideSourceMaps: false,
  disableLogger: true,
  automaticVercelMonitors: true,
});

// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: 'https://xxx',
  integrations: [Sentry.replayIntegration()],
  tracesSampleRate: 1,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  debug: true,
});

// sentry.edge.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: 'https://xxx',
  tracesSampleRate: 1,
  debug: false,
});

// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: 'https://xxx',
  tracesSampleRate: 1,
  debug: false,
});

Steps to Reproduce

  1. Play an endless live stream (CCTV in FLV format).
  2. Memory usage keeps increasing over time.

Expected Result

I believe this is causing the issue:
Unresolved promises and their context are stacking up in the task queue.

// sentry-javascript/packages/utils/src/instrument/fetch.ts

async function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): Promise<void> {
  if (res && res.body && res.body.getReader) {
    const responseReader = res.body.getReader();

    // eslint-disable-next-line no-inner-declarations
    async function consumeChunks({ done }: { done: boolean }): Promise<void> {
      if (!done) {
        try {
          // abort reading if read op takes more than 5s
          const result = await Promise.race([
            responseReader.read(),
            new Promise<{ done: boolean }>(res => {
              setTimeout(() => {
                res({ done: true });
              }, 5000);
            }),
          ]);
          await consumeChunks(result);
        } catch (error) {
          // handle error if needed
        }
      } else {
        return Promise.resolve();
      }
    }

    return responseReader
      .read()
      .then(consumeChunks)
      .then(onFinishedResolving)
      .catch(() => undefined);
  }
}

Is there any specific reason to use recursion in this case? If not, may I submit a PR with an alternative approach?

example:

async function resolveResponse(res, onFinishedResolving) {
  if (!res?.body?.getReader) return;

  const responseReader = res.body.getReader();

  while (true) {
    try {
      const { done } = await Promise.race([
        responseReader.read(),
        new Promise((resolve) => setTimeout(() => resolve({ done: true }), 5000)),
      ]);

      if (done) break;
    } catch (error) {
      // handle error if needed
      break;
    }
  }

  responseReader.releaseLock();
  onFinishedResolving();
}

Actual Result

Image

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions