Wasp 后台任务 Jobs 实战指南:用 PgBoss 实现持久化、重试与定时任务
2026/9/16 0:28:15 网站建设 项目流程

Wasp 后台任务 Jobs 实战指南:用 PgBoss 实现持久化、重试与定时任务

【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp

本篇指南以 Wasp v0.19 文档中的 Jobs 后台任务模块为核心,系统讲解如何在 Wasp 应用中声明 Job、编写 worker 函数、通过submit/delay提交任务,以及借助PgBoss执行器实现任务持久化、失败重试、延迟执行与 cron 定时调度。读完本文,你将能够在自己的 Wasp 项目中把发邮件、调用外部 API、批量数据处理等耗时工作移出请求链路,同时掌握PG_BOSS_NEW_OPTIONS定制、数据保留策略与常见坑位规避等实战要点。

为什么需要后台任务

在大多数 Web 应用中,用户向服务器发请求并得到响应。只要服务器响应够快,应用就足够流畅。但有些请求需要额外时间才能完整处理——比如发送一封邮件,或向外部 API 发起一次较慢的 HTTP 调用。此时更好的做法是尽快响应用户,把剩余工作放到后台执行

Wasp 的后台任务(Jobs)就是为此设计的,它在 v0.19 中具备四大核心能力:

  • 跨服务器重启持久化:任务在服务器重启后依然存在,不会丢失;
  • 失败自动重试:任务执行失败时可以按配置重试;
  • 延迟执行:任务可以被推迟到未来的某个时间点执行;
  • 周期调度:任务可以挂上 cron 表达式按计划反复运行。

三步定义一个 Job 并投入使用

以官方文档中的经典示例为例:写一个 Job,向控制台打印一条消息,并从数据库中返回任务列表。整个过程分三步。

第一步:在.wasp文件中声明 Job

main.wasp中新增一段job声明(JavaScript 与 TypeScript 项目写法一致):

job mySpecialJob { executor: PgBoss, perform: { fn: import { foo } from "@src/workers/bar" }, entities: [Task], }

这段声明表达了三件事:使用PgBoss作为执行器;perform.fn指向src/workers/bar.ts(或.js)中导出的foo函数;并把这个 Job 需要用到的 Prisma 实体Task注入到上下文。

第二步:实现 worker 函数

export const foo = async ({ name }, context) => { console.log(`Hello ${name}!`) const tasks = await context.entities.Task.findMany({}) return { tasks } }

TypeScript 版本可以利用 Wasp 生成的泛型类型获得完整类型推导:

import { type MySpecialJob } from 'wasp/server/jobs' import { type Task } from 'wasp/entities' type Input = { name: string; } type Output = { tasks: Task[]; } export const foo: MySpecialJob<Input, Output> = async ({ name }, context) => { console.log(`Hello ${name}!`) const tasks = await context.entities.Task.findMany({}) return { tasks } }

关于 worker 函数有几点必须牢记:

  • 必须是async函数,其返回值即 Job 的结果;
  • 接收两个参数:args(提交任务时传入的数据)与context: { entities }(声明在 Job 中的实体集合);
  • 由于 Wasp 在服务端执行 Job,perform.fn的 import 路径必须指向一个 NodeJS 文件(不能是浏览器端代码)。

第三步:提交任务

在 Operations(即 Query/Action)、setupFn,或任何其他 NodeJS 代码中导入并提交任务:

import { mySpecialJob } from 'wasp/server/jobs' const submittedJob = await mySpecialJob.submit({ job: "Johnny" }) // 或者希望延迟执行,加一个 .delay() 即可。 // 参数可以是秒数(number)、Date 对象,或 ISO 日期字符串。 await mySpecialJob .delay(10) .submit({ name: "Johnny" })

提交后,Job 会由PgBoss执行,效果等同于你直接调用了foo({ name: "Johnny" })。需要注意的是,foo是否接收参数完全取决于 worker 函数的实现——不传参的任务同样合法。

从源码结构看,Wasp 编译器在waspc/src/Wasp/AppSpec/Job.hs中定义了 Job 的完整数据结构(Job.hs):executorperform、可选的scheduleentities,其中Perform又由fn和可选的executorOptions组成(Job.hs)。你在.wasp文件里写的声明会被解析成这个数据模型,随后由代码生成器产出对应的 SDK 调用接口。

定时任务:给 Job 加上 cron 调度

如果某些工作需要周期性执行,只需在 Job 声明中追加schedule

job mySpecialJob { executor: PgBoss, perform: { fn: import { foo } from "@src/workers/bar" }, schedule: { cron: "0 * * * *", args: {=json { "job": "args" } json=} // 可选 } }

加了schedule后,你不需要再手动调用任何东西——可以想象成foo({ job: "args" })被自动注册进调度器,每个整点自动执行一次。其中:

  • cron必填的 5 字段 cron 表达式(分钟级精度),例如"0 * * * *"表示每小时整点执行,"*/5 * * * *"表示每 5 分钟执行一次;
  • args可选的 JSON 对象,作为固定参数传给perform.fn

这个数据模型同样对应着源码中的Schedule类型(Job.hs):cron :: String、可选的args :: JSONexecutorOptions。代码生成器在 JobGenerator.hs 中会把schedule的 cron、args 与 options 序列化进生成的 Job 定义,由运行时的 pg-boss 调度器接管。

Job 执行器:PgBoss 深入解析

Wasp 通过**Job 执行器(job executor)**来调度、监控和执行任务。在 v0.19 中,Wasp 只内置了唯一一个执行器:PgBoss。从编译器源码的JobExecutor类型可以确认,它只有一个数据构造器PgBoss(Job.hs),且 JSON 解析只接受字面量"PgBoss"

PgBoss 是什么

PgBoss是一个构建在 PostgreSQL 之上的轻量级任务队列。它不需要额外的中间件或复杂的基础设施,适合低流量的生产场景。它的设计核心是直接用 PostgreSQL 作为存储与同步机制(借助SELECT ... SKIP LOCKED实现行级锁跳过),从而让你在已有 Postgres 数据库上白得传统任务队列的绝大部分能力——持久化、重试、延迟、调度一次到位。

前置要求

使用PgBoss前,必须把schema.prisma中的数据库 provider 设置为"postgresql"(详见数据库配置)。SQLite 无法运行 pg-boss。

运行机制与局限性

PgBoss与你的 Web 服务器运行在同一个进程中,并不是独立进程或独立服务。它随服务器启动而启动、随服务器停止而停止。由此带来两点明确的局限:

  • 不适合 CPU 密集型任务:它和 Web 服务器的业务逻辑共享 CPU,重计算任务会拖慢主进程;
  • 暂不支持独立水平扩展:不能把 pg-boss 单独拆成 worker/进程/线程横向扩容。服务器必须在线才能处理任务;如果确实需要扩展任务处理能力,只能运行多个 Web 服务器实例,每个实例各自持有一个PgBoss实例。

从模板源码可以看到这套"与服务器同生命周期"机制:生成的 pgBoss.ts 中startPgBoss()保证一个服务器生命周期内只启动一次 pg-boss,boss.start()会自动在目标数据库中创建所需的内部对象,随后才允许提交任务。

定制 PgBoss 实例:PG_BOSS_NEW_OPTIONS

如果需要自定义 PgBoss 实例的初始化参数,可以设置环境变量PG_BOSS_NEW_OPTIONS,值为一个字符串化的 JSON 对象,直接对应 pg-boss 的new PgBoss(options)参数。

关键警告:设置该环境变量会覆盖 Wasp 的全部默认值,因此必须把connectionString一并写进去,否则 pg-boss 将无法连接数据库。模板源码印证了这一点:默认配置只有{ connectionString: config.databaseUrl },一旦检测到PG_BOSS_NEW_OPTIONS就整体JSON.parse替换(pgBoss.ts)。

例如,同时设置连接串、任务归档时间与删除策略:

# 在 .env 文件中 PG_BOSS_NEW_OPTIONS={"connectionString":"postgresql://user:password@server:5432/database","archiveCompletedAfterSeconds":86400,"deleteAfterDays":30,"maintenanceIntervalMinutes":5} # 在 shell 中 PG_BOSS_NEW_OPTIONS='{"connectionString":"postgresql://user:password@server:5432/database","archiveCompletedAfterSeconds":86400,"deleteAfterDays":30,"maintenanceIntervalMinutes":5}'

关于 JSON 环境变量的转义规则,可参考环境变量文档中的 JSON Env Vars 一节。

数据库结构与自动建表

使用 PgBoss 时,数据库的准备工作完全由 Wasp 服务器自动完成,不需要写进你的 schema 或 migration。以下信息仅作了解:

所有任务数据存放在一个独立的数据库 schemapgboss中,内部包含jobarchiveschedule等追踪表。这些表大多带有一个name列,其值对应你的 Job 标识符(即.wasp文件中的 job 名)。表内还会维护参数(arguments)、状态(states)、返回值、重试信息、开始与过期时间等 pg-boss 运行所需的元数据。

已知问题:重命名定时任务会导致任务"幽灵化"

.wasp文件中的 Job 名会被原样写进pgboss各表的name列。如果你重命名了一个带schedule的任务,pg-boss 会继续按旧名称调度,但此时已经没有对应的 handler 了——这些任务会一直积压,最终因无处理器而过期作废。

解决办法是手动清理pgboss.schedule表中的旧记录。例如把任务从emailReminder改名为sendEmailReminder后,执行:

BEGIN; DELETE FROM pgboss.schedule WHERE name = 'emailReminder'; COMMIT;

重要提醒:只有对 SQL 操作有把握时才直接改数据库。不确定的话,要么保留旧任务名,要么在开发环境用全新数据库重启。

任务数据保留与清理

默认情况下,PgBoss在任务完成或失败后保留数据12 小时,之后移动到 archive(归档)表,归档数据再保留7 天后删除。如果需要调整,可通过PG_BOSS_NEW_OPTIONS配置归档与删除参数:

  • 归档相关:archiveCompletedAfterSecondsarchiveFailedAfterSeconds
  • 删除相关:deleteAfterSecondsdeleteAfterMinutesdeleteAfterDays等;
  • 维护周期:maintenanceIntervalMinutes
PG_BOSS_NEW_OPTIONS={"connectionString":"...your postgress connection url...","archiveCompletedAfterSeconds":86400,"deleteAfterDays":30,"maintenanceIntervalMinutes":5}

API 参考

Job 声明的全部字段

一个完整的 Job 声明示例如下(main.wasp):

job mySpecialJob { executor: PgBoss, perform: { fn: import { foo } from "@src/workers/bar", executorOptions: { pgBoss: {=json { "retryLimit": 1 } json=} } }, schedule: { cron: "*/5 * * * *", args: {=json { "foo": "bar" } json=}, executorOptions: { pgBoss: {=json { "retryLimit": 0 } json=} } }, entities: [Task], }

各字段说明:

  • executor: JobExecutor(必填):任务使用的执行器,目前唯一支持PgBoss
  • perform: dict(必填):
    • fn: ExtImport(必填):执行任务的async函数,import 路径必须指向 NodeJS 文件;接收args(提交时传入的数据)与context: { entities }两个参数;
    • executorOptions: dict:提交任务时使用的执行器默认选项,直接透传给执行器。其中pgBoss为 JSON,具体参数参见 pg-boss 的send(name, data, options)文档。这些默认值可在submit()调用时或schedule中覆盖;
  • schedule: dict
    • cron: string(必填):5 字段 cron 表达式(分钟级精度);
    • args: JSON:执行时传给perform.fn的参数;
    • executorOptions: dict:调度提交时的执行器选项。优先级规则perform.executorOptions是默认值,schedule.executorOptions可以在其基础上覆盖/扩展;
  • entities: [Entity]:Job 内需要使用的 Prisma 实体列表,用法与 Query/Action 中的实体声明一致(参见Queries 文档)。

从编译器数据模型看,ExecutorOptions就是包裹着pgBoss :: Maybe JSON的结构(Job.hs),performschedule各持一份,生成代码时会被分别提取(对应源码中的performExecutorOptionsJsonscheduleExecutorOptionsJson辅助函数,Job.hs)。

JavaScript API

导入 Job:

import { mySpecialJob } from 'wasp/server/jobs'

TypeScript 项目可以同时导入生成的值与类型:

import { mySpecialJob, type MySpecialJob } from 'wasp/server/jobs'

类型安全的 Jobs(TypeScript):Wasp 会为每个 Job 声明生成一个泛型类型,用于约束 worker 函数。该类型以 Job 声明命名(如MySpecialJob),暴露在wasp/server/jobs模块中,接收两个类型参数:

  • Inputperform.fnargs参数类型;
  • Outputperform.fn返回值类型。

代码生成器在 JobGenerator.hs 中正是通过toUpperFirst jobName把 job 名转成 PascalCase 的类型名,再与运行值一起导出。

submit(jobArgs, executorOptions)

  • jobArgs: Input:传给 worker 函数的 JSON 参数;
  • executorOptions: object:执行器专属的提交选项(可选)。
const submittedJob = await mySpecialJob.submit({ job: "args" })

delay(startAfter)

  • startAfter: int | string | Date(必填):延迟调度时间,支持三种形态:
    • 整数:延迟的秒数(默认 0);
    • 字符串:ISO 日期字符串,到点执行;
    • Date:Date 对象,到点执行。
const submittedJob = await mySpecialJob .delay(10) .submit({ job: "args" }, { "retryLimit": 2 })

上面示例把任务延迟 10 秒提交,并附加retryLimit: 2作为执行器选项。

任务追踪

submit()返回一个SubmittedJob实例,包含以下字段:

  • jobId:该 Job 在执行器中的 ID;
  • jobName:你在.wasp文件中使用的 Job 名;
  • executorName:Job 执行器名称的 Symbol。

此外还有执行器专属的命名空间对象。对 pg-boss 而言,可通过submittedJob.pgBoss访问:

  • details():获取 pg-boss 专属的任务详情(对应 pg-boss 的getJobById(id));
  • cancel():尝试取消一个任务(对应cancel(id));
  • resume():尝试恢复一个已取消的任务(对应resume(id))。

仓库中的真实示例:kitchen-sink 的 Jobs 模块

Wasp 仓库自带的 kitchen-sink 示例应用包含一个完整的 Jobs 功能模块,可以用来对照验证。它的任务声明位于 jobs.wasp.ts(当前仓库已演进为 TypeScript spec 语法,job(...)构造器与 v0.19 文档的job ... {}声明语义一致):

job(uppercaseTextJob, { executor: "PgBoss", entities: ["UppercaseTextRequest"], }), job(mySpecialJob, { executor: "PgBoss", performExecutorOptions: { pgBoss: { retryLimit: 1 } }, entities: ["Task"], }), job(mySpecialScheduledJob, { executor: "PgBoss", schedule: { cron: "0 * * * *", args: { foo: "bar" }, executorOptions: { pgBoss: { retryLimit: 2 } }, }, }),

其中uppercaseTextJob是一个很有代表性的真实 worker 函数(uppercaseText.ts):它接收{ requestId: string },通过context.entities.UppercaseTextRequest查询数据库、模拟 2 秒延迟、把输入文本转成大写并回写状态;出错时捕获异常、把记录标记为ERROR再重新抛出——完整演示了"查询实体 → 执行耗时逻辑 → 更新实体 → 错误处理"的标准任务模式,与文档中context.entities.Task.findMany({})的用法一脉相承。

小结

Wasp 的 Jobs 特性让后台任务从"自己搭队列、自己写重试"变成了一行声明 + 一个函数。核心要点回顾:

  • Job 声明包含executorperform(必填)与scheduleentities(可选);
  • worker 必须是async函数,通过context.entities访问 Prisma 实体;
  • submit()立即入队,.delay(秒数 | ISO 字符串 | Date)延迟执行,schedule.cron实现周期调度;
  • 执行器目前只有PgBoss,要求 Postgres 数据库、与服务器同进程运行,PG_BOSS_NEW_OPTIONS可完全接管其初始化参数(务必带上connectionString);
  • 重命名带调度的任务前,记得清理pgboss.schedule表,避免产生无人认领的"幽灵任务"。

掌握了这些,发邮件、拉取外部 API、批量数据处理等耗时操作都可以放心地搬进后台,让用户请求路径保持轻快。

【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询