Skip to content

Commit ab6b400

Browse files
authored
chore(trigger): fix queues that never serialized and schedule missing self-hosted crons (#8715)
* chore(trigger): tighten task limits, fix queues that never serialized, schedule missing self-hosted crons * refactor(trigger): derive the fork ceiling from STALE_ACTIVE_MS and trim sizing comments * chore(trigger): drop caps whose work grows with user data or that lose cleanup on kill * chore(trigger): keep unbounded-fan-out tasks on the global ceiling; bump chart for new cronjobs
1 parent ccce7f7 commit ab6b400

11 files changed

Lines changed: 62 additions & 11 deletions

File tree

‎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-stale-executions.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,16 @@ 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, so it must exceed the longest gap between a live worker's heartbeats.
5354
*/
5455
const TABLE_JOB_STALE_THRESHOLD_MINUTES = 95
5556
/** Terminal table-jobs older than this are pruned; only the latest job per table is ever read. */
5657
const TABLE_JOB_RETENTION_HOURS = 24
5758
/**
5859
* 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.
60+
* treated as dead. Same rule as the table-job threshold above, measured from the
61+
* dispatcher's own per-window heartbeat rather than from when the run was requested.
6362
*/
6463
const TABLE_DISPATCH_STALE_THRESHOLD_MINUTES = 95
6564
/** Per-run ceiling on reaped dispatches, so one tick cannot fan out unbounded SSE. */

‎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/lifecycle-email.ts‎

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

6060
export const lifecycleEmailTask = task({
6161
id: LIFECYCLE_EMAIL_TASK_ID,
62+
queue: { concurrencyLimit: 10 },
6263
retry: { maxAttempts: 2 },
6364
run: async (params: LifecycleEmailParams) => {
6465
await sendLifecycleEmail(params)

‎apps/sim/background/run-data-drain.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ interface RunDataDrainPayload {
99

1010
export const runDataDrainTask = task({
1111
id: 'run-data-drain',
12+
/**
13+
* The drain cursor commits only after the last chunk is delivered, so a run
14+
* cut off by its ceiling restarts from the old cursor and re-exports the same
15+
* rows. A shorter cap would loop a large drain forever.
16+
*/
17+
maxDuration: 5400,
18+
/**
19+
* Enqueue sites key runs by `data-drain:<id>`; this limit is what makes that
20+
* key serialize one drain's runs instead of granting each key the
21+
* environment's full concurrency.
22+
*/
23+
queue: { concurrencyLimit: 1 },
1224
run: async ({ drainId, trigger }: RunDataDrainPayload, { signal }) =>
1325
runDrain(drainId, trigger, { signal }),
1426
})

‎apps/sim/background/table-run-dispatcher.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ export const tableRunDispatcherTask = task({
2929
* 0.03 for p90, so the larger preset is bought for its RAM.
3030
*/
3131
machine: 'small-2x',
32+
/**
33+
* The trigger site keys each run by its `dispatchId`, so the limit applies
34+
* per dispatch: 1 serializes a duplicate run of the same dispatch without
35+
* throttling distinct dispatches against each other.
36+
*/
3237
queue: {
3338
name: 'table-run-dispatcher',
34-
concurrencyLimit: 8,
39+
concurrencyLimit: 1,
3540
},
3641
run: async (payload: TableRunDispatcherPayload) => {
3742
const { dispatchId, concurrency } = payload

‎apps/sim/lib/table/dispatcher.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ const STALE_DISPATCH_EVENT_CONCURRENCY = 10
5252
* How long past its stale threshold a dispatch may be spared by cell activity
5353
* before it is reclaimed anyway. Bounds the one masking case the probe cannot
5454
* resolve — two table-wide dispatches sharing a group — which continuous
55-
* activity would otherwise hide forever. Far beyond any single window: the
56-
* Trigger.dev run ceiling is ninety minutes, and a live dispatch heartbeats
57-
* between windows no matter what its cells are doing.
55+
* activity would otherwise hide forever. Far beyond any single window: a live
56+
* dispatch heartbeats between windows no matter what its cells are doing.
5857
*/
5958
const DISPATCH_ABSOLUTE_STALE_MS = 24 * 60 * 60 * 1000
6059

‎apps/sim/trigger.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const grafanaTelemetry = grafanaFullyConfigured
8787
export default defineConfig({
8888
project: env.TRIGGER_PROJECT_ID!,
8989
runtime: 'node-24',
90-
logLevel: 'log',
90+
logLevel: 'info',
9191
maxDuration: 5400,
9292
retries: {
9393
enabledInDev: false,

‎docker/crontab‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ SHELL=/bin/sh
5656
# Deletes table rows whose TTL column has expired
5757
*/15 * * * * curl -fsS -m 60 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/cleanup-table-row-ttl"
5858

59+
# Fails runs whose worker died without a terminal state and prunes their history
60+
*/30 * * * * curl -fsS -m 60 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/cleanup-stale-executions"
61+
5962
# Deletes OAuth token rows after the retention tail
6063
0 * * * * curl -fsS -m 120 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/cleanup-oauth-tokens"
6164

@@ -68,6 +71,9 @@ SHELL=/bin/sh
6871
# Reclaims sandbox images
6972
30 4 * * * curl -fsS -m 300 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/cleanup-sandbox-images"
7073

74+
# Deletes idempotency records past the seven-day dedupe window
75+
0 5 * * * curl -fsS -m 300 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/webhooks/cleanup/idempotency"
76+
7177
# Inbox entitlement reconciliation
7278
0 3 * * * curl -fsS -m 120 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/reconcile-inbox-entitlement"
7379

‎helm/sim/Chart.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: sim
33
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
44
type: application
5-
version: 1.11.6
5+
version: 1.11.7
66
appVersion: "v0.8.26"
77
kubeVersion: ">=1.25.0-0"
88
home: https://sim.ai

0 commit comments

Comments
 (0)