Skip to content

Kysely, Adapter-Aware Workers, and Hot Reloading

Choose a tag to compare

@github-actions github-actions released this 30 Jul 07:18
· 12 commits to main since this release

What's Changed

New Features

Kysely Adapter

@boringnode/queue now includes a Kysely adapter for PostgreSQL, MySQL, and SQLite.

The adapter uses an application-owned Kysely instance and supports the same queue and schedule primitives as the other persistent adapters, including delayed jobs, priorities, retries, retention, deduplication, and scheduled jobs.

import {
  kysely,
  KyselyQueueSchemaService,
  type QueueDatabase,
} from '@boringnode/queue/drivers/kysely_adapter'

interface Database extends QueueDatabase {
  orders: OrderTable
}

const adapter = kysely<Database>(db, { dialect: 'postgres' })

KyselyQueueSchemaService can create and remove the required tables:

const schema = new KyselyQueueSchemaService(db, { dialect: 'postgres' })

await schema.createJobsTable()
await schema.createSchedulesTable()

The adapter never destroys the supplied Kysely instance. Custom job and schedule table names can be configured with tableName and schedulesTableName.

Adapter-Aware Workers and Schedules

Workers can now listen on a specific registered Adapter with worker.adapter. When omitted, the worker continues to use the queue manager's default Adapter.

const config = {
  default: 'redis',
  adapters: {
    redis: redis(redisConfig),
    database: knex(databaseConfig),
  },
  worker: {
    adapter: 'database',
    concurrency: 5,
  },
}

const worker = new Worker(config)
await worker.start(['default', 'emails'])

This makes it possible to run separate workers for queues stored by different Adapters.

Schedules can also select their owning Adapter with .with():

await CleanupJob.schedule({ days: 30 })
  .id('daily-cleanup')
  .with('redis')
  .cron('0 0 * * *')

Schedule.find() and Schedule.list() accept an Adapter selector when accessing schedules outside the default Adapter:

const schedule = await Schedule.find('daily-cleanup', { adapter: 'redis' })
const schedules = await Schedule.list({ status: 'active' }, { adapter: 'redis' })

A returned Schedule retains the selected Adapter for subsequent pause(), resume(), delete(), and trigger() calls. Jobs dispatched by a schedule stay on the Adapter that owns that schedule.

Scheduled jobs now also include their originating schedule ID in JobData.scheduleId. Jobs can access it through this.context.scheduleId:

async execute() {
  console.log(this.context.scheduleId)
}

The value is undefined for jobs that were not dispatched by a schedule.

Hot Reloading Jobs

Workers can now execute the latest saved version of a job without restarting during development.

Enable hotReload when initializing the queue manager:

await QueueManager.init({
  default: 'redis',
  adapters: {
    redis: redis({ host: 'localhost', port: 6379 }),
  },
  locations: ['./app/jobs/**/*.ts'],
  hotReload: process.env.NODE_ENV === 'development',
})

Hot reload integrates with Hot Hook. The queue provides the dynamic import boundary, while the application remains responsible for installing and initializing Hot Hook.

AdonisJS applications can use node ace serve --hmr. Standalone worker processes must initialize Hot Hook themselves.

Locator.registerFromGlob() also accepts the option directly:

await Locator.registerFromGlob(['./app/jobs/**/*.ts'], { hotReload: true })

Runtime Improvements

Consistent Job Dispatch and Execution

All job dispatch paths now apply the same routing and job options. This includes dispatch(), dispatchMany(), manual schedule triggers, and schedules claimed by workers.

Routing is now resolved consistently in the following order:

  1. Fluent overrides such as .toQueue() and .with()
  2. Static Job.options
  3. The Adapter configured for the selected queue
  4. The queue manager's default Adapter

Queue, Adapter, priority, custom job name, creation timestamp, and schedule provenance are therefore preserved consistently regardless of how a job is dispatched.

Static job options are resolved when the fluent builder runs, so changes made between builder creation and execution are applied.

The Sync adapter and Worker execution paths now also share the same job lifecycle behavior, including:

  • context construction;
  • dependency injection through jobFactory;
  • execution wrappers;
  • timeouts;
  • retries;
  • failed hooks;
  • tracing.

Bug Fixes

Scheduled Jobs Now Honor Static Queue Options

Fixed an issue where Job.schedule() ignored Job.options.queue, causing scheduled jobs to land on the default queue while regular dispatches correctly used the configured queue.

Scheduled jobs now use the same queue and Adapter resolution rules as every other dispatch path.

See #21.

Redis Connections from Duplicate ioredis Copies

Fixed Redis connection detection when the application and @boringnode/queue resolve different copies of the ioredis package.

The adapter previously relied on instanceof Redis. With package managers such as pnpm, a valid application-owned connection could fail that check and be mistaken for a configuration object. Its connection settings were then silently discarded, causing the queue to connect to the default localhost:6379 instance instead.

Redis connections are now detected by their interface, allowing the supplied connection to be reused across package boundaries.

See #20.

Upgrade Notes

Workers and Adapter Routing

Start a Worker for every Adapter that owns queues or schedules. A Worker only claims schedules and jobs from its configured Adapter.

When a schedule does not call .with(), its Adapter is resolved from:

  1. the job's adapter option;
  2. the Adapter configured for the job's queue;
  3. the queue manager's default Adapter.

An explicit .with() always takes precedence.

A job routed to a queue with queues.<name>.adapter now uses that Adapter when neither .with() nor Job.options.adapter selects another one. Previously, some dispatch paths could incorrectly fall back to the queue manager's default Adapter.

Verify that a Worker is running for every Adapter referenced by queue configuration.

Hot Reload

Hot reload is disabled by default and should only be enabled in development.

Only jobs discovered from locations or registered with Locator.registerFromGlob() can be reloaded. Jobs registered manually with Locator.register() do not have a module path to reload.

Changes to the set of registered jobs still require a restart. This includes adding, deleting, moving, or renaming a job, as well as changing its configured name.

A job that is already running keeps its current implementation. The next execution receives the updated version.

Avoid import-time side effects in hot-reloaded job modules, since their module code can execute again after an invalidation.

Commits Since v0.6.0

  • fe122cd feat: support hot reloading jobs
  • b88f4c9 refactor: deepen job execution runtime
  • d437182 refactor: concentrate job dispatch paths
  • 757055a docs: update changelog for latest runtime changes
  • 207d641 feat: add Kysely queue adapter
  • 591dafd ci: add MySQL service to checks
  • f86e54e fix(redis): support connections from duplicate ioredis copies

Full Changelog: v0.6.0...v0.7.0