From d7617a7599aa786f229951abc02fef9ab0ee8ee9 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 28 Aug 2026 15:06:43 +0200 Subject: [PATCH] fix: put the remedy in the warning and match it to how the schedule ticks --- backend/windmill-api-schedule/src/lib.rs | 7 +++- .../schedules/ScheduleEditorInner.svelte | 40 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/backend/windmill-api-schedule/src/lib.rs b/backend/windmill-api-schedule/src/lib.rs index ee036338a5..8b51646ebe 100644 --- a/backend/windmill-api-schedule/src/lib.rs +++ b/backend/windmill-api-schedule/src/lib.rs @@ -1223,8 +1223,11 @@ async fn fetch_interval_drift( if !schedule.enabled { return Ok(None); } - // Same index, same budget and same `edited_at` bound, for the same reasons as - // the sample in `list_schedule_with_jobs`. + // Query plan: `(workspace_id, runnable_path, created_at DESC)` index, hence the + // `parent_job IS NULL` clause, with the walk capped by DRIFT_SCAN_BUDGET. The + // `edited_at` bound is not that cap: it drops the runs of whatever schedule last + // held this path, which carry the same `trigger`. Nothing writes `edited_at` after + // the insert, so no run of this schedule can predate it. let push_times = sqlx::query_scalar!( "SELECT created_at FROM ( SELECT created_at, trigger, trigger_kind diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index ec103e8862..339519b68c 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -133,6 +133,10 @@ let no_flow_overlap = $state(false) // Measured on the deployed schedule, so it is read back rather than edited. let intervalDrift: ScheduleIntervalDrift | undefined = $state(undefined) + // A flow queues its next run when the previous one starts, and so does a script + // carrying a skip handler, which is pushed as a single step flow. A retry policy + // does not: it is materialized natively onto a plain script job. + let queuesNextRunAtStart = $derived(is_flow || dynamicSkipPath != undefined) let tag: string | undefined = $state(undefined) let validCRON = $state(true) let isValid = $state(true) @@ -957,6 +961,15 @@ intervalDrift.effective_s * 1000 )} instead of every {msToReadableTime(intervalDrift.configured_s * 1000)}: each of the last runs was queued too late for the slot that would have kept the cadence. + {#if queuesNextRunAtStart} + Its next run is already queued when the previous one starts, so the runs are + starting late rather than overrunning: look at worker capacity or a concurrency + limit. + {:else} + Script runs never overlap, so a run that outlasts its interval pushes the next one + out. To keep the cadence, schedule a flow instead: a flow queues its next run when + the previous one starts. + {/if} {/if}
@@ -1022,27 +1035,12 @@ class="mt-2" /> {/if} - {#if itemKind == 'script'} - - {@const runsAsFlow = dynamicSkipPath != undefined} -
- {#if !runsAsFlow} - - {/if} -

- {#if runsAsFlow} - A skip handler makes this schedule run as a single step flow: the next run is - queued when the previous one starts, so runs can overlap. - {:else} - Script runs never overlap: the next run is queued once the previous one has - completed, so a run that outlasts its interval pushes the next one to a later - slot. To keep the configured cadence, schedule a flow instead: a flow starts on - time, and its "no overlap of flows" setting skips a slot while the previous run is - still going. - {/if} -

+ {#if itemKind == 'script' && !queuesNextRunAtStart} +
+ Script runs never overlap: the next run is queued once the previous one has + completed.
{/if} {/if}