Files
upstream-mirror e7da97194a
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
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

198 lines
11 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 백그라운드 작업
description: 긴 시간이 걸리거나 속도 제한이 걸린 작업은 모두 인라인으로 처리하지 말고, 또 다른 로직 함수 실행을 큐에 넣어 Twenty 워커에 위임하세요.
icon: layer-group
---
로직 함수 실행은 `timeoutSeconds`(최대 900초)로 제한됩니다. 해당 시간 안에 끝나지 않는 모든 작업(전체 재동기화, 레코드별 팬아웃, 속도 제한이 걸린 서드파티 API 등)은 더 작은 실행 단위로 나눠야 합니다.
`enqueueJobs`는 바로 그 일을 합니다. Twenty 워커에게 나중에 앱의 로직 함수 중 하나를 실행해 달라고 요청하며, 각 페이로드당 한 번씩 실행되고 각 실행은 자체 프로세스에서 자체 타임아웃 한도로 처리됩니다. 호출은 즉시 반환됩니다.
```text
┌─────────────────┐ enqueueJobs(...) ┌──────────────┐ ┌────────────────────┐
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
└─────────────────┘ └──────────────┘ └────────────────────┘
```
## 실행 대기열에 추가하기
`twenty-sdk/logic-function`에서 `enqueueJobs`를 가져와 실행할 로직 함수의 `universalIdentifier`를 지정하고, 대기열에 추가할 각 실행마다 하나의 페이로드를 전달하세요.
```ts src/logic-functions/sync-all-contacts.ts
import { enqueueJobs } from 'twenty-sdk/logic-function';
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
});
```
각 실행은 다른 모든 트리거와 정확히 동일하게 핸들러 인수로 해당 페이로드를 받습니다. 대상은 호출자와 **동일한 애플리케이션**에 속해야 합니다. 다른 앱의 함수를 대기열에 추가하려고 하면 `Logic function not found` 오류와 함께 거부되며 아무것도 대기열에 추가되지 않습니다. 한 번의 호출로 최대 `200`개의 페이로드를 허용합니다.
<Note>
`enqueueJobs`는 작업이 실행되었을 때가 아니라 작업이 수락되는 즉시 반환됩니다. 대상들의 결과를 반환하지는 않습니다. 결과를 다시 읽어와야 한다면, 각 대상이 생성한 내용을 [키-값 스토어](/l/ko/developers/extend/apps/logic/key-value-store) 또는 워크스페이스 레코드에 기록하도록 하세요.
</Note>
<Note>
호출당 하나의 작업을 대기열에 추가하는 이전 `enqueueJob` 도우미는 더 이상 사용되지 않습니다. 대신 요소가 하나인 `payloads` 목록과 함께 `enqueueJobs`를 사용하세요.
</Note>
## 작업 옵션
옵션은 배치의 모든 실행에 적용됩니다.
| 옵션 | 기본값 | 범위 | 하는 일 |
| ------------ | --- | ------------------------ | ------------------------------------------------------------------------------- |
| `retryLimit` | `0` | `0`–`10` | 전체 추가 큐 시도 횟수. 애플리케이션에서 요청한 재시도는 `3`회로 제한됩니다. 두 번 실행해도 안전한 핸들러에 대해서만 이 값을 올리세요. |
| `delayMs` | `0` | `0`–`604800000` (7 days) | 실행이 가능해지기 전까지 이만큼 대기합니다. |
```ts
await enqueueJobs({
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
payloads: [{ page: 1 }],
retryLimit: 3,
delayMs: 60_000,
});
```
<Note>
**우선순위는 아직 설정할 수 없습니다.** 대기열에 추가된 작업은 항상 가장 낮은 우선순위로 실행되므로, 플랫폼 작업이 애플리케이션 작업보다 뒤로 밀려 지연되는 일은 없습니다. 우선순위를 제어하는 기능은 곧 제공될 예정입니다.
</Note>
대기열에 추가된 실행은 이를 대기열에 추가한 함수의 실행 사용자를 상속하므로, 동일한 권한으로 동작합니다.
## 일시적 실패 재시도하기
Twenty는 애플리케이션 코드의 모든 예외를 재시도하지 않습니다. 일반 오류가 throw되면 영구적 실패로 처리됩니다. 일시적 실패의 경우, 큐에 대기 중인 로직 함수는 `RetryableLogicFunctionError`를 throw하여 최대 세 번의 재시도를 요청할 수 있습니다.
```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`를 직접 throw하세요. 이를 확장하는 경우 `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 { enqueueJobs } 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 enqueueJobs({
logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
payloads: [{ 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,
});
```
## 레코드별로 팬아웃하기
작업이 본질적으로 항목 단위일 때는, 단일 호출에서 항목마다 하나의 작업을 대기열에 추가하고 워커들이 인라인 루프를 도는 대신 병렬로 처리하도록 하세요.
```ts
const companies = await listCompaniesToEnrich();
await enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
payloads: companies.map((company) => ({ companyId: company.id })),
retryLimit: 2,
});
```
## 장시간 실행 작업을 위한 모범 사례
대부분의 장시간 작업은 두 가지 규칙으로 다룰 수 있습니다: **루프 대신 재귀를 사용**하고, **실행마다 한정된 크기의 청크를 처리**하세요.
한 번에 모든 것을 처리하려는 실행은 실패 패턴입니다. 타임아웃에 걸리고, 재시도가 발생하면 전체 작업을 다시 처음부터 시작합니다. 대신, 하나의 청크가 `timeoutSeconds` 안에 여유 있게 끝나도록 크기를 잡고, 현재 위치를 저장한 뒤 다음 실행을 대기열에 추가하세요.
```ts src/logic-functions/enrich-companies-batch.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { enqueueJobs, 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 enqueueJobs({
logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
payloads: [{ 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 × 최악의 항목 처리 시간`은 여유를 두고 `timeoutSeconds` 안에 들어와야 합니다. 그렇지 않으면 실행이 중단될 때 청크의 끝부분이 유실됩니다.
* **종료 조건을 명시적으로 만드세요.** 가득 찬 청크가 반환되었을 때만 재귀적으로 호출하세요. "결과 없음"만을 기준으로 멈추는 체인은, 소스가 중간에 짧은 페이지를 한 번이라도 반환하면 영원히 계속될 수 있습니다.
* **다음 실행을 대기열에 추가하기 전에 진행 상태를 저장**해서, 실패한 링크가 처음부터가 아니라 마지막으로 완료된 청크부터 다시 시작하도록 하세요.
* **각 청크는 멱등성을 유지하세요.** 재시도 후에 하나의 청크를 다시 처리하더라도 중복 기록이 발생하지 않도록, 처리 중인 레코드나 외부 ID를 기준으로 쓰기를 수행해야 합니다.
* **거대한 한 번의 팬아웃보다 청크 단위 체인을 우선적으로 사용**하세요. 작업이 호출 한도(rate limit)가 있는 서드파티에 도달하는 경우, `delayMs`가 있는 체인은 스스로 속도를 조절하지만, 수천 개의 작업을 한 번에 대기열에 추가하면 모두 즉시 실행 가능 상태가 됩니다.