boringnode/queue

repository·main·Indexed 41 days ago

https://github.com/boringnode/queue

A simple and efficient background job queue system for Node.js applications (v0.7.1). It supports multiple adapters including Redis, SQL databases (via Knex and Kysely for PostgreSQL, MySQL, and SQLite), as well as sync and fake adapters for testing. Features include job retention control, delayed jobs, retry strategies with backoff, recurring schedules via Cron or intervals, and experimental OpenTelemetry instrumentation.

Tokens
14.9K
Snippets
44
Records
62
Agent score
51%

What's inside boringnode/queue

  1. Understand OpenTelemetry Span Attributes and Trace Propagation

    main

    Trace Context Propagation

    Instrumentation supports two modes for connecting dispatch and execution traces:

    • Link mode (default): Each job execution is an independent trace, linked to the dispatch span via a link.
    • Parent mode: The job execution becomes a child of the dispatch span, sharing the same trace.

    Span Attributes

    Attributes follow OTel messaging semantic conventions plus custom queue-specific fields:

    AttributeKindDescription
    messaging.systemSemconv'boringqueue' (configurable)
    messaging.operation.nameSemconv'publish' or 'process'
    messaging.destination.nameSemconvQueue name
    messaging.message.idSemconvJob ID for single-message spans
    messaging.batch.message_countSemconvNumber of jobs in a batch dispatch
    messaging.message.retry.countCustomRetry count (0-based) for a job attempt
    messaging.job.nameCustomJob class name (e.g. SendEmailJob)
    messaging.job.statusCustom'completed', 'failed', or 'retrying'
    messaging.job.group_idCustomQueue-specific group identifier
    messaging.job.priorityCustomQueue-specific job priority
    messaging.job.delay_msCustomDelay before the job becomes available
    messaging.job.queue_time_msCustomTime spent waiting in queue before processing
  2. How Job Deduplication works

    main

    Deduplication prevents the same job from being pushed multiple times using the .dedup({ id, ... }) method. The deduplication key is automatically prefixed with the job name (JobName::id).

    Deduplication Modes

    • Simple: Skips dispatch if a job with the same ID already exists.
    • Throttle: Skips dispatch if a job with the same ID exists within a specific ttl (e.g., '5s').
    • Extend: Resets the TTL timer of an existing job if a duplicate is dispatched. Requires ttl to be set.
    • Debounce: Replaces the payload of a pending or delayed job with the new payload and resets the TTL. Requires ttl, replace: true, and extend: true.
    // Simple deduplication
    await SendInvoiceJob.dispatch({ orderId: 123 }).dedup({ id: 'order-123' }).run()
    
    // Throttle (5s window)
    await SendEmailJob.dispatch({ to: 'user@example.com' })
      .dedup({ id: 'welcome-123', ttl: '5s' })
      .run()
    
    // Debounce (Replace payload and reset TTL)
    await SaveDraftJob.dispatch({ content: 'latest draft' })
      .dedup({ id: 'draft-42', ttl: '2s', replace: true, extend: true })
      .run()
  3. Use the Kysely Adapter

    main

    The Kysely adapter supports PostgreSQL, MySQL, and SQLite. Pass your application-owned Kysely instance to the adapter factory. The adapter will not destroy your instance. If you use custom table names in your migrations, specify them using tableName and schedulesTableName in the options.

    import {
      kysely,
      KyselyQueueSchemaService,
      type QueueDatabase,
    } from '@boringnode/queue/drivers/kysely_adapter'
    
    interface Database extends QueueDatabase {
      orders: OrderTable
    }
    
    const adapter = kysely<Database>(db, { dialect: 'postgres' })
    
    // Schema setup
    const schema = new KyselyQueueSchemaService(db, { dialect: 'postgres' })
    await schema.createJobsTable()
    await schema.createSchedulesTable()
  4. Install @boringnode/queue

    main

    Install the package using npm to start using the queue system in your Node.js application.

    npm install @boringnode/queue
  5. Use the Sync Adapter for Testing

    main

    The sync adapter executes jobs immediately in the same process. It is intended for tests and lightweight local development only.

    Warning: When using the sync adapter, retries and backoff delays happen inline in the caller. If a job has a high maxRetries and a long backoff, the await job.dispatch().run() call will block the caller for the entire duration of the retry cycle. Use a worker-backed adapter in production to avoid blocking your main execution flow.

    import { sync } from '@boringnode/queue/drivers/sync_adapter'
    
    const adapter = sync() // Jobs execute immediately
    
    // Note: This will block until all retries are exhausted
    await MyJob.dispatch(payload).run()
  6. Enable Hot Reloading for Jobs during development

    main

    To reload job implementations without restarting workers, use hotReload: true in QueueManager.init(). This requires the hot-hook package and an environment where Hot Hook is initialized (like an AdonisJS server with --hmr).

    Note: Only jobs discovered via locations or Locator.registerFromGlob() support hot reloading. Manual registrations via Locator.register() do not.

    npm install --save-dev hot-hook
    await QueueManager.init({
      default: 'redis',
      adapters: {
        redis: redis({ host: 'localhost', port: 6379 }),
      },
      locations: ['./app/jobs/**/*.ts'],
      hotReload: process.env.NODE_ENV === 'development',
    })
  7. Set up OpenTelemetry instrumentation (experimental)

    main

    The library provides built-in OpenTelemetry instrumentation that creates PRODUCER spans for job dispatch and CONSUMER spans for job execution.

    Note: This API is experimental and subject to change.

    To enable instrumentation, use QueueInstrumentation and call manuallyRegister(boringqueue). This patches QueueManager.init() to automatically inject wrappers without requiring changes to your existing queue configuration.

    import { QueueInstrumentation } from '@boringnode/queue/otel'
    import * as boringqueue from '@boringnode/queue'
    
    const instrumentation = new QueueInstrumentation({
      messagingSystem: 'boringqueue', // default
      executionSpanLinkMode: 'link', // or 'parent'
    })
    
    instrumentation.enable()
    instrumentation.manuallyRegister(boringqueue)
  8. Use the Knex Adapter (PostgreSQL, MySQL, SQLite)

    main

    The Knex adapter allows you to use relational databases. It requires tables to be created before use via the QueueSchemaService.

    Database Setup: You can use QueueSchemaService to create the necessary jobs and schedules tables. You can also extend the jobs table with custom columns during creation.

    import { knex } from '@boringnode/queue/drivers/knex_adapter'
    import { QueueSchemaService } from '@boringnode/queue'
    import Knex from 'knex'
    
    // Initialize adapter
    const connection = Knex({ client: 'pg', connection: '...' })
    const adapter = knex(connection)
    
    // Setup schema
    const schemaService = new QueueSchemaService(connection)
    await schemaService.createJobsTable()
    await schemaService.createSchedulesTable()
    
    // Custom columns example
    await schemaService.createJobsTable('queue_jobs', (table) => {
      table.string('tenant_id', 255).nullable()
    })
  9. Use the Redis Adapter

    main

    The Redis adapter is recommended for production environments. You can initialize it with a configuration object or pass an existing ioredis instance.

    Important: Migrating Redis schedules If you are upgrading from a version that predates the schedules::due sorted-set index, you must run the adapter migration once during deployment before starting any workers. This migration is idempotent but should not be run inside a worker polling loop as it is an O(number of schedules) operation.

    import { redis } from '@boringnode/queue/drivers/redis_adapter'
    
    // With options
    const adapter = redis({ host: 'localhost', port: 6379 })
    
    // With existing ioredis instance
    import { Redis } from 'ioredis'
    const connection = new Redis({ host: 'localhost' })
    const adapter = redis(connection)
    
    // Migration (Run once during deployment)
    await QueueManager.init(config)
    await QueueManager.use('redis').migrate()
  10. Core components of @boringnode/queue

    main

    The @boringnode/queue package provides the following primary building blocks for managing asynchronous task execution:

    • Job: The fundamental unit of work to be executed.
    • Worker: The consumer that processes jobs from the queue.
    • QueueManager: The central orchestrator for managing queues, jobs, and workers.
    • Locator: Used for finding or identifying specific queue resources.
    • Schedule & ScheduleBuilder: Tools for defining when jobs should run (e.g., cron-like or delayed execution).
    • JobBatchDispatcher: Specialized component for dispatching multiple jobs at once.
    • QueueSchemaService: Service for managing and validating queue schemas.
    • backoff_strategies: Functions to define retry behaviors (customBackoff, linearBackoff, exponentialBackoff, fixedBackoff).
  11. Configure Job History and Retention

    main

    You can control how long completed and failed jobs are kept in the queue by setting removeOnComplete and removeOnFail in the JobOptions of your job class. This is useful for debugging without bloating your storage indefinitely.

    Retention options:

    • true (default): Remove immediately.
    • false: Keep forever.
    • { count: n }: Keep the last n jobs.
    • { age: 'duration' }: Keep jobs for a specific duration (e.g., '7d', '1d').
    • { count: n, age: 'duration' }: Both limits apply.

    To query job history, use the adapter's getJob method.

    export default class ImportantJob extends Job<Payload> {
      static options: JobOptions = {
        // Keep last 1000 completed jobs
        removeOnComplete: { count: 1000 },
    
        // Keep failed jobs for 7 days
        removeOnFail: { age: '7d' },
      }
    }
    
    // Querying history
    const job = await adapter.getJob('job-id', 'queue-name')
    console.log(job.status) // 'completed' | 'failed'
    console.log(job.finishedAt) // timestamp
    console.log(job.error) // error message (if failed)
  12. Handle stalled jobs and worker heartbeats

    main

    To ensure reliability in distributed environments, the Adapter supports mechanisms to handle worker crashes and long-running tasks:

    Recovering Stalled Jobs

    Use recoverStalledJobs(queue: string, stalledThreshold: number, maxStalledCount: number): Promise<number> to find jobs that were active but the worker stopped responding.

    • Jobs within maxStalledCount are moved back to pending.
    • Jobs exceeding maxStalledCount are failed permanently.

    Worker Heartbeats (Renewing Jobs)

    To prevent long-running handlers from being mistaken for stalled jobs, workers should periodically call renewJobs(queue: string, jobIds: string[]): Promise<number>.

    • This updates the acquiredAt timestamp.
    • It only renews jobs owned by the worker ID set via setWorkerId.
    • This prevents a slow worker from accidentally hijacking a job that has already been recovered or re-acquired by another worker.