mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 16:03:29 +00:00
* feat: opt-in move of perpetual runs to a newly deployed script version Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep the perpetual-run opt-in across relocks and check the new version's tag Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: pin the tag check on a perpetual version switch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: count perpetual runs past the first queue page in the deploy prompt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: restart perpetual runs on the version a deploy makes runnable Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: claim a perpetual run and queue its replacement in one transaction Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: honour a cancel that lands after the worker last read its queue row Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: leave the lost-cancel fix to its own PR and match the scale down to 0 wording Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: run a preprocessor the deployed version adds over the arguments carried over Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: shorten the wording of the modal's argument warning Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: never preprocess a restarted perpetual run, as every other restart does Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: preprocess for a replacement whose run had not been through one Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: push a perpetual replacement without the deployed debounce settings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: name the runs the deploy button restarts Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf: skip the perpetual restart lookups on a deploy that is not perpetual Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: move perpetual runs before anything that can fail after the deploy commits Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: report cancellation on the queue listing so the deploy prompt can skip it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: pass the tag workspace to the availability check after the merge Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: say which arguments are defined differently and which values are kept Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve a dynamic tag before checking it for a restarted run Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: pin that a deployed dynamic tag is checked as it resolves Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
import { JobService, ScriptService, type QueuedJob, type Script } from '$lib/gen'
|
|
import { computeDiff } from '$lib/components/schema/schemaUtils.svelte'
|
|
|
|
const QUEUE_PAGE_SIZE = 1000
|
|
|
|
export type PerpetualRunsAtPath = {
|
|
/** Undefined when the runs at the path could not be listed: the deploy restarts them either way. */
|
|
count: number | undefined
|
|
/** Arguments the deployed schema removes, retypes or newly requires, compared with the runs' versions. */
|
|
mismatchedArgs: string[]
|
|
}
|
|
|
|
const RUNS_UNKNOWN: PerpetualRunsAtPath = { count: undefined, mismatchedArgs: [] }
|
|
|
|
/** By path rather than by the ids listed: a loop restarts every 10s, so the run listed when the
|
|
* modal opened is usually gone, and this endpoint makes the two passes that window needs. */
|
|
export async function stopPerpetualRuns(workspace: string, path: string): Promise<void> {
|
|
await JobService.cancelPersistentQueuedJobs({
|
|
workspace,
|
|
path,
|
|
requestBody: { reason: 'stopped before a new version was deployed' }
|
|
})
|
|
}
|
|
|
|
// Every page: the deploy switches every perpetual run at the path, so one past the first page
|
|
// still needs the prompt and its version's arguments compared.
|
|
async function listQueuedAtPath(workspace: string, path: string): Promise<QueuedJob[]> {
|
|
const jobs = new Map<string, QueuedJob>()
|
|
for (let page = 1; ; page++) {
|
|
const batch = await JobService.listQueue({
|
|
workspace,
|
|
scriptPathExact: path,
|
|
jobKinds: 'script',
|
|
perPage: QUEUE_PAGE_SIZE,
|
|
page
|
|
})
|
|
for (const job of batch) jobs.set(job.id, job)
|
|
if (batch.length < QUEUE_PAGE_SIZE) return [...jobs.values()]
|
|
}
|
|
}
|
|
|
|
export async function loadPerpetualRunsAtPath(
|
|
workspace: string,
|
|
path: string,
|
|
schema: { [key: string]: any } | undefined
|
|
): Promise<PerpetualRunsAtPath | undefined> {
|
|
// A deploy restarts every perpetual run at the path whatever this finds, so anything it cannot
|
|
// read leaves the count unknown rather than reporting none and skipping the prompt.
|
|
let queued: QueuedJob[]
|
|
try {
|
|
queued = await listQueuedAtPath(workspace, path)
|
|
} catch (error) {
|
|
console.error('Could not list the runs of this perpetual script', error)
|
|
return RUNS_UNKNOWN
|
|
}
|
|
// Only what the backend restarts: never a flow step or a run already canceled, and only a run
|
|
// of a perpetual version.
|
|
const candidates = queued.filter((job) => !job.is_flow_step && !job.canceled && job.script_hash)
|
|
const hashes = [...new Set(candidates.map((job) => job.script_hash!))]
|
|
let versions: Map<string, Script>
|
|
try {
|
|
versions = new Map(
|
|
await Promise.all(
|
|
hashes.map(
|
|
async (hash) => [hash, await ScriptService.getScriptByHash({ workspace, hash })] as const
|
|
)
|
|
)
|
|
)
|
|
} catch (error) {
|
|
console.error('Could not read the versions the runs of this script are on', error)
|
|
return RUNS_UNKNOWN
|
|
}
|
|
const runs = candidates.filter((job) => versions.get(job.script_hash!)?.restart_unless_cancelled)
|
|
if (runs.length === 0) {
|
|
return undefined
|
|
}
|
|
|
|
const mismatchedArgs = new Set<string>()
|
|
for (const hash of new Set(runs.map((job) => job.script_hash!))) {
|
|
const previous = versions.get(hash)?.schema
|
|
for (const [arg, { diff }] of Object.entries(computeDiff(schema, previous))) {
|
|
// An added argument only breaks a reused run when it is required, checked below.
|
|
if (diff !== 'same' && diff !== 'added') mismatchedArgs.add(arg)
|
|
}
|
|
const previouslyRequired: unknown[] = Array.isArray(previous?.required) ? previous.required : []
|
|
for (const arg of schema?.required ?? []) {
|
|
if (!previouslyRequired.includes(arg)) mismatchedArgs.add(arg)
|
|
}
|
|
}
|
|
|
|
return {
|
|
count: runs.length,
|
|
mismatchedArgs: [...mismatchedArgs]
|
|
}
|
|
}
|