队列

Warning

@adonisjs/queue 包目前处于实验阶段。在达到稳定版本之前,其 API 可能会在次要版本之间发生变化。请在你的 package.json 中锁定该包的版本,以避免更新时出现意外的破坏性变更。

本指南介绍 AdonisJS 中基于队列的后台任务处理。你将学习如何:

  • 使用 Redis 或 Database 后端安装并配置队列系统
  • 创建任务并在后台派发它们
  • 通过派发时的去重防止重复任务
  • 延迟任务、设置优先级并批量派发
  • 配置带有指数、线性或固定退避的重试策略
  • 使用 cron 表达式或时间间隔调度定时任务
  • 启动 worker 以处理队列中的任务
  • 使用 fake 适配器测试任务派发

概述

Web 应用经常需要执行那些在 HTTP 请求期间运行过慢或资源消耗过高的任务。发送邮件、生成报表、处理支付或调整图片尺寸,都是应该在后台完成的典型工作,以便用户能立即获得响应。

@adonisjs/queue 包为 AdonisJS 提供了任务队列系统,构建于 @boringnode/queue 之上。你将任务(job) 定义为带有类型化负载的类,从应用代码中派发它们,并运行一个独立的 worker 进程来领取并执行这些任务。

该包支持多种后端。Redis 适配器被推荐用于生产环境,提供了原子操作和高吞吐量。Database 适配器通过 Lucid 使用你现有的 SQL 数据库(PostgreSQL、MySQL 或 SQLite)。另外还提供了一个 Sync 适配器用于开发和测试,它会立即执行任务而无需单独的 worker。

安装

使用以下命令安装并配置该包:

node ace add @adonisjs/queue
查看 add 命令执行的步骤
  1. 使用检测到的包管理器安装 @adonisjs/queue 包。

  2. adonisrc.ts 文件中注册以下服务提供者、命令和预加载文件。

    adonisrc.ts
    {
      commands: [
        // ...other commands
        () => import('@adonisjs/queue/commands')
      ],
      providers: [
        // ...other providers
        () => import('@adonisjs/queue/queue_provider')
      ],
      preloads: [
        // ...other preloads
        () => import('#start/scheduler')
      ]
    }
  3. 创建 config/queue.ts 文件。

  4. 创建用于定义定时任务的 start/scheduler.ts 预加载文件。

  5. 定义 QUEUE_DRIVER 环境变量及其校验。

  6. 如果选择了数据库驱动,则会创建一个迁移以建立队列表。

配置

配置文件位于 config/queue.ts。它定义了你的适配器、要使用的默认适配器、worker 设置,以及任务文件的存放位置。

另见: 配置桩文件

config/queue.ts
import env from '#start/env'
import { defineConfig, drivers } from '@adonisjs/queue'

export default defineConfig({
  default: env.get('QUEUE_DRIVER', 'redis'),

  adapters: {
    redis: drivers.redis({
      connectionName: 'main',
    }),
    sync: drivers.sync(),
  },

  worker: {
    concurrency: 5,
    idleDelay: '2s',
  },

  locations: ['./app/jobs/**/*.{ts,js}'],
})
default
string

派发任务时默认使用的适配器名称。该值通常通过 QUEUE_DRIVER 环境变量设置。

adapters
Record<string, AdapterFactory>

一组已命名的适配器。每个适配器都使用 drivers 辅助函数之一创建:drivers.redis()drivers.database()drivers.sync()。你可以配置多个适配器并在运行时切换。

worker
WorkerConfig

worker 进程的配置。有关所有可用选项,请参阅 Worker 配置

locations
string[]

指向你的任务文件的一组 glob 模式。队列系统使用这些模式自动发现并注册任务类。

config/queue.ts
{
  locations: ['./app/jobs/**/*.{ts,js}'],
}
retry
RetryConfig

应用于所有任务的全局重试配置,除非在队列或任务级别被覆盖。详见 重试与退避

queues
Record<string, QueueConfig>

按队列进行的配置,允许你为特定队列设置不同的重试策略或默认任务选项。

config/queue.ts
{
  queues: {
    emails: {
      retry: {
        maxRetries: 5,
      },
    },
  },
}
defaultJobOptions
JobOptions

应用于所有任务的默认选项。单个任务可以在其 static options 属性中覆盖这些选项。 ::

适配器配置

Redis

Redis 适配器使用你的 @adonisjs/redis 连接。由于其原子操作与高吞吐量,它被推荐用于生产环境。

config/queue.ts
import { defineConfig, drivers } from '@adonisjs/queue'

export default defineConfig({
  default: 'redis',
  adapters: {
    redis: drivers.redis({
      // 使用 config/redis.ts 中的 'main' 连接
      connectionName: 'main',
    }),
  },
  // ...
})

要让该适配器工作,你必须已安装并配置了 @adonisjs/redis

Database

Database 适配器通过 @adonisjs/lucid 使用 PostgreSQL、MySQL 或 SQLite 连接。当你想避免向基础设施中引入 Redis 时,这是一个不错的选择。

config/queue.ts
import { defineConfig, drivers } from '@adonisjs/queue'

export default defineConfig({
  default: 'database',
  adapters: {
    database: drivers.database({
      connectionName: 'primary',
    }),
  },
  // ...
})

在选择数据库驱动进行安装时,会自动创建一个迁移。如果你需要手动创建这些表,请使用 QueueSchemaService

database/migrations/xxxx_create_queue_tables.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
import { QueueSchemaService } from '@adonisjs/queue'

export default class extends BaseSchema {
  async up() {
    const schemaService = new QueueSchemaService(this.db.getWriteClient())

    await schemaService.createJobsTable()
    await schemaService.createSchedulesTable()
  }

  async down() {
    const schemaService = new QueueSchemaService(this.db.getWriteClient())

    await schemaService.dropSchedulesTable()
    await schemaService.dropJobsTable()
  }
}

要让该适配器工作,你必须已安装并配置了 @adonisjs/lucid

Sync

Sync 适配器会在同一进程内立即执行任务,无需单独的 worker。当你想立即看到任务结果时,这对开发和测试很有用。

config/queue.ts
import { defineConfig, drivers } from '@adonisjs/queue'

export default defineConfig({
  default: 'sync',
  adapters: {
    sync: drivers.sync(),
  },
  // ...
})
Tip

你可以使用 QUEUE_DRIVER 环境变量按环境在适配器之间切换。在生产环境中使用 redisdatabase,在开发环境中使用 sync

创建任务

任务(job) 是一个封装了要在后台执行的工作单元的类。每个任务都继承带有类型化负载的 Job 基类。

使用 make:job Ace 命令生成一个新的任务:

node ace make:job process_payment

这会在 app/jobs/process_payment.ts 创建一个任务类:

app/jobs/process_payment.ts
import { Job } from '@adonisjs/queue'
import type { JobOptions } from '@adonisjs/queue/types'

interface ProcessPaymentPayload {
  orderId: number
  amount: number
  currency: string
}

export default class ProcessPayment extends Job<ProcessPaymentPayload> {
  static options: JobOptions = {
    queue: 'default',
    maxRetries: 3,
  }

  async execute() {
    const { orderId, amount, currency } = this.payload

    // 使用你的支付网关处理支付
    console.log(`Processing payment of ${amount} ${currency} for order ${orderId}`)
  }

  async failed(error: Error) {
    console.error(`Payment processing failed for order ${this.payload.orderId}:`, error.message)
  }
}

Job 类提供了两个需要实现的方法:

  • execute()(必需):包含主要的任务逻辑。此处抛出的任何错误都会触发重试机制。
  • failed(error)(可选):当任务在耗尽所有重试后永久失败时调用。可用它来进行清理、记录日志或发送通知。

任务选项

通过设置 static options 属性来配置任务的默认行为。

queue
string

该任务被派发到的队列。默认为 'default'

maxRetries
number

在任务永久失败之前的最大重试次数。除非在全局或队列级别配置,否则默认为 0

priority
number

任务优先级,从 1 到 10。数字越小越先处理。默认为 5

timeout
Duration

任务超时前的最大执行时间。接受毫秒为单位的数字,或像 '30s''5m' 这样的时长字符串。默认不超时。

failOnTimeout
boolean

是否在超时时将任务标记为永久失败。当为 false 时,超时的任务遵循正常的重试策略。默认为 false

retry
RetryConfig

特定于该任务的重试配置,覆盖全局和队列级别的配置。详见 重试与退避

removeOnComplete
JobRetention

控制已完成的任务是否保留在历史记录中。设为 true 可立即移除(默认),false 可永久保留,或设为带有 age 和/或 count 约束的对象。

{ removeOnComplete: { age: '7d', count: 1000 } }
removeOnFail
JobRetention

控制失败的任务是否保留在历史记录中。格式与 removeOnComplete 相同。 :::

任务上下文

在执行期间,你可以通过 this.context 访问当前任务的元数据:

app/jobs/process_payment.ts
export default class ProcessPayment extends Job<ProcessPaymentPayload> {
  async execute() {
    // 访问任务元数据
    console.log(`Job ID: ${this.context.jobId}`)
    console.log(`Attempt: ${this.context.attempt}`)
    console.log(`Queue: ${this.context.queue}`)
    console.log(`Priority: ${this.context.priority}`)
  }
}

处理超时

对于长时间运行的任务,你可以检查 this.signal 以检测是否已达到超时,并优雅地处理:

app/jobs/generate_report.ts
import { Job } from '@adonisjs/queue'
import type { JobOptions } from '@adonisjs/queue/types'

interface GenerateReportPayload {
  reportId: number
  rows: number[]
}

export default class GenerateReport extends Job<GenerateReportPayload> {
  static options: JobOptions = {
    timeout: '5m',
    failOnTimeout: false, // 重试而非永久失败
  }

  async execute() {
    for (const rowId of this.payload.rows) {
      // 在处理每一行之前检查任务是否已超时
      if (this.signal?.aborted) {
        throw new Error('Job timed out during report generation')
      }

      await this.processRow(rowId)
    }
  }

  private async processRow(rowId: number) {
    // 处理单个行
  }
}

依赖注入

任务是通过 AdonisJS IoC 容器实例化的,因此你可以使用构造函数注入来访问服务:

app/jobs/process_payment.ts
import { inject } from '@adonisjs/core'
import { Job } from '@adonisjs/queue'
import type { JobOptions } from '@adonisjs/queue/types'
import PaymentService from '#services/payment_service'

interface ProcessPaymentPayload {
  orderId: number
  amount: number
  currency: string
}

@inject()
export default class ProcessPayment extends Job<ProcessPaymentPayload> {
  static options: JobOptions = {
    queue: 'payments',
    maxRetries: 3,
  }

  constructor(private paymentService: PaymentService) {
    super()
  }

  async execute() {
    await this.paymentService.charge(
      this.payload.orderId,
      this.payload.amount,
      this.payload.currency
    )
  }
}

派发任务

使用静态的 dispatch 方法从应用中的任意位置派发任务。任务负载是类型安全的,与任务类上定义的泛型参数相匹配。

app/controllers/orders_controller.ts
import ProcessPayment from '#jobs/process_payment'

export default class OrdersController {
  async store({ request, response }: HttpContext) {
    const order = await Order.create(request.body())

    // 派发任务以进行后台处理
    await ProcessPayment.dispatch({
      orderId: order.id,
      amount: order.total,
      currency: 'USD',
    })

    return response.created(order)
  }
}

从 Ace 命令派发任务

当你的应用为 HTTP 请求启动时,来自 locations 配置的任务文件会被自动加载。queue:work 命令也会在启动 worker 之前加载它们。

自定义的 Ace 命令则不同:在控制台启动生命周期中,任务不会被自动发现。如果你的命令会派发任务,并且你在本地或测试中使用 sync 适配器,请在派发前加载配置的任务位置。

commands/import_orders.ts
import { BaseCommand } from '@adonisjs/core/ace'
import ImportOrders from '#jobs/import_orders'

export default class ImportOrdersCommand extends BaseCommand {
  static commandName = 'orders:import'

  static options = {
    startApp: true,
  }

  async run() {
    const queue = await this.app.container.make('queue.manager')

    await queue.loadJobs()

    await ImportOrders.dispatch({
      source: 'warehouse',
    })
  }
}

如果你的命令只是将任务派发到 Redis 或 Database,导入任务类就足以将其入队。调用 queue.loadJobs() 使同一个命令与 sync 适配器保持兼容,后者会在当前进程中立即执行任务。

派发选项

dispatch 方法返回一个流畅的构建器,让你能够在将任务发送到队列之前自定义其行为:

app/controllers/orders_controller.ts
// 派发到特定队列并设为高优先级
await ProcessPayment.dispatch({
  orderId: order.id,
  amount: order.total,
  currency: 'USD',
})
  .toQueue('payments')
  .priority(1)
.toQueue(name)
string

将任务发送到特定队列,而非默认队列。

await ProcessPayment.dispatch(payload).toQueue('payments')
.priority(level)
number

设置任务优先级。数字越小越先处理(1 = 最高优先级,10 = 最低优先级)。

await ProcessPayment.dispatch(payload).priority(1)
.in(delay)
Duration

将任务执行延迟一个指定的时长。接受毫秒值或像 '5m''1h''7d' 这样的字符串。

// 24 小时后发送提醒
await SendReminder.dispatch(payload).in('24h')
.group(groupId)
string

分配一个组标识符以组织相关任务。便于跟踪批量操作。

await GenerateReport.dispatch(payload).group('monthly-reports-2025')
.with(adapter)
string

为该任务使用特定适配器,而非默认适配器。

await ProcessPayment.dispatch(payload).with('redis')

任务去重

任务去重 可防止多次单个任务派发将相同的工作加入队列。当 HTTP 重试、Webhook 重试或重复的按钮点击可能多次派发相同的后台任务时,这会很有用。

dedup 方法接受一个用户定义的 id。在底层,队列会为其加上任务名称前缀,因此两个不同的任务类可以使用相同的 id 而不会冲突。

app/controllers/orders_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import Order from '#models/order'
import ProcessPayment from '#jobs/process_payment'

export default class OrdersController {
  async retryPayment({ params, response }: HttpContext) {
    const order = await Order.findOrFail(params.id)

    const result = await ProcessPayment.dispatch({
      orderId: order.id,
      amount: order.total,
      currency: order.currency,
    })
      .toQueue('payments')
      .dedup({ id: `order:${order.id}`, ttl: '10m' })

    return response.accepted({
      jobId: result.jobId,
      deduped: result.deduped,
    })
  }
}

当使用 .dedup() 时,派发结果会包含一个 deduped 结果值。当你的控制器或服务需要对被跳过或替换的派发做出不同反应时,可以使用该值。

结果含义
added插入了一个新任务
skipped已存在重复的待处理任务,新的派发被忽略
replaced一个待处理或延迟的重复任务保留了其任务 id,但接收了最新的负载
extended一个重复任务被跳过,其去重窗口被刷新

去重支持以下派发模式:

方法调用行为
.dedup({ id })当具有相同键的现有任务仍然存在时,跳过重复项
.dedup({ id, ttl })在一个时间窗口内跳过重复项
.dedup({ id, ttl, extend: true })跳过重复项,并在每次重复时刷新时间窗口
.dedup({ id, ttl, replace: true })替换现有待处理或延迟任务的负载
.dedup({ id, ttl, extend: true, replace: true })通过替换负载并刷新时间窗口来防抖(debounce)派发

extendreplace 选项需要一个正的 ttl。使用 replace 时,只更新任务负载。现有任务会保留其队列、优先级、延迟和组 id。活跃的任务以及已保留的已完成或失败任务会被跳过,而非被修改。

Warning

去重仅由 Redis 和 Database 适配器针对单个任务派发应用。Sync 适配器会以内联方式运行每次派发,而批量派发或定时任务不会应用去重。

当防止重复很重要时,请使用 Redis 或 Database。对于批量或定时的流程,请在你的应用逻辑、数据库约束或专门的锁中强制唯一性。

用户提供的 id 长度必须不超过 400 个字符。最终构建为 <jobName>::<id> 的键长度必须不超过 510 个字符。

批量派发

当你需要派发许多相同类型的任务时,请使用 dispatchMany 以获得更好的性能。它在底层使用了批处理操作(Redis 流水线或 SQL 批量插入)。

app/services/newsletter_service.ts
import SendNewsletter from '#jobs/send_newsletter'

export default class NewsletterService {
  async sendToAllSubscribers(subscribers: { email: string }[]) {
    const payloads = subscribers.map((subscriber) => ({
      email: subscriber.email,
      subject: 'Monthly Newsletter',
    }))

    const { jobIds } = await SendNewsletter.dispatchMany(payloads)
      .toQueue('emails')
      .group('newsletter-march-2025')

    console.log(`Dispatched ${jobIds.length} newsletter jobs`)
  }
}

重试与退避

当任务抛出错误时,队列系统会根据配置的重试策略自动重试。你可以在三个级别配置重试,更具体的设置优先级更高:任务 > 队列 > 全局

退避策略

退避策略控制重试尝试之间的延迟。该包提供了四种内置策略:

config/queue.ts
import { defineConfig, drivers } from '@adonisjs/queue'
import { exponentialBackoff } from '@adonisjs/queue'

export default defineConfig({
  default: 'redis',
  adapters: {
    redis: drivers.redis({ connectionName: 'main' }),
  },
  retry: {
    maxRetries: 3,
    backoff: exponentialBackoff(),
  },
  // ...
})

指数退避(exponential backoff) 会使每次尝试的延迟翻倍:1s、2s、4s、8s,依此类推。这是大多数用例的推荐策略,因为它能防止压垮失败的服务。

import { exponentialBackoff } from '@adonisjs/queue'

// 默认:1s 基准、5m 最大、2x 倍数、启用抖动
exponentialBackoff()

// 自定义
exponentialBackoff({ baseDelay: '500ms', maxDelay: '1m' })

线性退避(linear backoff) 每次尝试增加基准量的延迟:5s、10s、15s、20s,依此类推。

import { linearBackoff } from '@adonisjs/queue'

// 默认:5s 基准、2m 最大
linearBackoff()

// 自定义
linearBackoff({ baseDelay: '10s', maxDelay: '5m' })

固定退避(fixed backoff) 每次重试都使用相同的延迟:10s、10s、10s,依此类推。

import { fixedBackoff } from '@adonisjs/queue'

// 默认:10s
fixedBackoff()

// 自定义
fixedBackoff('30s')

自定义退避(custom backoff) 让你完全控制策略配置:

import { customBackoff } from '@adonisjs/queue'

customBackoff({
  strategy: 'exponential',
  baseDelay: '100ms',
  maxDelay: '30s',
  multiplier: 3,
  jitter: false,
})
Tip

在指数和线性策略上启用 jitter,以为重试延迟添加随机性。这能防止多个失败的任务在同一时刻重试,从而压垮下游服务(一种被称为「惊群效应」的模式)。

单任务重试

你可以为特定任务覆盖重试配置:

app/jobs/process_payment.ts
import { Job } from '@adonisjs/queue'
import { exponentialBackoff } from '@adonisjs/queue'
import type { JobOptions } from '@adonisjs/queue/types'

export default class ProcessPayment extends Job<ProcessPaymentPayload> {
  static options: JobOptions = {
    maxRetries: 5,
    retry: {
      backoff: exponentialBackoff({ baseDelay: '2s', maxDelay: '10m' }),
    },
  }

  async execute() {
    // 支付处理逻辑
  }
}

定时任务

定时任务会在定义的时间间隔或 cron 表达式下自动运行。在 start/scheduler.ts 预加载文件中定义你的计划。

Cron 计划

使用 cron 表达式进行精确调度:

start/scheduler.ts
import CleanupExpiredSessions from '#jobs/cleanup_expired_sessions'
import GenerateWeeklyReport from '#jobs/generate_weekly_report'

// 每天午夜运行
await CleanupExpiredSessions.schedule({ retentionDays: 30 })
  .cron('0 0 * * *')
  .timezone('Europe/Paris')

// 每周一上午 9 点运行
await GenerateWeeklyReport.schedule({ type: 'weekly' })
  .cron('0 9 * * MON')
  .timezone('America/New_York')

间隔计划

对于更简单的用例,以固定的时间间隔调度任务:

start/scheduler.ts
import SyncInventory from '#jobs/sync_inventory'

// 每 5 分钟运行一次
await SyncInventory.schedule({ source: 'warehouse-api' })
  .every('5m')

计划选项

计划构建器支持以下选项:

.cron(expression)
string

一个定义任务何时运行的 cron 表达式。与 .every() 互斥。

.every(interval)
Duration

两次运行之间的时间间隔。与 .cron() 互斥。

.id(scheduleId)
string

计划的自定义标识符。默认为任务类名。如果已存在具有相同 ID 的计划,它将被更新。

.timezone(tz)
string

用于计算 cron 表达式的 IANA 时区。默认为 'UTC'

.from(date)
Date

开始边界。在此日期之前不会派发任何任务。

.to(date)
Date

结束边界。在此日期之后不会派发任何任务。

.between(from, to)
Date, Date

同时设置 .from().to() 的简写。

.limit(maxRuns)
number

计划运行的最大次数。 :::

管理计划

你可以使用 Schedule 类以编程方式管理计划:

app/controllers/admin_controller.ts
import { Schedule } from '@adonisjs/queue'

export default class AdminController {
  async listSchedules() {
    // 列出所有计划
    const schedules = await Schedule.list()

    // 仅列出活跃的计划
    const active = await Schedule.list({ status: 'active' })

    return schedules
  }

  async pauseSchedule({ params }: HttpContext) {
    const schedule = await Schedule.find(params.id)

    if (schedule) {
      await schedule.pause()
    }
  }

  async resumeSchedule({ params }: HttpContext) {
    const schedule = await Schedule.find(params.id)

    if (schedule) {
      await schedule.resume()
    }
  }

  async triggerSchedule({ params }: HttpContext) {
    const schedule = await Schedule.find(params.id)

    if (schedule) {
      // 立即派发定时任务
      await schedule.trigger()
    }
  }

  async deleteSchedule({ params }: HttpContext) {
    const schedule = await Schedule.find(params.id)

    if (schedule) {
      await schedule.delete()
    }
  }
}

计划相关的 Ace 命令

该包提供了用于在终端中管理计划的 Ace 命令:

# 列出所有定时任务
node ace queue:scheduler:list

# 仅列出活跃的计划
node ace queue:scheduler:list --status=active

# 移除特定的计划
node ace queue:scheduler:remove <schedule-id>

# 移除所有计划
node ace queue:scheduler:clear

运行 worker

在启动 worker 之前,任务不会被处理。worker 是一个长期运行的进程,它会轮询队列中可用的任务并执行它们。

worker 还会为活跃的任务发送心跳。这些心跳会在处理器仍在运行时刷新任务所有权的时间戳,因此健康的长时间运行任务不会因为运行时间超过 stalledThreshold 就被当作卡住而回收。一个 worker 只能刷新它仍然拥有的任务,因此迟到的心跳无法延长一个已被另一个 worker 回收的任务。

使用 queue:work Ace 命令启动 worker:

node ace queue:work

Worker 选项

# 处理特定的队列
node ace queue:work --queue=payments,emails

# 设置并发数(同时处理的任务数量)
node ace queue:work --concurrency=10
Warning

你必须将 worker 作为一个独立的进程,与你的 Web 服务器一同启动。在 worker 运行之前,从你的应用派发的任务不会被处理。

在生产环境中,请使用像 PM2 这样的进程管理器或容器编排器来保持 worker 运行,并在其失败时重启。

Worker 配置

在你的 config/queue.ts 文件中配置 worker 的行为:

config/queue.ts
export default defineConfig({
  // ...
  worker: {
    concurrency: 5,
    idleDelay: '2s',
    stalledThreshold: '30s',
    stalledInterval: '30s',
    maxStalledCount: 1,
    gracefulShutdown: true,
  },
})
concurrency
number

同时处理的最大任务数。默认为 1

idleDelay
Duration

当没有可用任务时,worker 在下一次轮询之前等待的时间。默认为 '2s'

timeout
Duration

任何任务的全局最大执行时间。可以通过 JobOptions.timeout 在任务级别覆盖。默认不超时。

stalledThreshold
Duration

一个任务在没有心跳的情况下,在被认定为卡住之前可以持续的时间。这是一个崩溃检测窗口,而非最大任务运行时长。当你想限制任务可运行的时长时,请使用 timeoutJobOptions.timeout。默认为 '30s'

stalledInterval
Duration

worker 检查卡住任务的频率。默认为 '30s'

maxStalledCount
number

一个卡住的任务在被永久标记失败之前,最多可被回收的次数。默认为 1

gracefulShutdown
boolean

true 时,worker 会在收到 SIGINT/SIGTERM 信号后处理完正在运行的任务再停止。默认为 true。 :::

测试

该包提供了一个 fake 适配器,它会将派发的任务记录在内存中,并暴露断言辅助函数。这让你能够验证应用派发了正确的任务,而无需真正处理它们。

伪造队列

使用 QueueManager.fake() 将所有适配器替换为 fake 适配器,并使用 QueueManager.restore() 进行恢复:

tests/functional/orders.spec.ts
import { test } from '@japa/runner'
import { QueueManager } from '@adonisjs/queue'
import ProcessPayment from '#jobs/process_payment'

test.group('Orders', (group) => {
  group.each.teardown(() => {
    QueueManager.restore()
  })

  test('dispatches a payment job when creating an order', async ({ client }) => {
    const fake = QueueManager.fake()

    const response = await client.post('/orders').json({
      product_id: 1,
      quantity: 2,
    })

    response.assertStatus(201)

    // 断言任务已被派发
    fake.assertPushed(ProcessPayment)

    // 使用负载匹配进行断言
    fake.assertPushed(ProcessPayment, {
      payload: { orderId: 1, amount: 100, currency: 'USD' },
    })
  })

  test('does not dispatch a payment job for free orders', async ({ client }) => {
    const fake = QueueManager.fake()

    await client.post('/orders').json({
      product_id: 1,
      quantity: 0,
    })

    fake.assertNotPushed(ProcessPayment)
  })
})

断言方法

fake 适配器提供了以下断言方法:

assertPushed(job, query?)
void

断言某个任务已被派发。可按队列、负载或延迟进行筛选。

fake.assertPushed(ProcessPayment)
fake.assertPushed(ProcessPayment, { queue: 'payments' })
fake.assertPushed(ProcessPayment, {
  payload: { orderId: 1 },
})
assertNotPushed(job, query?)
void

断言某个任务未被派发。

fake.assertNotPushed(SendEmail)
assertPushedCount(count, options?)
void

断言派发的任务总数,可按队列筛选。

fake.assertPushedCount(3)
fake.assertPushedCount(2, { queue: 'emails' })
assertNothingPushed()
void

断言根本没有派发任何任务。

fake.assertNothingPushed()

:::

进阶匹配

你可以使用函数进行更复杂的负载匹配:

tests/functional/newsletter.spec.ts
import { test } from '@japa/runner'
import { QueueManager } from '@adonisjs/queue'
import SendNewsletter from '#jobs/send_newsletter'

test('dispatches newsletter jobs for all subscribers', async ({ client }) => {
  const fake = QueueManager.fake()

  await client.post('/newsletters/send')

  // 使用函数进行匹配
  fake.assertPushed(SendNewsletter, {
    payload: (payload) => payload.email.endsWith('@example.com'),
  })

  // 检查延迟
  fake.assertPushed(SendNewsletter, {
    delay: (delay) => delay !== undefined && delay > 0,
  })
})