From f9a2eaa9b3cf649da3244b8af07cff09cb0f1e2c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 28 Apr 2026 15:34:59 +0530 Subject: [PATCH 1/7] refactor: start queue in all environments except the console environment and during start phase BREAKING CHANGE: Any custom console commands relying on jobs to be registered with the queue manager will not work. They will have to resolve the queue.manager from the container and explicitly invoke the start method --- .github/workflows/stale.yml | 12 +++++----- README.md | 7 ++---- commands/queue_work.ts | 10 ++++----- providers/queue_provider.ts | 44 ++++++++++++++++--------------------- src/types/extended.ts | 4 +++- src/utils.ts | 19 ++++++++++++++++ 6 files changed, 54 insertions(+), 42 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b750bf5..cad5c91 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,7 +1,7 @@ -name: "Close stale issues and PRs" +name: 'Close stale issues and PRs' on: schedule: - - cron: "30 0 * * *" + - cron: '30 0 * * *' jobs: stale: @@ -9,9 +9,9 @@ jobs: steps: - uses: actions/stale@v9 with: - stale-issue-message: "This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue" - stale-pr-message: "This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request" - close-issue-message: "This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue" - close-pr-message: "This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request" + stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue' + stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request' + close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue' + close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request' days-before-stale: 21 days-before-close: 5 diff --git a/README.md b/README.md index 2bfc420..412a59c 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,9 @@ In order to ensure that the AdonisJS community is welcoming to all, please revie AdonisJS Queue is open-sourced software licensed under the [MIT license](LICENSE.md). [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/queue/checks.yml?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/queue/actions/workflows/checks.yml "Github action" - +[gh-workflow-url]: https://github.com/adonisjs/queue/actions/workflows/checks.yml 'Github action' [npm-image]: https://img.shields.io/npm/v/@adonisjs/queue/latest.svg?style=for-the-badge&logo=npm -[npm-url]: https://www.npmjs.com/package/@adonisjs/queue/v/latest "npm" - +[npm-url]: https://www.npmjs.com/package/@adonisjs/queue/v/latest 'npm' [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript - [license-url]: LICENSE.md [license-image]: https://img.shields.io/github/license/adonisjs/queue?style=for-the-badge diff --git a/commands/queue_work.ts b/commands/queue_work.ts index aba1b97..538223f 100644 --- a/commands/queue_work.ts +++ b/commands/queue_work.ts @@ -8,8 +8,8 @@ */ import { flags, BaseCommand } from '@adonisjs/core/ace' -import { resolveAdapters, resolveJobFactory } from '../src/utils.js' import type { CommandOptions } from '@adonisjs/core/types/ace' +import { resolveAdapters, resolveJobFactory } from '../src/utils.js' import type { QueueConfig, QueueManagerConfig } from '../src/types/main.js' export default class QueueWork extends BaseCommand { @@ -30,8 +30,9 @@ export default class QueueWork extends BaseCommand { async run() { const { Worker } = await import('@boringnode/queue') const config = this.app.config.get('queue') - const queueManager = await this.app.container.make('queue.manager') const logger = await this.app.container.make('logger') + const QueueManager = await this.app.container.make('queue.manager') + await QueueManager.start() /** * Commit the router to ensure all routes are registered. @@ -45,12 +46,11 @@ export default class QueueWork extends BaseCommand { const queues = this.queue ? this.queue.split(',').map((q) => q.trim()) : ['default'] this.logger.info(`Starting worker for queues: ${queues.join(', ')}`) - const jobFactory = resolveJobFactory(config, this.app) const workerConfig = { ...config, adapters: resolvedAdapters, - jobFactory, + jobFactory: resolveJobFactory(config, this.app), logger: config.logger ?? logger, worker: { ...config.worker, @@ -63,7 +63,7 @@ export default class QueueWork extends BaseCommand { try { await worker.start(queues) } finally { - await queueManager.destroy() + await QueueManager.destroy() } } } diff --git a/providers/queue_provider.ts b/providers/queue_provider.ts index dbbe2e2..36be066 100644 --- a/providers/queue_provider.ts +++ b/providers/queue_provider.ts @@ -7,10 +7,11 @@ * file that was distributed with this source code. */ -import '../src/types/extended.js' -import { resolveAdapters, resolveJobFactory } from '../src/utils.js' import type { ApplicationService } from '@adonisjs/core/types' -import type { QueueConfig } from '../src/types/main.js' + +import '../src/types/extended.js' +import { initQueue } from '../src/utils.ts' +import { type QueueConfig } from '../src/types/main.ts' export default class QueueProvider { constructor(protected app: ApplicationService) {} @@ -18,35 +19,28 @@ export default class QueueProvider { register() { this.app.container.singleton('queue.manager', async () => { const { QueueManager } = await import('@boringnode/queue') - const config = this.app.config.get('queue') - - const resolvedAdapters = await resolveAdapters(config, this.app) - - /** - * Inject jobFactory if not already defined. - * This enables automatic dependency injection for job classes. - */ - const jobFactory = resolveJobFactory(config, this.app) - - const logger = await this.app.container.make('logger') - await QueueManager.init({ - ...config, - adapters: resolvedAdapters, - jobFactory, - logger: config.logger ?? (logger as any), - }) + ;(QueueManager as any)['start'] = async () => { + const config = this.app.config.get('queue') + const logger = await this.app.container.make('logger') + return initQueue(QueueManager, this.app, config, logger) + } - return QueueManager + return QueueManager as typeof QueueManager & { + start(): Promise + } }) } - async boot() { - await this.app.container.make('queue.manager') + async start() { + if (this.app.getEnvironment() !== 'console') { + const QueueManager = await this.app.container.make('queue.manager') + await QueueManager.start() + } } async shutdown() { - const queueManager = await this.app.container.make('queue.manager') - await queueManager.destroy() + const QueueManager = await this.app.container.make('queue.manager') + await QueueManager.destroy() } } diff --git a/src/types/extended.ts b/src/types/extended.ts index 8dda43c..3db423c 100644 --- a/src/types/extended.ts +++ b/src/types/extended.ts @@ -11,6 +11,8 @@ import type { QueueManager } from '@boringnode/queue' declare module '@adonisjs/core/types' { export interface ContainerBindings { - 'queue.manager': typeof QueueManager + 'queue.manager': typeof QueueManager & { + start(): Promise + } } } diff --git a/src/utils.ts b/src/utils.ts index 8861b4f..246ecec 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,7 +7,9 @@ * file that was distributed with this source code. */ +import { type Logger } from '@adonisjs/core/logger' import type { ApplicationService } from '@adonisjs/core/types' +import type { QueueManager as QueueManagerSingleton } from '@boringnode/queue' import type { AdapterFactory, JobFactory, QueueConfig } from './types/main.js' /** @@ -39,3 +41,20 @@ export async function resolveAdapters( export function resolveJobFactory(config: QueueConfig, app: ApplicationService): JobFactory { return config.jobFactory ?? ((jobClass: any) => app.container.make(jobClass)) } + +export async function initQueue( + manager: typeof QueueManagerSingleton, + app: ApplicationService, + config: QueueConfig, + logger: Logger +) { + const resolvedAdapters = await resolveAdapters(config, app) + const jobFactory = resolveJobFactory(config, app) + + await manager.init({ + ...config, + adapters: resolvedAdapters, + jobFactory, + logger: config.logger ?? logger, + }) +} From 53bd21c5b14b5847c825549dcc11cfbe37b94425 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Aug 2026 14:06:01 +0530 Subject: [PATCH 2/7] fix: acquire redis and database connections lazily from the drivers The redis and database drivers acquired their connection when the config provider was resolved, which happens during the provider's start hook. Therefore, starting the app opened a connection even when nothing used the queue. The connection is now acquired from within the adapter factory, which the queue manager invokes on first use of the adapter. An app that never becomes ready (for example, the codegen command) skips the shutdown hooks, hence the eagerly opened redis connection kept the process alive. The driver tests run against a real redis server and an in-memory sqlite database. The CI workflow defines the tests job inline, since a job using the shared workflow cannot attach the redis service. --- .github/workflows/checks.yml | 32 ++++++++- package.json | 1 + src/drivers.ts | 24 +++++-- tests/drivers/database.spec.ts | 119 +++++++++++++++++++++++++++++++++ tests/drivers/redis.spec.ts | 116 ++++++++++++++++++++++++++++++++ tests/helpers.ts | 50 +++++++++++++- 6 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 tests/drivers/database.spec.ts create mode 100644 tests/drivers/redis.spec.ts diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 99f8d55..d4b6516 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,4 +13,34 @@ jobs: uses: adonisjs/.github/.github/workflows/typecheck.yml@main tests: - uses: adonisjs/.github/.github/workflows/test.yml@main + runs-on: ubuntu-latest + env: + REDIS_HOST: 127.0.0.1 + REDIS_PORT: 6379 + strategy: + matrix: + node-version: ['lts/krypton', 'latest'] + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test diff --git a/package.json b/package.json index c72e466..953f04f 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@poppinss/ts-exec": "^1.4.4", "@release-it/conventional-changelog": "^10.0.5", "@types/node": "~24.11.0", + "better-sqlite3": "^13.0.3", "c8": "^11.0.0", "cpy-cli": "^7.0.0", "del-cli": "^7.0.0", diff --git a/src/drivers.ts b/src/drivers.ts index 869e12b..88d27ab 100644 --- a/src/drivers.ts +++ b/src/drivers.ts @@ -48,8 +48,18 @@ export const drivers: { const redis = await app.container.make('redis') const { redis: redisAdapter } = await import('@boringnode/queue/drivers/redis_adapter') - const connection = redis.connection(config?.connectionName) - return redisAdapter((connection as any).ioConnection) + /** + * The connection is acquired from within the factory and not when the + * config provider is resolved. The queue manager resolves every adapter + * when the app starts, whereas it invokes the factory only when the + * adapter is used for the first time. Acquiring the connection eagerly + * would open a socket during the app start, even when nothing ever + * touches the queue + */ + return () => { + const connection = redis.connection(config?.connectionName) + return redisAdapter((connection as any).ioConnection)() + } }) }, @@ -59,9 +69,15 @@ export const drivers: { const { knex } = await import('@boringnode/queue/drivers/knex_adapter') const connectionName = config?.connectionName || db.primaryConnectionName - const connection = db.connection(connectionName) - return knex(connection.getWriteClient(), config?.tableName) + /** + * Same as the redis driver. The connection is acquired when the adapter + * is used for the first time and not when the app starts + */ + return () => { + const connection = db.connection(connectionName) + return knex(connection.getWriteClient(), config?.tableName)() + } }) }, diff --git a/tests/drivers/database.spec.ts b/tests/drivers/database.spec.ts new file mode 100644 index 0000000..35c66cd --- /dev/null +++ b/tests/drivers/database.spec.ts @@ -0,0 +1,119 @@ +/* + * @adonisjs/queue + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { defineConfig, drivers } from '../../index.js' +import { resolveAdapters } from '../../src/utils.js' +import { getDatabaseConfig, setupApp } from '../helpers.js' +import type { QueueConfig } from '../../src/types/main.js' + +const databaseProvider = () => import('@adonisjs/lucid/database_provider') + +test.group('drivers | database', () => { + test('do not acquire a connection when resolving the adapter', async ({ assert }) => { + const app = await setupApp( + 'console', + { + database: getDatabaseConfig(), + queue: defineConfig({ + default: 'database', + adapters: { + database: drivers.database({ connectionName: 'jobs' }), + }, + }), + }, + [databaseProvider] + ) + + const db = await app.container.make('lucid.db') + const config = app.config.get('queue') + const resolvedAdapters = await resolveAdapters(config, app) + + assert.isFunction(resolvedAdapters.database) + assert.isFalse(db.manager.isConnected('jobs')) + assert.isFalse(db.manager.isConnected('main')) + }) + + test('acquire the configured connection when creating the adapter', async ({ assert }) => { + const app = await setupApp( + 'console', + { + database: getDatabaseConfig(), + queue: defineConfig({ + default: 'database', + adapters: { + database: drivers.database({ connectionName: 'jobs' }), + }, + }), + }, + [databaseProvider] + ) + + const db = await app.container.make('lucid.db') + const config = app.config.get('queue') + const resolvedAdapters = await resolveAdapters(config, app) + + resolvedAdapters.database() + assert.isTrue(db.manager.isConnected('jobs')) + assert.isFalse(db.manager.isConnected('main')) + }) + + test('use the primary connection when no connection name is configured', async ({ assert }) => { + const app = await setupApp( + 'console', + { + database: getDatabaseConfig(), + queue: defineConfig({ + default: 'database', + adapters: { + database: drivers.database(), + }, + }), + }, + [databaseProvider] + ) + + const db = await app.container.make('lucid.db') + const config = app.config.get('queue') + const resolvedAdapters = await resolveAdapters(config, app) + + resolvedAdapters.database() + assert.isTrue(db.manager.isConnected('main')) + assert.isFalse(db.manager.isConnected('jobs')) + }) + + test('do not acquire a connection when the app starts', async ({ assert }) => { + const app = await setupApp( + 'web', + { + database: getDatabaseConfig(), + queue: defineConfig({ + default: 'database', + adapters: { + database: drivers.database(), + }, + }), + }, + [databaseProvider] + ) + + /** + * Starting the app in the "web" environment initializes the queue + * manager from the provider's start hook. Doing so must not open any + * connection, since nothing releases them in a process that never + * becomes ready (for example, the "codegen" command) + */ + await app.start(() => {}) + + const db = await app.container.make('lucid.db') + assert.isFalse(db.manager.isConnected('main')) + assert.isFalse(db.manager.isConnected('jobs')) + }) +}) diff --git a/tests/drivers/redis.spec.ts b/tests/drivers/redis.spec.ts new file mode 100644 index 0000000..bd33e47 --- /dev/null +++ b/tests/drivers/redis.spec.ts @@ -0,0 +1,116 @@ +/* + * @adonisjs/queue + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import { defineConfig, drivers } from '../../index.js' +import { resolveAdapters } from '../../src/utils.js' +import { getRedisConfig, setupApp } from '../helpers.js' +import type { QueueConfig } from '../../src/types/main.js' + +const redisProvider = () => import('@adonisjs/redis/redis_provider') + +test.group('drivers | redis', () => { + test('do not acquire a connection when resolving the adapter', async ({ assert }) => { + const app = await setupApp( + 'console', + { + redis: getRedisConfig(), + queue: defineConfig({ + default: 'redis', + adapters: { + redis: drivers.redis({ connectionName: 'jobs' as any }), + }, + }), + }, + [redisProvider] + ) + + const redis = await app.container.make('redis') + const config = app.config.get('queue') + const resolvedAdapters = await resolveAdapters(config, app) + + assert.isFunction(resolvedAdapters.redis) + assert.equal(redis.activeConnectionsCount, 0) + }) + + test('acquire the configured connection when creating the adapter', async ({ assert }) => { + const app = await setupApp( + 'console', + { + redis: getRedisConfig(), + queue: defineConfig({ + default: 'redis', + adapters: { + redis: drivers.redis({ connectionName: 'jobs' as any }), + }, + }), + }, + [redisProvider] + ) + + const redis = await app.container.make('redis') + const config = app.config.get('queue') + const resolvedAdapters = await resolveAdapters(config, app) + + resolvedAdapters.redis() + assert.equal(redis.activeConnectionsCount, 1) + assert.property(redis.activeConnections, 'jobs') + }) + + test('use the default connection when no connection name is configured', async ({ assert }) => { + const app = await setupApp( + 'console', + { + redis: getRedisConfig(), + queue: defineConfig({ + default: 'redis', + adapters: { + redis: drivers.redis(), + }, + }), + }, + [redisProvider] + ) + + const redis = await app.container.make('redis') + const config = app.config.get('queue') + const resolvedAdapters = await resolveAdapters(config, app) + + resolvedAdapters.redis() + assert.property(redis.activeConnections, 'main') + }) + + test('do not acquire a connection when the app starts', async ({ assert }) => { + const app = await setupApp( + 'web', + { + redis: getRedisConfig(), + queue: defineConfig({ + default: 'redis', + adapters: { + redis: drivers.redis(), + }, + }), + }, + [redisProvider] + ) + + /** + * Starting the app in the "web" environment initializes the queue + * manager from the provider's start hook. Doing so must not open any + * connection, since nothing releases them in a process that never + * becomes ready (for example, the "codegen" command) + */ + await app.start(() => {}) + + const redis = await app.container.make('redis') + assert.equal(redis.activeConnectionsCount, 0) + }) +}) diff --git a/tests/helpers.ts b/tests/helpers.ts index bd6fc59..914b77a 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -11,21 +11,67 @@ import { getActiveTest } from '@japa/runner' import { IgnitorFactory } from '@adonisjs/core/factories' import type { AppEnvironments } from '@adonisjs/core/types/app' +import { defineConfig as defineRedisConfig } from '@adonisjs/redis' +import { defineConfig as defineDatabaseConfig } from '@adonisjs/lucid' + import { defineConfig, drivers } from '../index.js' const BASE_URL = new URL('./tmp/', import.meta.url) +/** + * Returns the redis config with "main" and "jobs" connections pointing to + * the redis server from the environment + */ +export function getRedisConfig() { + const connection = { + host: process.env.REDIS_HOST || '127.0.0.1', + port: process.env.REDIS_PORT || 6379, + } + + return defineRedisConfig({ + connection: 'main', + connections: { + main: { ...connection }, + jobs: { ...connection }, + }, + }) +} + +/** + * Returns the database config with "main" and "jobs" in-memory sqlite + * connections, so the tests can open a real connection without any + * external database server + */ +export function getDatabaseConfig() { + const connection = { + client: 'better-sqlite3' as const, + connection: { filename: ':memory:' }, + useNullAsDefault: true, + } + + return defineDatabaseConfig({ + connection: 'main', + connections: { + main: { ...connection }, + jobs: { ...connection }, + }, + }) +} + export async function setupApp( env?: AppEnvironments, config: { queue?: ReturnType - } = {} + [key: string]: unknown + } = {}, + providers: (() => Promise<{ default: any }>)[] = [] ) { const ignitor = new IgnitorFactory() .withCoreProviders() .withCoreConfig() .merge({ config: { + ...config, queue: config.queue || defineConfig({ @@ -36,7 +82,7 @@ export async function setupApp( }), }, rcFileContents: { - providers: [() => import('../providers/queue_provider.js')], + providers: [() => import('../providers/queue_provider.js'), ...providers], }, }) .create(BASE_URL, { From 152e250388aaaebe808bfeec1e844bbb629b3269 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Aug 2026 14:12:42 +0530 Subject: [PATCH 3/7] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/checks.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d4b6516..48519c6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -5,6 +5,9 @@ on: - pull_request - workflow_call +permissions: + contents: read + jobs: lint: uses: adonisjs/.github/.github/workflows/lint.yml@main From 5105f991fefc939ee1a152ec2d807aa01421f932 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Aug 2026 14:16:02 +0530 Subject: [PATCH 4/7] chore: update dependencies --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 837642b..d4280ac 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,11 @@ "version": "npm run build" }, "dependencies": { - "@boringnode/queue": "^0.6.0" + "@boringnode/queue": "^0.7.1" }, "devDependencies": { - "@adonisjs/assembler": "^8.4.0", - "@adonisjs/core": "^7.3.4", + "@adonisjs/assembler": "^8.5.0", + "@adonisjs/core": "^7.5.0", "@adonisjs/eslint-config": "^3.1.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/prettier-config": "^1.5.0", @@ -50,16 +50,16 @@ "@japa/file-system": "^3.0.0", "@japa/runner": "^5.3.0", "@poppinss/ts-exec": "^1.4.4", + "@release-it/conventional-changelog": "^12.0.0", + "@types/node": "~26.3.0", "better-sqlite3": "^13.0.3", - "@release-it/conventional-changelog": "^11.0.1", - "@types/node": "~24.11.2", - "c8": "^11.0.0", + "c8": "^12.0.0", "cpy-cli": "^7.0.0", "del-cli": "^7.0.0", - "eslint": "^9.39.4", + "eslint": "^10.9.1", "kysely": "^0.29.3", - "prettier": "^3.9.1", - "release-it": "^20.2.1", + "prettier": "^3.9.6", + "release-it": "^21.0.2", "typescript": "^5.9.3" }, "peerDependencies": { From a3fe07887f2e992c09ee49acee62bb14f7617151 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Aug 2026 14:49:57 +0530 Subject: [PATCH 5/7] fix: do not load jobs when warming up the app An app created in the "warmup" mode never becomes ready, hence nothing dispatches or processes jobs. Loading the jobs only discovers them for no use and warns when there are none. The mode is checked defensively, since the older versions of the framework core do not have the "getMode" method. --- providers/queue_provider.ts | 41 +++++++++++++++++++++++-------------- tests/helpers.ts | 9 ++++++-- tests/provider.spec.ts | 35 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/providers/queue_provider.ts b/providers/queue_provider.ts index f6803fc..1478665 100644 --- a/providers/queue_provider.ts +++ b/providers/queue_provider.ts @@ -7,11 +7,10 @@ * file that was distributed with this source code. */ -import type { ApplicationService } from '@adonisjs/core/types' - import '../src/types/extended.js' -import { initQueue } from '../src/utils.ts' -import { type QueueConfig } from '../src/types/main.ts' +import { resolveAdapters, resolveJobFactory } from '../src/utils.js' +import type { ApplicationService } from '@adonisjs/core/types' +import type { QueueConfig } from '../src/types/main.js' export default class QueueProvider { constructor(protected app: ApplicationService) {} @@ -19,6 +18,17 @@ export default class QueueProvider { register() { this.app.container.singleton('queue.manager', async () => { const { QueueManager } = await import('@boringnode/queue') + const config = this.app.config.get('queue') + + const resolvedAdapters = await resolveAdapters(config, this.app) + + /** + * Inject jobFactory if not already defined. + * This enables automatic dependency injection for job classes. + */ + const jobFactory = resolveJobFactory(config, this.app) + + const logger = await this.app.container.make('logger') await QueueManager.init({ ...config, @@ -28,21 +38,22 @@ export default class QueueProvider { logger: config.logger ?? (logger as any), }) - return QueueManager as typeof QueueManager & { - start(): Promise - } + return QueueManager }) } - async start() { - if (this.app.getEnvironment() !== 'console') { - const QueueManager = await this.app.container.make('queue.manager') - await QueueManager.start() - } + async boot() { + await this.app.container.make('queue.manager') } async start() { - if (this.app.getEnvironment() === 'console') { + /** + * Nothing dispatches or processes jobs in the console environment or in + * an app warming up, since a warmed up app never becomes ready. The + * "getMode" method is missing in the older versions of the framework core + * without the "warmup" mode. + */ + if (this.app.getEnvironment() === 'console' || this.app.getMode?.() === 'warmup') { return } @@ -51,7 +62,7 @@ export default class QueueProvider { } async shutdown() { - const QueueManager = await this.app.container.make('queue.manager') - await QueueManager.destroy() + const queueManager = await this.app.container.make('queue.manager') + await queueManager.destroy() } } diff --git a/tests/helpers.ts b/tests/helpers.ts index 914b77a..69cdcd4 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -9,7 +9,7 @@ import { getActiveTest } from '@japa/runner' import { IgnitorFactory } from '@adonisjs/core/factories' -import type { AppEnvironments } from '@adonisjs/core/types/app' +import type { AppEnvironments, ApplicationModes } from '@adonisjs/core/types/app' import { defineConfig as defineRedisConfig } from '@adonisjs/redis' import { defineConfig as defineDatabaseConfig } from '@adonisjs/lucid' @@ -64,7 +64,8 @@ export async function setupApp( queue?: ReturnType [key: string]: unknown } = {}, - providers: (() => Promise<{ default: any }>)[] = [] + providers: (() => Promise<{ default: any }>)[] = [], + mode?: ApplicationModes ) { const ignitor = new IgnitorFactory() .withCoreProviders() @@ -96,6 +97,10 @@ export async function setupApp( }) const app = ignitor.createApp(env || 'web') + if (mode) { + app.setMode(mode) + } + await app.init().then(() => app.boot()) getActiveTest()?.cleanup(() => app.terminate()) diff --git a/tests/provider.spec.ts b/tests/provider.spec.ts index 87b39b4..551fc25 100644 --- a/tests/provider.spec.ts +++ b/tests/provider.spec.ts @@ -207,4 +207,39 @@ test.group('Provider', (group) => { assert.deepEqual(order, ['router', 'loadJobs', 'start', 'destroy']) }) + + test('should not load jobs when warming up the app', async ({ assert, fs }) => { + await fs.create( + 'app/jobs/provider_warmup_job.ts', + ` + import { Job } from '@boringnode/queue' + + export default class ProviderWarmupJob extends Job { + async execute() {} + } + ` + ) + + const app = await setupApp( + 'web', + { + queue: defineConfig({ + default: 'sync', + adapters: { + sync: sync(), + }, + locations: [`${fs.basePath}/app/jobs/provider_warmup_job.ts`], + }), + }, + [], + 'warmup' + ) + + assert.isUndefined(Locator.get('ProviderWarmupJob')) + + await app.warmUp() + + assert.equal(app.getState(), 'warmed') + assert.isUndefined(Locator.get('ProviderWarmupJob')) + }) }) From fd2822fd9a84d7eb2c2c304ad4d1e98598dd0f91 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Aug 2026 14:49:57 +0530 Subject: [PATCH 6/7] fix: remove the leftovers of the 0.x merge The merge kept the "start" method patched on the queue manager and the "initQueue" helper, whereas the 0.x branch initializes the queue manager from the container binding and loads the jobs from the "loadJobs" method --- commands/queue_work.ts | 10 +++++----- src/types/extended.ts | 4 +--- src/utils.ts | 19 ------------------- 3 files changed, 6 insertions(+), 27 deletions(-) diff --git a/commands/queue_work.ts b/commands/queue_work.ts index 19f2df9..d900000 100644 --- a/commands/queue_work.ts +++ b/commands/queue_work.ts @@ -8,8 +8,8 @@ */ import { flags, BaseCommand } from '@adonisjs/core/ace' -import type { CommandOptions } from '@adonisjs/core/types/ace' import { resolveAdapters, resolveJobFactory } from '../src/utils.js' +import type { CommandOptions } from '@adonisjs/core/types/ace' import type { QueueConfig, QueueManagerConfig } from '../src/types/main.js' export default class QueueWork extends BaseCommand { @@ -30,9 +30,8 @@ export default class QueueWork extends BaseCommand { async run() { const { Worker } = await import('@boringnode/queue') const config = this.app.config.get('queue') + const queueManager = await this.app.container.make('queue.manager') const logger = await this.app.container.make('logger') - const QueueManager = await this.app.container.make('queue.manager') - await QueueManager.start() /** * Commit the router to ensure all routes are registered. @@ -48,12 +47,13 @@ export default class QueueWork extends BaseCommand { await queueManager.loadJobs() this.logger.info(`Starting worker for queues: ${queues.join(', ')}`) + const jobFactory = resolveJobFactory(config, this.app) const workerConfig = { ...config, autoLoadJobs: false, adapters: resolvedAdapters, - jobFactory: resolveJobFactory(config, this.app), + jobFactory, logger: config.logger ?? logger, worker: { ...config.worker, @@ -66,7 +66,7 @@ export default class QueueWork extends BaseCommand { try { await worker.start(queues) } finally { - await QueueManager.destroy() + await queueManager.destroy() } } } diff --git a/src/types/extended.ts b/src/types/extended.ts index 3db423c..8dda43c 100644 --- a/src/types/extended.ts +++ b/src/types/extended.ts @@ -11,8 +11,6 @@ import type { QueueManager } from '@boringnode/queue' declare module '@adonisjs/core/types' { export interface ContainerBindings { - 'queue.manager': typeof QueueManager & { - start(): Promise - } + 'queue.manager': typeof QueueManager } } diff --git a/src/utils.ts b/src/utils.ts index 246ecec..8861b4f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,9 +7,7 @@ * file that was distributed with this source code. */ -import { type Logger } from '@adonisjs/core/logger' import type { ApplicationService } from '@adonisjs/core/types' -import type { QueueManager as QueueManagerSingleton } from '@boringnode/queue' import type { AdapterFactory, JobFactory, QueueConfig } from './types/main.js' /** @@ -41,20 +39,3 @@ export async function resolveAdapters( export function resolveJobFactory(config: QueueConfig, app: ApplicationService): JobFactory { return config.jobFactory ?? ((jobClass: any) => app.container.make(jobClass)) } - -export async function initQueue( - manager: typeof QueueManagerSingleton, - app: ApplicationService, - config: QueueConfig, - logger: Logger -) { - const resolvedAdapters = await resolveAdapters(config, app) - const jobFactory = resolveJobFactory(config, app) - - await manager.init({ - ...config, - adapters: resolvedAdapters, - jobFactory, - logger: config.logger ?? logger, - }) -} From c02aa35f6e03b21454d091076d8e7b99a078e24a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Aug 2026 14:51:51 +0530 Subject: [PATCH 7/7] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d4280ac..34651b2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@adonisjs/eslint-config": "^3.1.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/prettier-config": "^1.5.0", - "@adonisjs/redis": "^10.0.0", + "@adonisjs/redis": "^10.0.1", "@adonisjs/tsconfig": "^2.0.0", "@japa/assert": "^4.2.0", "@japa/file-system": "^3.0.0", @@ -57,10 +57,10 @@ "cpy-cli": "^7.0.0", "del-cli": "^7.0.0", "eslint": "^10.9.1", - "kysely": "^0.29.3", + "kysely": "^0.29.5", "prettier": "^3.9.6", "release-it": "^21.0.2", - "typescript": "^5.9.3" + "typescript": "^6.0.3" }, "peerDependencies": { "@adonisjs/assembler": "^8.0.0",