Skip to content

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

@soapproject

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Package: nextjsIssues related to the Sentry Nextjs SDK

    Type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions