fix(queue): share one ioredis connection across BullMQ queues and workers (#1009)

BullMQ instantiates a fresh ioredis client per Queue/Worker when handed a
plain {host, port} config object, and under sustained ZIM ingestion the
embed pipeline leaked ~1 client/sec until Redis maxclients was exhausted.
Pass a single shared ioredis instance (maxRetriesPerRequest: null, as
required by BullMQ) so all queues and workers reuse one client pool.
Workers still duplicate the connection once for their blocking client,
which is expected and bounded.

Closes #885
This commit is contained in:
Lorenzo Galassi 2026-06-19 19:19:16 +02:00 committed by jakeaturner
parent b507b8bd4e
commit fe6735fdcb
No known key found for this signature in database
GPG Key ID: B1072EBDEECE328D
4 changed files with 24 additions and 10 deletions

View File

@ -1,11 +1,11 @@
import { Queue } from 'bullmq'
import queueConfig from '#config/queue'
// Process-wide singleton. Each `Queue` opens two ioredis connections (one for
// commands, one blocking). Instantiating a fresh QueueService per dispatch /
// status lookup leaks both, and under sustained job churn (e.g. multi-batch ZIM
// ingestion enqueueing a continuation every few seconds) it saturates Redis's
// maxclients within hours.
// Process-wide singleton. Instantiating a fresh QueueService per dispatch /
// status lookup leaks connections, and under sustained job churn (e.g.
// multi-batch ZIM ingestion enqueueing a continuation every few seconds) it
// saturates Redis's maxclients within hours. All queues additionally reuse the
// single shared ioredis instance exported from #config/queue (#885).
export class QueueService {
private queues: Map<string, Queue> = new Map()

View File

@ -1,11 +1,23 @@
import env from '#start/env'
import { Redis } from 'ioredis'
// BullMQ treats a plain `{host, port}` connection object as a recipe: every
// Queue / Worker instantiates its own ioredis client from it, and script
// commands executed against those clients can spawn further short-lived
// connections. Under sustained ZIM ingestion this leaked ~1 client/sec until
// Redis maxclients was exhausted (#885). Passing a single shared ioredis
// instance instead gives BullMQ a pool to reuse — Workers still duplicate it
// once for their blocking client, which is expected and bounded.
// `maxRetriesPerRequest: null` is mandatory for connections shared with BullMQ.
const sharedConnection = new Redis({
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT') ?? 6379,
db: env.get('REDIS_DB') ?? 0,
maxRetriesPerRequest: null,
})
const queueConfig = {
connection: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT') ?? 6379,
db: env.get('REDIS_DB') ?? 0,
},
connection: sharedConnection,
}
export default queueConfig

View File

@ -47,6 +47,7 @@
"edge.js": "6.4.0",
"fast-xml-parser": "5.7.0",
"fuse.js": "7.1.0",
"ioredis": "5.10.1",
"ipaddr.js": "2.4.0",
"jszip": "3.10.1",
"luxon": "3.7.2",

View File

@ -100,6 +100,7 @@
"edge.js": "6.4.0",
"fast-xml-parser": "5.7.0",
"fuse.js": "7.1.0",
"ioredis": "5.10.1",
"ipaddr.js": "2.4.0",
"jszip": "3.10.1",
"luxon": "3.7.2",