550aeafd90
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
166 lines
8.1 KiB
Plaintext
166 lines
8.1 KiB
Plaintext
---
|
||
title: Hintergrundjobs
|
||
description: Übergib lang laufende oder rate-limitierte Arbeiten an die Twenty-Worker, indem du einen weiteren Lauf einer Logikfunktion in die Warteschlange stellst, statt alles inline auszuführen.
|
||
icon: layer-group
|
||
---
|
||
|
||
Ein Logikfunktionslauf ist durch seine `timeoutSeconds` begrenzt (maximal 900 Sekunden). Alles, was in diesem Zeitfenster nicht abgeschlossen werden kann – ein vollständiger Re-Sync, ein Fan-out pro Datensatz, eine Drittanbieter-API, die dich rate-limitiert – muss in kleinere Läufe aufgeteilt werden.
|
||
|
||
`enqueueJob` macht genau das: Es weist die Twenty-Worker an, eine der Logikfunktionen deiner App später, in einem eigenen Prozess, mit eigenem Timeout-Budget auszuführen. Der Aufrufer kehrt sofort zurück.
|
||
|
||
```text
|
||
┌─────────────────┐ enqueueJob(...) ┌──────────────┐ ┌────────────────────┐
|
||
│ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
|
||
│ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
|
||
└─────────────────┘ └──────────────┘ └────────────────────┘
|
||
```
|
||
|
||
## Einen Lauf in die Warteschlange stellen
|
||
|
||
Importiere `enqueueJob` aus `twenty-sdk/logic-function` und gib den `universalIdentifier` der Logikfunktion an, die du ausführen möchtest.
|
||
|
||
```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 },
|
||
});
|
||
```
|
||
|
||
Die Zielfunktion erhält `payload` als Handler-Argument, genau wie jeder andere Trigger. Sie muss zur **gleichen Applikation** gehören wie der Aufrufer — das Einreihen der Funktion einer anderen App wird mit `Logic function not found` abgelehnt.
|
||
|
||
<Note>
|
||
`enqueueJob` kehrt zurück, sobald der Job akzeptiert wurde, nicht wenn er ausgeführt wurde. Sie gibt nicht das Ergebnis des Ziels zurück — lasse das Ziel das Ergebnis in den [Key-Value-Speicher](/l/de/developers/extend/apps/logic/key-value-store) oder in einen Workspace-Datensatz schreiben, wenn du es wieder auslesen musst.
|
||
</Note>
|
||
|
||
## Job-Optionen
|
||
|
||
| Option | Standard | Bereich | Was es tut |
|
||
| ------------ | -------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `retryLimit` | `0` | `0`–`10` | Zusätzliche Versuche, falls der Lauf eine Exception auslöst. Erhöhe diesen Wert nur für Handler, die sicher zweimal ausgeführt werden können. |
|
||
| `delayMs` | `0` | `0`–`604800000` (7 Tage) | So lange warten, bevor der Lauf zur Ausführung ansteht. |
|
||
|
||
```ts
|
||
await enqueueJob({
|
||
logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
|
||
payload: { page: 1 },
|
||
retryLimit: 3,
|
||
delayMs: 60_000,
|
||
});
|
||
```
|
||
|
||
<Note>
|
||
**Priorität ist noch nicht konfigurierbar.** Eingereihte Jobs laufen immer mit der niedrigsten Priorität, sodass Plattformarbeit niemals hinter Applikationsjobs verzögert wird. Steuerung der Priorität kommt bald.
|
||
</Note>
|
||
|
||
Der eingereihte Lauf erbt den agierenden Benutzer der Funktion, die ihn eingereiht hat, sodass er mit denselben Berechtigungen agiert.
|
||
|
||
## Verwendung: eine lange Synchronisation seitenweise verarbeiten
|
||
|
||
Die klassische Form ist eine Funktion, die *sich selbst* mit dem nächsten Cursor in die Warteschlange stellt. Jeder Lauf erledigt eine Seite Arbeit deutlich innerhalb seines eigenen Timeouts, und die Kette endet, wenn nichts mehr übrig ist.
|
||
|
||
```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,
|
||
});
|
||
```
|
||
|
||
## Aufteilung pro Datensatz
|
||
|
||
Wenn die Arbeit natürlich pro Element anfällt, reihe einen Job pro Element ein und lasse die Worker sie parallel verarbeiten, anstatt inline zu iterieren.
|
||
|
||
```ts
|
||
const companies = await listCompaniesToEnrich();
|
||
|
||
await Promise.all(
|
||
companies.map((company) =>
|
||
enqueueJob({
|
||
logicFunctionUniversalIdentifier: ENRICH_COMPANY,
|
||
payload: { companyId: company.id },
|
||
retryLimit: 2,
|
||
}),
|
||
),
|
||
);
|
||
```
|
||
|
||
## Bewährte Vorgehensweisen für langlaufende Arbeit
|
||
|
||
Zwei Regeln decken fast jeden langen Job ab: **rekursiv statt in einer Schleife arbeiten** und **pro Lauf einen begrenzten Chunk verarbeiten**.
|
||
|
||
Ein Lauf, der versucht, alles zu erledigen, ist der Fehlerfall — er erreicht das Timeout, und bei einem Retry startet er den gesamten Vorgang erneut von vorn. Bestimme stattdessen die Größe eines Chunks so, dass er bequem innerhalb von `timeoutSeconds` fertig wird, speichere deine Position und reihe den nächsten Lauf ein.
|
||
|
||
```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,
|
||
});
|
||
```
|
||
|
||
Warum das funktioniert:
|
||
|
||
* **Lege die Chunk-Größe anhand des langsamsten Elements fest, nicht anhand des Durchschnitts.** `CHUNK_SIZE × Worst-Case-Elementzeit` muss mit ausreichendem Puffer in `timeoutSeconds` passen, sonst geht das Ende eines Chunks verloren, wenn der Lauf abgeschnitten wird.
|
||
* **Mache die Abbruchbedingung explizit.** Rekursiere nur, solange ein voller Chunk zurückgekommen ist. Eine Kette, die nur bei „keine Ergebnisse“ anhält, läuft für immer weiter, wenn die Quelle unterwegs einmal eine verkürzte Seite zurückgibt.
|
||
* **Persistiere den Fortschritt, bevor du den nächsten Lauf einreihst,** damit ein fehlgeschlagener Link beim letzten abgeschlossenen Chunk und nicht am Anfang neu startet.
|
||
* **Halte jeden Chunk idempotent.** Die erneute Verarbeitung eines Chunks nach einem Retry darf nicht zu doppelten Schreibvorgängen führen — führe Schlüsselschreibvorgänge auf dem Datensatz oder der externen ID aus, die du verarbeitest.
|
||
* **Bevorzuge eine verkettete Chunk-Verarbeitung gegenüber einem riesigen Fan-out**, wenn die Arbeit auf einen rate-limitierten Drittanbieter trifft: Eine Kette mit `delayMs` taktet sich selbst, wohingegen Tausende auf einmal eingereihte Jobs sofort zur Ausführung anstehen.
|
||
|
||
<Warning>
|
||
Retries führen den gesamten Handler erneut aus. Halte eingereihte Handler idempotent, bevor du `retryLimit` über `0` setzt.
|
||
</Warning>
|