Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible Memory Leak - heapdumps show continuous growth in compiled code being retained by dummy job #3040

Open
1 task done
garrettg123 opened this issue Jan 31, 2025 · 8 comments

Comments

@garrettg123
Copy link

Version

v5.39.1

Platform

NodeJS

What happened?

As seen in the heap dump comparison after ~100k dummy job completions, there is compiled code that looks like Redis commands being retained. Tested with concurrency = 1 and 5, limited to 1 or 100 per second.
Image

How to reproduce.

new Worker(
      myQueue,
      job => {
        // empty
      },
      {
        connection: new IORedis({
      host: env.REDIS_HOST || 'localhost',
      port: Number(env.REDIS_PORT) || 6379,
      maxRetriesPerRequest: null,
    }),
        concurrency: 5,
        limiter: {
          max: 100,
          duration: 1000,
        },
      }
    )

Relevant log output

When tracking metrics, it appears that only 10% get drained:

  Worker stats: {
    workers: 1,
    active: 157430,
    progress: 0,
    completed: 157430,
    stalled: 0,
    failed: 0,
    errored: 0,
    drained: 1575,
  }


This was calculated using the handlers:

worker.on('completed', job => {
  WORKER_STATS.completed++
})

worker.on('failed', (job, error) => {
  WORKER_STATS.failed++
})

worker.on('error', error => {
  WORKER_STATS.errored++
})

worker.on('progress', (job, progress) => {
  WORKER_STATS.progress++
})

worker.on('stalled', job => {
  WORKER_STATS.stalled++
})

worker.on('active', job => {
  WORKER_STATS.active++
})

worker.on('drained', () => {
  WORKER_STATS.drained++
})

Code of Conduct

  • I agree to follow this project's Code of Conduct
@garrettg123 garrettg123 added the bug Something isn't working label Jan 31, 2025
@manast
Copy link
Contributor

manast commented Jan 31, 2025

Can you produce the complete source code, as the one that you provided is not enough as it is not adding any jobs and so on.

@garrettg123
Copy link
Author

Of course:

import { Queue, Worker } from 'bullmq'

// producer
const myQueueName = 'my-queue'
const queue = new Queue(QUEUE_NAME.PROCESS_KEYWORD_MATCH, {
  connection: new IORedis({
    host: env.REDIS_HOST || 'localhost',
    port: Number(env.REDIS_PORT) || 6379,
    maxRetriesPerRequest: null,
  }),
  defaultJobOptions: {
    removeOnComplete: true,
  },
})

setInterval(() => queue.add(myQueueName, {}), 100)

// worker
new Worker(
  myQueueName,
  job => {
    // empty
  },
  {
    connection: new IORedis({
      host: env.REDIS_HOST || 'localhost',
      port: Number(env.REDIS_PORT) || 6379,
      maxRetriesPerRequest: null,
    }),
    concurrency: 5,
    limiter: {
      max: 100,
      duration: 1000,
    },
  }
)

@manast
Copy link
Contributor

manast commented Feb 3, 2025

Btw, jobs are not "drained", the drained event is generated when the queue is empty, i.e. there are no jobs to be processed.

@manast
Copy link
Contributor

manast commented Feb 3, 2025

Furthermore, by definition this code is going to generate a leak:

{
    connection: new IORedis({
      host: env.REDIS_HOST || 'localhost',
      port: Number(env.REDIS_PORT) || 6379,
      maxRetriesPerRequest: null,
    }),

As you are passing an instance of IORedis that you are never closing (BullMQ would only close connections created by itself).

@manast
Copy link
Contributor

manast commented Feb 3, 2025

I am running this code which creates something like 1k jobs per second, and I keep it running for some time, like 10 minutes or so, running the garbage collector from time to time and the memory is quite stable at around 10-11Mb. So I am going to need more proof that there is indeed a memory leak in this code.

import { Queue, Worker } from "bullmq";

const queueName = "test-leaks";

// producer
const queue = new Queue(queueName, {
  connection: {
    host: "localhost",
    port: 6379,
    maxRetriesPerRequest: null,
  },
  defaultJobOptions: {
    removeOnComplete: true,
  },
});

setInterval(() => queue.add(queueName, {}), 1);

// worker
new Worker(
  queueName,
  (job) => {
    // empty
  },
  {
    connection: {
      host: "localhost",
      port: 6379,
      maxRetriesPerRequest: null,
    },
    concurrency: 5,
    limiter: {
      max: 100,
      duration: 1000,
    },
  }
);

@manast manast added cannot reproduce and removed bug Something isn't working labels Feb 3, 2025
@manast
Copy link
Contributor

manast commented Feb 3, 2025

Here some proof that there are no leaks, after 15 minutes running I took a new heapshop and all the allocations produced between 1 and 2 (processed thousands of jobs before those two heap shots) where almost none (just 29kb which most likely is some GC collection delay):

Image

@manast
Copy link
Contributor

manast commented Feb 3, 2025

Now, there could be a leak, but it is too small to be debugged unfortunately. This is the state of NodeJS ecosystem, some leaks you must live with as long as they are small enough we do not have tools to guarantee 100% that there are no leaks.

@manast manast closed this as completed Feb 3, 2025
@manast manast changed the title [Bug]: Memory Leak - heapdumps show continuous growth in compiled code being retained by dummy job Possible Memory Leak - heapdumps show continuous growth in compiled code being retained by dummy job Feb 3, 2025
@manast manast reopened this Feb 3, 2025
@manast
Copy link
Contributor

manast commented Feb 3, 2025

Reopened in case the author can provide more evidence.

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

No branches or pull requests

2 participants