Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
196 lines
8.9 KiB
Plaintext
196 lines
8.9 KiB
Plaintext
---
|
||
title: 后台作业
|
||
description: 通过将另一次逻辑函数运行入队,而不是在当前执行中完成所有工作,把耗时较长或受速率限制的工作交给 Twenty 工作进程处理。
|
||
icon: layer-group
|
||
---
|
||
|
||
一次逻辑函数运行会受到其 `timeoutSeconds`(最大 900 秒)的限制。 任何无法在该时间窗口内完成的工作——例如完整的重新同步、逐记录的扇出、对你进行速率限制的第三方 API——都必须拆分为更小的运行。
|
||
|
||
`enqueueJob` 正是用于此:它会请求 Twenty 工作进程稍后在其自己的进程中运行你应用中的某个逻辑函数,并使用其独立的超时预算。 调用方会立即返回。
|
||
|
||
```text
|
||
┌─────────────────┐ enqueueJob(...) ┌──────────────┐ ┌────────────────────┐
|
||
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
|
||
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
|
||
└─────────────────┘ └──────────────┘ └────────────────────┘
|
||
```
|
||
|
||
## 将一次运行加入队列
|
||
|
||
从 `twenty-sdk/logic-function` 导入 `enqueueJob`,并将其指向你想要运行的逻辑函数的 `universalIdentifier`。
|
||
|
||
```ts src/logic-functions/sync-all-contacts.ts
|
||
import { enqueueJob } from 'twenty-sdk/logic-function';
|
||
|
||
await enqueueJob({
|
||
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
|
||
payload: { page: 1 },
|
||
});
|
||
```
|
||
|
||
目标函数会像其他任何触发器一样,将 `payload` 作为其处理程序参数接收。 它必须属于与调用方**相同的应用程序**——将其他应用的函数入队会被拒绝,并返回 `Logic function not found`。
|
||
|
||
<Note>
|
||
`enqueueJob` 在作业被接受后立即返回,而不是在它运行完成后返回。 它不会返回目标的结果——如果你需要再次读取结果,让目标将其输出写入[key-value store](/l/zh/developers/extend/apps/logic/key-value-store)或工作区记录。
|
||
</Note>
|
||
|
||
## 作业选项
|
||
|
||
| 选项 | 默认 | 范围 | 作用 |
|
||
| ------------ | --- | -------------------- | ---------------------------------------------------- |
|
||
| `retryLimit` | `0` | `0`–`10` | 总体额外队列尝试次数。 应用程序请求的重试次数上限为 `3`。 仅对可以安全运行两次的处理程序提高该值。 |
|
||
| `delayMs` | `0` | `0`–`604800000`(7 天) | 在运行符合条件之前等待这么长时间。 |
|
||
|
||
```ts
|
||
await enqueueJob({
|
||
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
|
||
payload: { page: 1 },
|
||
retryLimit: 3,
|
||
delayMs: 60_000,
|
||
});
|
||
```
|
||
|
||
<Note>
|
||
**优先级尚不可配置。** 入队的作业始终以最低优先级运行,因此平台工作永远不会被应用程序作业延迟。 对优先级的控制即将推出。
|
||
</Note>
|
||
|
||
排队的运行会继承将其入队的函数的当前用户,因此会以相同的权限执行。
|
||
|
||
## 重试瞬时故障
|
||
|
||
Twenty 不会重试应用程序代码引发的每个异常。 普通的抛出错误会被视为永久性故障。 对于瞬时故障,已排队的逻辑函数可以通过抛出 `RetryableLogicFunctionError` 请求最多重试三次。
|
||
|
||
```ts
|
||
import {
|
||
type LogicFunctionExecutionContext,
|
||
RetryableLogicFunctionError,
|
||
} from 'twenty-sdk/logic-function';
|
||
|
||
export const handler = async (
|
||
_payload: unknown,
|
||
{ retryCount, maxRetries }: LogicFunctionExecutionContext,
|
||
) => {
|
||
const response = await fetch('https://api.example.com/contacts');
|
||
|
||
if (response.status === 429 || response.status >= 500) {
|
||
throw new RetryableLogicFunctionError(
|
||
`The contacts API is temporarily unavailable (${response.status}); retry ${retryCount} of ${maxRetries}`,
|
||
);
|
||
}
|
||
};
|
||
```
|
||
|
||
尽可能直接抛出 `RetryableLogicFunctionError`。 如果扩展它,请勿替换其 `name`:Twenty 会在各个执行运行时中识别序列化后的名称 `RetryableLogicFunctionError`。
|
||
|
||
`retryCount` 在初始执行时为 `0`,且仅在应用程序代码请求重试时增加。 `maxRetries` 最多为 `3`;当已排队作业的总体重试限制更小时,该值可能更低。 平台故障不会增加 `retryCount`,尽管它们仍会消耗队列的总体安全预算。
|
||
|
||
队列使用指数退避和抖动来延迟重试尝试。 有意不保证确切的延迟时间,因此应用程序代码不应依赖于在精确时间发生重试。 达到 `maxRetries` 后,另一个 `RetryableLogicFunctionError` 会被记录为最终的应用程序故障,且不会再次执行。
|
||
|
||
<Warning>
|
||
重试会重新运行整个处理程序,并且可能在某些副作用已成功后发生。 在请求重试之前,请确保处理程序具有幂等性。
|
||
</Warning>
|
||
|
||
## 用法:分页处理一次长时间同步
|
||
|
||
典型模式是函数使用下一个游标将*自身*入队。 每次运行都在自己的超时时间内完成一页工作,当没有剩余内容时链条就会停止。
|
||
|
||
```ts src/logic-functions/sync-contacts-page.ts
|
||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||
import { enqueueJob } from 'twenty-sdk/logic-function';
|
||
|
||
const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
|
||
|
||
const handler = async (params: { cursor?: string }) => {
|
||
const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
|
||
|
||
await importContacts(contacts);
|
||
|
||
if (nextCursor) {
|
||
await enqueueJob({
|
||
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
|
||
payload: { cursor: nextCursor },
|
||
delayMs: 2_000,
|
||
});
|
||
}
|
||
|
||
return { imported: contacts.length, done: !nextCursor };
|
||
};
|
||
|
||
export default defineLogicFunction({
|
||
universalIdentifier: SYNC_CONTACTS_PAGE,
|
||
name: 'sync-contacts-page',
|
||
timeoutSeconds: 120,
|
||
handler,
|
||
});
|
||
```
|
||
|
||
## 按记录扇出(fan-out)
|
||
|
||
当工作天然按条目划分时,为每个条目入队一个作业,并让工作进程并行处理它们,而不是在内联循环中处理。
|
||
|
||
```ts
|
||
const companies = await listCompaniesToEnrich();
|
||
|
||
await Promise.all(
|
||
companies.map((company) =>
|
||
enqueueJob({
|
||
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
|
||
payload: { companyId: company.id },
|
||
retryLimit: 2,
|
||
}),
|
||
),
|
||
);
|
||
```
|
||
|
||
## 长时间运行工作的最佳实践
|
||
|
||
两条规则涵盖几乎所有长作业:**用递归代替循环**,以及**每次运行处理一块有界的数据块(chunk)**。
|
||
|
||
试图一次完成所有事情的运行就是失败模式——它会触发超时,并在重试时从头开始重新执行整个过程。 相反,应调整单个数据块的大小,使其能够从容地在 `timeoutSeconds` 内完成,持久化当前位置,然后入队下一次运行。
|
||
|
||
```ts src/logic-functions/enrich-companies-batch.ts
|
||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||
import { enqueueJob, kv } from 'twenty-sdk/logic-function';
|
||
|
||
const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
|
||
const CHUNK_SIZE = 50;
|
||
|
||
const handler = async (params: { offset?: number }) => {
|
||
const offset = params.offset ?? 0;
|
||
const companies = await listCompaniesToEnrich({
|
||
offset,
|
||
limit: CHUNK_SIZE,
|
||
});
|
||
|
||
for (const company of companies) {
|
||
await enrichCompany(company);
|
||
}
|
||
|
||
await kv.set('enrich:progress', { offset: offset + companies.length });
|
||
|
||
if (companies.length === CHUNK_SIZE) {
|
||
await enqueueJob({
|
||
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
|
||
payload: { offset: offset + CHUNK_SIZE },
|
||
});
|
||
}
|
||
|
||
return { processed: companies.length, done: companies.length < CHUNK_SIZE };
|
||
};
|
||
|
||
export default defineLogicFunction({
|
||
universalIdentifier: ENRICH_COMPANIES_BATCH,
|
||
name: 'enrich-companies-batch',
|
||
timeoutSeconds: 300,
|
||
handler,
|
||
});
|
||
```
|
||
|
||
支撑这一模式的要点:
|
||
|
||
* **以最慢的条目而不是平均值来确定块大小。** `CHUNK_SIZE × worst-case item time` 必须在留有余量的情况下适配到 `timeoutSeconds` 中,否则在运行被切断时,一个数据块的尾部会丢失。
|
||
* **让终止条件显式化。** 仅在返回的是完整数据块时继续递归。 仅在“无结果”时停止的链,如果源在中途返回一个较短的分页,将会永远继续下去。
|
||
* **在入队下一次运行之前持久化进度,** 这样失败的链路会从上一个完成的数据块而不是从头开始重新启动。
|
||
* **保持每个数据块幂等。** 在重试后重新处理一个数据块时,不能产生重复写入——应基于你处理的记录或外部 id 进行关键写入。
|
||
* **在工作会访问受限频率的第三方服务时,优先使用分块链而不是一次巨大的 fan-out。** 使用 `delayMs` 的链会自我节奏控制,而一次性入队的数千个作业会立即全部变为可运行状态。
|