运行定时任务和后台任务

CloudflareBeginner
立即练习

简介

支持团队的健康监控需要通过两种方式运行同一个小型检查:用户请求后运行,以及无需用户请求、按固定周期运行。你将实现这两种方式,区分「已确认接收」和「已完成」,在本地测试事件生命周期,并观察一次真实的云端定时调用。

这台独立 VM 启动时位于 /home/labex/project/task-monitor,环境包含 Node.js 22.22.0、项目本地的 Wrangler 4.131.1、用于隔离评估的 Miniflare 4.20260730.0,以及一个合成的内部健康服务测试装置。你将复用之前学习过的服务绑定和设备授权技能,而不是使用之前实验中的 VM 或资源。请使用你自己的学习账号;不需要域名、存储产品或付费升级。请求和定时调用会计入账号的正常用量。

保持一个终端窗口打开。每分钟一次的短周期仅用于一次性测试。结束实验前,请停止日志流,在仍获得授权的情况下删除两个 Worker,确认它们已不存在,并退出登录。本实验中的后台工作有时间限制且不持久化;不要将其视为可靠的排队投递承诺。

在后台工作完成前返回响应

在此步骤中,公开端点会确认收到一个小型健康检查请求,同时由提供的内部服务执行异步工作。这是一台独立 VM;该服务是新的测试装置,不是之前实验中的资源。

cd /home/labex/project/task-monitor
node --version
npx wrangler --version
cat health/index.js

该测试装置等待 250 毫秒后返回合成 JSON。它不会发送外部请求,也不会存储数据。生成唯一的基础名称,然后配置公开 Worker 及其内部 HEALTH 服务。这些就是你之前使用过的标准服务绑定。

WORKER_NAME="labex-tasks-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false,
  "services": [
    {
      "binding": "HEALTH",
      "service": "$WORKER_NAME-health"
    }
  ],
  "triggers": {
    "crons": []
  }
}
CONFIG
cat > health/wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME-health",
  "main": "index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": false,
  "preview_urls": false
}
CONFIG
cat > src/index.js <<'JS'
async function checkHealth(env, details) {
  const url = new URL('https://health.internal/health');
  url.searchParams.set('probe', details.probe);
  const response = await env.HEALTH.fetch(url, {signal: AbortSignal.timeout(3000)});
  if (!response.ok) throw new Error('health_service_unavailable');
  const data = await response.json();
  if (data.status !== 'ok' || data.service !== 'labex-health-fixture' || data.probe !== details.probe) {
    throw new Error('unexpected_health_response');
  }
  console.log(JSON.stringify({event: 'health_check', ...details, status: data.status, service: data.service}));
}

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/checks') return Response.json({error: 'not_found'}, {status: 404});
    if (request.method !== 'POST') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'POST'}});
    }
    const probe = url.searchParams.get('probe') || '';
    if (!/^[a-z0-9-]{1,48}$/.test(probe)) {
      return Response.json({error: 'invalid_probe'}, {status: 400});
    }
    ctx.waitUntil(checkHealth(env, {source: 'request', probe}).catch(() => {
      console.error(JSON.stringify({event: 'health_check_failed', source: 'request', probe}));
    }));
    return Response.json({accepted: true, probe}, {status: 202, headers: {'Cache-Control': 'no-store'}});
  }
};
JS

checkHealth 会验证实际依赖服务的响应,并输出一条简短的结构化结果。三秒的中止信号会限制请求时长。公开处理程序将其 Promise 传递给 ctx.waitUntil,然后立即返回 HTTP 202。202 表示这次短时尝试已被确认接收,但不承诺持久化投递。catch 会记录失败,但不会输出原始异常、请求或凭据。这里只能使用上述显示的合成探针值。

npx wrangler dev -c wrangler.jsonc -c health/wrangler.jsonc --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &

等待命令行提示符出现,然后检查日志。当公开开发服务器已在 8080 端口就绪后继续。如果服务器仍在启动,请短暂等待,再次读取日志。

cat dev.log
curl -i http://127.0.0.1:8080/health
curl -i -X POST "http://127.0.0.1:8080/checks?probe=manual-one"
cat dev.log

前台健康路由应返回 200,状态为 ok。POST 请求应返回 202,并包含 acceptedtrueprobemanual-one。依赖服务完成后,日志应包含事件 health_check、来源 request、相同的探针值和状态 ok。如果读取日志过早,请稍等片刻后再次读取。响应先于后续日志到达只能作为观察结果,不能作为精确的性能基准。

在服务器运行期间使用验证功能。隔离运行时会在受控门后运行自己的健康服务测试装置,要求前台响应先返回,再打开该门,随后要求后台工作完成。它还会检查公开健康路由,以及被拒绝的方法和探针请求。该测试不会调用 Cloudflare。

在本地调用定时处理程序

在此步骤中,从 Cron 触发的处理程序中复用健康检查操作。修改代码前,先检查并停止实际的开发任务。如果任务编号与示例不同,请替换为当前任务编号。

jobs
kill %1
cat > src/index.js <<'JS'
async function checkHealth(env, details) {
  const url = new URL('https://health.internal/health');
  url.searchParams.set('probe', details.probe);
  const response = await env.HEALTH.fetch(url, {signal: AbortSignal.timeout(3000)});
  if (!response.ok) throw new Error('health_service_unavailable');
  const data = await response.json();
  if (data.status !== 'ok' || data.service !== 'labex-health-fixture' || data.probe !== details.probe) {
    throw new Error('unexpected_health_response');
  }
  console.log(JSON.stringify({event: 'health_check', ...details, status: data.status, service: data.service}));
}

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/checks') return Response.json({error: 'not_found'}, {status: 404});
    if (request.method !== 'POST') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'POST'}});
    }
    const probe = url.searchParams.get('probe') || '';
    if (!/^[a-z0-9-]{1,48}$/.test(probe)) {
      return Response.json({error: 'invalid_probe'}, {status: 400});
    }
    ctx.waitUntil(checkHealth(env, {source: 'request', probe}).catch(() => {
      console.error(JSON.stringify({event: 'health_check_failed', source: 'request', probe}));
    }));
    return Response.json({accepted: true, probe}, {status: 202, headers: {'Cache-Control': 'no-store'}});
  },
  async scheduled(controller, env) {
    await checkHealth(env, {
      source: 'scheduled',
      probe: `cron-${controller.scheduledTime}`,
      cron: controller.cron,
      scheduledTime: controller.scheduledTime
    });
  }
};
JS
cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false,
  "services": [
    {
      "binding": "HEALTH",
      "service": "$WORKER_NAME-health"
    }
  ],
  "triggers": {
    "crons": [
      "* * * * *"
    ]
  }
}
CONFIG

五字段表达式 * * * * * 表示按 UTC 时间每分钟运行一次。这个高频计划仅用于短时间的一次性实验。scheduled 会等待健康操作完成,因此调用结果会反映完成状态;它不会返回 HTTP 响应。日志会记录触发器实际使用的 Cron 表达式和计划时间。

npx wrangler dev -c wrangler.jsonc -c health/wrangler.jsonc --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &

等待命令行提示符出现,然后检查日志。当公开开发服务器已在 8080 端口就绪后继续。如果服务器仍在启动,请短暂等待,再次读取日志。

cat dev.log
curl -i "http://127.0.0.1:8080/cdn-cgi/local/scheduled?cron=*+*+*+*+*&time=1700000000000&format=json"
cat dev.log

本地触发器应返回结果 ok,应用日志应包含来源 scheduled、Cron * * * * *scheduledTime 1700000000000。这个较早的时间戳是刻意设置的合成测试输入,不代表当前的云端执行。使用验证功能测试另一个受控的计划时间,并确认健康服务确实被调用。

对于 HTTP 调用,waitUntil 可以在响应发送或客户端断开后,将工作延长最多 30 秒;这段时间由请求的后台 Promise 共享。它不是持久化队列,也不保证重试。本实验中有三秒时限的小型操作适合这种场景。需要可靠投递或重试的工作,应在本实验之外采用合适的队列或工作流设计。有关不同调用生命周期的说明,请参阅 context APIscheduled handler 文档。

部署并观察请求触发的后台工作

在此步骤中,将两个新 Worker 部署到你自己的学习账号。先停止实际的本地任务,然后使用熟悉的设备流程为这台新 VM 授权。

jobs
kill %1
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read

在已登录的浏览器中打开显示的设备链接,输入设备代码,查看请求的访问权限,然后选择学习账号。等待终端显示成功。

npx wrangler whoami --json

即使只列出一个账号,也要确认账号名称。将下面两个配置中的 YOUR_ACCOUNT_ID 替换为实际账号 ID。保留相同的已生成 Worker 名称和每分钟一次的计划。公开 Worker 还会启用 Workers Logs,因此可以在 Dashboard 的 Observability 中查看其调用日志和应用日志。这与实时的 wrangler tail 连接相互独立。本实验只会输出合成的健康检查数据;绝不要记录凭据。请参阅 Workers Logs 文档

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false,
  "services": [
    {
      "binding": "HEALTH",
      "service": "$WORKER_NAME-health"
    }
  ],
  "triggers": {
    "crons": [
      "* * * * *"
    ]
  },
  "observability": {
    "enabled": true
  },
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
cat > health/wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME-health",
  "main": "index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": false,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy -c health/wrangler.jsonc
npx wrangler deploy

先部署内部健康服务,这样公开 Worker 的绑定才能解析到它。确认公开部署输出包含计划:* * * * *。复制实际的 workers.dev 地址,并在下面使用。

APP_URL="https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev"
curl -i "$APP_URL/health"
npx wrangler tail --format pretty > events.log 2> tail-errors.log &

等待几秒,让 tail 连接完成初始化,然后发送一次合成检查。

sleep 5
curl -i -X POST "$APP_URL/checks?probe=remote-one"
cat events.log

找到 POST 事件及其 health_check 日志。该日志应包含来源 request 和探针 remote-one。如果暂时看不到事件,请检查 tail-errors.log,短暂等待后再次发送请求,再次读取 events.log。之前实验留下的日志文件不能证明这次部署成功。

在 Dashboard 中,打开此账号的 Compute → Workers & Pages。确认两个 Worker 的完整名称、公开 Worker 地址,以及它的 HEALTH 绑定是否指向匹配的内部服务。使用验证功能:它会检查已授权的所有权和端点/绑定元数据,然后建立自己的短时实时 tail 会话并发送新的探针,以确认后台工作完成。评估程序不会将你的 events.log 视为证据。下一步中继续保持学习者的 tail 任务运行。

观察一次真实的 Cron 执行

在此步骤中,区分部署配置和实际执行。打开 Dashboard 中的公开 Worker,检查 Settings → Trigger events → Cron triggers。确认计划显示为 Every minute。Next time 是预测时间,不代表执行已完成。仅配置计划并不能证明处理程序已经运行。

下面的示例显示受支持的最短间隔:* * * * *,即每分钟一次。将 Worker 名称与自己的配置进行比较。名称和显示时间只是示例;如果 Wrangler 已经完成配置,就不需要在 Dashboard 中再次添加触发器。

已配置为每分钟运行一次的 Worker Cron 触发器

保持现有的学习者 tail 连接运行。等待真实的定时事件,然后检查同一个日志文件:

sleep 60
cat events.log

在可读的 tail 输出中找到一次成功的定时调用,依据 Cron 表达式和执行时间识别它。对应的 health_check 日志必须包含来源 scheduled、Cron * * * * *、状态 ok、服务 labex-health-fixture 以及 scheduledTime。探针值应为 cron- 后接该 scheduledTime。独立验证程序会单独将 Cloudflare 的结构化事件元数据与此应用日志进行比对。POST 事件、本地合成时间戳或空日志都不能证明结果成立。

要将该输出与浏览器中的信息关联起来,请打开同一 Worker 的 Observability → Events 页面。等待时使用 Live,或者刷新已保存的事件查询,并选择包含本次部署的时间范围。该处理程序的调用行会显示 * * * * *。展开其中一行,选择 View invocation,再展开关联的应用日志行,检查健康检查字段。结构化应用日志的 Message 单元格可能为空;请展开该行,不要将其视为缺失数据。阅读时可以暂停实时显示。

在这个真实示例中,sourcescheduledstatusokservicelabex-health-fixturecron-... 探针应与应用日志中的毫秒级 scheduledTime 相匹配。Dashboard 会使用其显示的时区渲染可见时间戳(此处为 GMT+8),而 Cron 计划使用 UTC。你的名称、调用 ID 和时间会有所不同。请结合调用结果一起读取这些字段;截图本身不能证明执行已完成。

Dashboard 中包含定时健康检查结果的真实 Cron 调用

Cron 更新最多可能需要 15 分钟才能传播。重复等待一分钟并检查日志,最多允许从成功部署开始经过 17 分钟。如果日志流为空或已停止,请检查 tail-errors.log。如果在规定时间内没有出现匹配事件,请停止并诊断配置、授权和触发器状态;不要报告成功。这是一次有时间限制的学习观察,不保证精确的执行延迟。Cron Triggers 文档 介绍了传播过程和基于 UTC 的计划。已保存的 Workers Logs 也可能需要一小段时间才会显示;等待日志写入后刷新查询。新 Worker 的独立 Past Cron Events 历史记录最多可能需要 30 分钟才显示事件。历史记录为空不代表执行失败;请在本实验规定的时间限制内使用上述实时观察方式。

观察到一次执行后,使用验证功能。它会检查已部署的计划,并在独立的实时流中等待最多 70 秒,以获取一次带有健康结果的真实定时事件。这覆盖了连接启动后完整的一分钟边界。如果没有事件,结果只能视为不确定;请检查连接和传播状态,并在同一观察时限内重试。评估程序不会伪造定时事件。只有在验证成功后,才停止学习者的 tail;使用实际的任务编号。

jobs
kill %1

完成下一步,及时结束这个一次性计划。不要让每分钟运行一次的学习任务无人看管地持续运行。

删除两个定时测试 Worker

在此步骤中,在仍获得授权的情况下,先删除公开 Worker 及其触发器,再删除内部健康服务测试装置。确认两个配置文件都包含本实验的准确名称和目标账号。

cat wrangler.jsonc
cat health/wrangler.jsonc
npx wrangler delete
npx wrangler delete -c health/wrangler.jsonc

在每个匹配名称的提示处按单个按键 y。不要影响无关项目、账号或其子域名。固定版本的 Wrangler 在删除 Worker 后可能输出已知的旧版 KV 清理身份验证诊断信息;该信息和失败的网络请求都不能证明删除失败。不要为了处理该诊断信息而扩大权限。

刷新 Dashboard 并使用验证功能。成功的已授权 Worker 清单应显示两个名称都已不存在。这证明已部署资源已被移除,但不表示所有调度器变更会立即完成全局传播。仅退出登录或关闭 VM 不会执行清理。

断开实验 VM

在此步骤中,断开这台 VM 前,确认 tail 任务已停止且资源已经删除。

jobs
npx wrangler logout
npx wrangler whoami --json

必须明确显示 loggedIn: false。结构化的未授权命令可能以非零状态退出;网络错误不等同于这个结果。使用验证功能,然后结束 VM。你的浏览器登录状态可以保留,以便之后进行其他独立实验。

总结

你使用 waitUntil 让有时间限制的健康检查在前台确认响应后继续完成,然后从定时处理程序中复用了同一操作。受控的本地测试将响应与延迟工作区分开来,而真实的云端事件则证明了部署后的定时执行。配置、手动调用和实时执行分别提供了不同类型的证据。

你检查了所有权和服务绑定,将合成探针与结构化日志进行关联,遵守了调用生命周期限制,并在断开连接前删除了这个一次性定时应用。可靠的长期投递需要采用不同于这种短时后台工作模式的架构。