Skip to content

Commit e20a3a4

Browse files
committed
chore(trigger): tighten task limits, fix queues that never serialized, schedule missing self-hosted crons
1 parent 8fc4c1a commit e20a3a4

16 files changed

Lines changed: 90 additions & 13 deletions

‎apps/docs/content/docs/platform/self-hosting/background-jobs.mdx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ Point cron at an **internal** address where possible (the in-cluster Service, or
4848
| Outbox processing | `/api/webhooks/outbox/process` | `*/1 * * * *` | Transactional-outbox retries for billing, membership, enterprise issuance, and workflow-deployment side effects |
4949
| Workspace file search dispatch | `/api/cron/workspace-file-search-dispatch` | `*/1 * * * *` | Dispatches indexing work for workspace file search |
5050
| Knowledge projection | `/api/cron/knowledge-projection` | `*/1 * * * *` | Brings knowledge base search up to date with document, permission, and chunk changes |
51+
| Table row-change fold | `/api/cron/fold-table-row-changes` | `*/1 * * * *` | Folds the table row-change log into each table's stored row count and version |
5152
| Connector sync | `/api/knowledge/connectors/sync` | `*/5 * * * *` | Knowledge base connector syncs |
5253
| Connector member sync | `/api/knowledge/connectors/member-sync` | `*/5 * * * *` | Per-member content/access sync for indexed connectors; live Search sources are skipped |
5354
| Connector directory sync | `/api/knowledge/connectors/directory-sync` | `*/5 * * * *` | Refreshes stored groups for indexed administrator-mode connectors; live Search sources are skipped |
5455
| Workspace events poll | `/api/workspace-events/poll` | `*/15 * * * *` | Workspace event triggers |
5556
| Table row TTL cleanup | `/api/cron/cleanup-table-row-ttl` | `*/15 * * * *` | Deletes table rows whose TTL column has expired |
57+
| Stale execution cleanup | `/api/cron/cleanup-stale-executions` | `*/30 * * * *` | Fails runs whose worker died without finishing — workflow executions, background jobs, table jobs and table runs — so they stop showing as running |
5658
| OAuth token cleanup | `/api/cron/cleanup-oauth-tokens` | `0 * * * *` | Deletes access and refresh tokens after the retention tail |
5759
| SCIM reconciliation | `/api/cron/scim-reconcile` | `17 * * * *` | Re-applies directory group mappings |
5860
| Data drains | `/api/cron/run-data-drains` | `0 * * * *` | Enterprise data drains |
@@ -61,6 +63,7 @@ Point cron at an **internal** address where possible (the in-cluster Service, or
6163
| Reconcile billing seats | `/api/cron/reconcile-billing-seats` | `0 * * * *` | Billing only — safe to disable when self-hosted |
6264
| Reconcile inbox entitlement | `/api/cron/reconcile-inbox-entitlement` | `0 3 * * *` | Inbox access reconciliation |
6365
| Cleanup sandbox images | `/api/cron/cleanup-sandbox-images` | `30 4 * * *` | Reclaims sandbox images |
66+
| Idempotency key cleanup | `/api/webhooks/cleanup/idempotency` | `0 5 * * *` | Deletes deduplication records older than seven days |
6467

6568
<Callout type="info">
6669
**Subscription renewal** covers Microsoft Teams chat triggers, whose Microsoft Graph subscriptions are hard-capped at about three days. Without it, Teams triggers work for a couple of days and then quietly stop. Gmail, Outlook, Drive, Calendar, and Sheets triggers are **polled** instead — they depend on the per-minute poll jobs above, not on this one.

‎apps/sim/background/cleanup-dispatch.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const CLEANUP_DISPATCH_MAX_ATTEMPTS = 3
1515
*/
1616
export const cleanupDispatchTask = task({
1717
id: 'cleanup-dispatch',
18+
maxDuration: 900,
1819
queue: { concurrencyLimit: 1 },
1920
retry: { maxAttempts: CLEANUP_DISPATCH_MAX_ATTEMPTS },
2021
run: ({ jobType }: CleanupDispatchPayload) => dispatchCleanupJobs(jobType),

‎apps/sim/background/cleanup-file-versions.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,13 @@ export async function runCleanupFileVersions(payload: CleanupJobPayload): Promis
188188

189189
export const cleanupFileVersionsTask = task({
190190
id: 'cleanup-file-versions',
191-
machine: 'large-1x',
191+
/**
192+
* Sized from production telemetry: peak RSS 219 MB across ~15,600 runs, so
193+
* `small-2x` (1 GB) holds over four times the worst case. Each pass works in
194+
* bounded batches of version rows, so the peak does not grow with backlog.
195+
*/
196+
machine: 'small-2x',
197+
maxDuration: 600,
192198
queue: retentionCleanupQueue,
193199
retry: { maxAttempts: 1 },
194200
run: runCleanupFileVersions,

‎apps/sim/background/cleanup-stale-executions.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,19 @@ const GENERIC_STALE_PROCESSING_ERROR = `Job terminated: stuck in processing for
4949
const EXECUTION_DEADLINE_ERROR = getTimeoutErrorMessage()
5050
/**
5151
* Table jobs run as detached workers with progress heartbeats, independently of workflow timeout
52-
* policy. Preserve their historical 90-minute task window plus five-minute cleanup grace.
52+
* policy. A running job is treated as dead once its last progress write (`updatedAt`) is older
53+
* than this. It measures the gap since the last heartbeat, not run length, so it does not track
54+
* any task's `maxDuration`; it only has to exceed the longest stretch a live worker goes without
55+
* writing progress.
5356
*/
5457
const TABLE_JOB_STALE_THRESHOLD_MINUTES = 95
5558
/** Terminal table-jobs older than this are pruned; only the latest job per table is ever read. */
5659
const TABLE_JOB_RETENTION_HOURS = 24
5760
/**
5861
* A table run dispatch whose holder has not made progress for this long is
59-
* treated as dead. Same shape and window as the table-job threshold above: the
60-
* 90-minute Trigger.dev task ceiling (`maxDuration` in `trigger.config.ts`) plus
61-
* five minutes of cleanup grace, measured from the dispatcher's own per-window
62-
* heartbeat rather than from when the run was requested.
62+
* treated as dead. Same shape and window as the table-job threshold above,
63+
* measured from the dispatcher's own per-window heartbeat rather than from when
64+
* the run was requested, so it is independent of the task's `maxDuration`.
6365
*/
6466
const TABLE_DISPATCH_STALE_THRESHOLD_MINUTES = 95
6567
/** Per-run ceiling on reaped dispatches, so one tick cannot fan out unbounded SSE. */
@@ -795,6 +797,7 @@ export async function runCleanupStaleExecutions() {
795797

796798
export const cleanupStaleExecutionsTask = task({
797799
id: 'cleanup-stale-executions',
800+
maxDuration: 1800,
798801
queue: { concurrencyLimit: 1 },
799802
run: () => runCleanupStaleExecutions(),
800803
})

‎apps/sim/background/cleanup-table-row-ttl.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ export async function runCleanupTableRowTtl(
335335

336336
export const cleanupTableRowTtlTask = task({
337337
id: 'cleanup-table-row-ttl',
338+
maxDuration: 600,
338339
queue: { concurrencyLimit: 1 },
339340
run: () => runCleanupTableRowTtl(),
340341
})

‎apps/sim/background/fork-content-copy.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import {
2020
export const forkContentCopyTask = task({
2121
id: 'fork-content-copy',
2222
machine: 'large-2x',
23+
/**
24+
* Inside `STALE_ACTIVE_MS` (30 minutes, in the background-work store), after
25+
* which the outbox cron marks the fork's status row failed. A run outliving
26+
* that would keep copying under a status that already says it failed; the
27+
* remaining five minutes absorb queue wait, since the row starts its clock at
28+
* fork time.
29+
*/
30+
maxDuration: 1500,
2331
retry: { maxAttempts: 1 },
2432
queue: {
2533
name: 'fork-content-copy',

‎apps/sim/background/knowledge-processing.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,16 @@ export async function runDocumentProcessing(
306306
*/
307307
export const interactiveProcessingQueue = queue({
308308
name: INTERACTIVE_PROCESSING_QUEUE_NAME,
309-
concurrencyLimit: envNumber(env.KB_CONFIG_CONCURRENCY_LIMIT, 20),
309+
concurrencyLimit: envNumber(env.KB_CONFIG_CONCURRENCY_LIMIT, 20, { min: 1, integer: true }),
310310
})
311311

312312
/** Referenced by no dispatch site: named per trigger, declared here so the deploy registers it. */
313313
export const backfillProcessingQueue = queue({
314314
name: BACKFILL_PROCESSING_QUEUE_NAME,
315-
concurrencyLimit: envNumber(env.KB_CONFIG_BACKFILL_CONCURRENCY_LIMIT, 20),
315+
concurrencyLimit: envNumber(env.KB_CONFIG_BACKFILL_CONCURRENCY_LIMIT, 20, {
316+
min: 1,
317+
integer: true,
318+
}),
316319
})
317320

318321
export const processDocument = task({

‎apps/sim/background/lifecycle-email.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ async function sendLifecycleEmail({ userId, type }: LifecycleEmailParams): Promi
5959

6060
export const lifecycleEmailTask = task({
6161
id: LIFECYCLE_EMAIL_TASK_ID,
62+
maxDuration: 120,
63+
queue: { concurrencyLimit: 10 },
6264
retry: { maxAttempts: 2 },
6365
run: async (params: LifecycleEmailParams) => {
6466
await sendLifecycleEmail(params)

‎apps/sim/background/mothership-inbox-execution.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface MothershipInboxExecutionParams {
1111
export const mothershipInboxExecution = task({
1212
id: 'mothership-inbox-execution',
1313
machine: { preset: 'medium-1x' },
14+
queue: { concurrencyLimit: 10 },
1415
retry: {
1516
maxAttempts: 2,
1617
minTimeoutInMs: 5000,

‎apps/sim/background/quickbooks-webhook-ingress.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export async function enqueueQuickBooksWebhookIngress(
143143
export const quickBooksWebhookIngressTask = task({
144144
id: 'quickbooks-webhook-ingress',
145145
machine: 'small-1x',
146+
maxDuration: 300,
146147
retry: {
147148
maxAttempts: QUICKBOOKS_WEBHOOK_INGRESS_MAX_ATTEMPTS,
148149
factor: 2,

0 commit comments

Comments
 (0)