Skip to content

Commit 3766582

Browse files
authored
fix(webhooks): run inactive deployment-version cleanup inline on deploy (#5250)
When a deploy activates a new version, superseded versions' webhooks are removed by a separate, best-effort CLEANUP_INACTIVE outbox event. When that event is lost/dead-letters, old-version webhooks linger as is_active orphans that fetchActiveWebhooks skips (version mismatch), so they silently stop polling (~515 webhooks across ~130 workflows in prod). Run the existing cleanupInactiveDeploymentVersions synchronously in the SYNC_ACTIVE handler, right after the active version's webhooks/schedules are registered, falling back to the deferred outbox event only if the inline pass throws. This reuses the existing guarded cleanup, which re-checks each version is still inactive before tearing anything down (so it never touches the active version) and runs strictly after registration (so a teardown failure can't block it).
1 parent 3e03f8c commit 3766582

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

apps/sim/lib/workflows/deployment-outbox.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,10 @@ const syncActiveSideEffects = async (rawPayload: unknown): Promise<void> => {
207207
return
208208
}
209209

210-
await enqueueWorkflowInactiveDeploymentCleanup(db, {
210+
await syncInactiveDeploymentCleanup({
211211
workflowId: payload.workflowId,
212212
activeDeploymentVersionId: payload.deploymentVersionId,
213+
workflow: workflowData,
213214
userId: payload.userId,
214215
requestId,
215216
})
@@ -278,6 +279,40 @@ const cleanupUndeployedSideEffects = async (rawPayload: unknown): Promise<void>
278279
await removeMcpToolsIfStillUndeployed(payload.workflowId, requestId)
279280
}
280281

282+
/**
283+
* Run inactive-version cleanup synchronously as part of the active-version sync, right
284+
* after the active version's webhooks/schedules are registered.
285+
*
286+
* {@link cleanupInactiveDeploymentVersions} re-checks that each version is still inactive
287+
* before tearing anything down, so it can never touch the now-active version. Running it
288+
* inline — rather than only enqueueing it — closes the window where a lost
289+
* `CLEANUP_INACTIVE` outbox event leaves superseded webhooks behind as live-but-never-polled
290+
* `is_active` orphans. The deferred event is kept as a fallback so cleanup still retries if
291+
* the inline pass throws, without failing the already-succeeded registration.
292+
*/
293+
async function syncInactiveDeploymentCleanup(params: {
294+
workflowId: string
295+
activeDeploymentVersionId: string
296+
workflow: Record<string, unknown>
297+
userId: string
298+
requestId: string
299+
}): Promise<void> {
300+
try {
301+
await cleanupInactiveDeploymentVersions(params)
302+
} catch (cleanupError) {
303+
logger.warn(
304+
`[${params.requestId}] Inline inactive-deployment cleanup failed; deferring to outbox retry`,
305+
cleanupError
306+
)
307+
await enqueueWorkflowInactiveDeploymentCleanup(db, {
308+
workflowId: params.workflowId,
309+
activeDeploymentVersionId: params.activeDeploymentVersionId,
310+
userId: params.userId,
311+
requestId: params.requestId,
312+
})
313+
}
314+
}
315+
281316
async function cleanupInactiveDeploymentVersions(params: {
282317
workflowId: string
283318
activeDeploymentVersionId: string

0 commit comments

Comments
 (0)