From e8397ecded1208ba85d4df126dd3b4efdfe56190 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 15 Sep 2026 17:33:38 +0200 Subject: [PATCH] fix: key a cached workflow-as-code task on its step key, not its name --- backend/tests/fixtures/wac_flow_script.sql | 2 +- backend/windmill-common/src/wac.rs | 8 +++----- backend/windmill-worker/src/bun_executor.rs | 1 - backend/windmill-worker/src/common.rs | 21 +++++++++++---------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/backend/tests/fixtures/wac_flow_script.sql b/backend/tests/fixtures/wac_flow_script.sql index da50a6d35d..d284a500c0 100644 --- a/backend/tests/fixtures/wac_flow_script.sql +++ b/backend/tests/fixtures/wac_flow_script.sql @@ -66,7 +66,7 @@ const double = task(async (n: number) => { }, { cache_ttl: 60 }); export const main = workflow(async (n: number) => { - const r = await step("pick", async () => Math.random()); + const r = await step("pick", async () => Math.floor(Math.random() * 1e9)); const d = await double(r); return { fresh: d === r * 2 }; });' diff --git a/backend/windmill-common/src/wac.rs b/backend/windmill-common/src/wac.rs index 7a42ba413d..c994dcc4c9 100644 --- a/backend/windmill-common/src/wac.rs +++ b/backend/windmill-common/src/wac.rs @@ -40,11 +40,9 @@ pub struct WacCheckpoint { #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub _executing_key: Option, - /// With `_executing_key`: the task the child runs and the arguments it was - /// called with, the identity its cached result is keyed on. - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(default)] - pub _executing_task: Option, + /// With `_executing_key`: the arguments the task was called with. A cached + /// result is keyed on both; the key alone is a name and a position, which two + /// tasks can share, and the arguments alone say nothing about which task ran. #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub _executing_args: Option>, diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index a0d25f6025..8380ab0ef2 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -3237,7 +3237,6 @@ pub async fn handle_wac_v2_output( let child_checkpoint_json = serde_json::json!({ "completed_steps": &checkpoint.completed_steps, "_executing_key": &step.key, - "_executing_task": &step.name, "_executing_args": &step.args, }); sqlx::query( diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 714652a484..b96416ad2a 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -1575,15 +1575,16 @@ pub async fn cached_result_path( } } // A workflow-as-code task child carries its parent's arguments, which say - // nothing about the task's own inputs; its result is keyed on the task it - // runs and the arguments that task was called with. - let (task, args) = match wac_task_identity(db, job).await? { - Some((task, args)) => (Some(task), Some(Json(args))), + // nothing about the task's own inputs; its result is keyed on the step it + // runs (a task's name and position, which is what tells two tasks of one + // name apart) and the arguments that task was called with. + let (step, args) = match wac_task_identity(db, job).await? { + Some((step, args)) => (Some(step), Some(Json(args))), None => (None, job.args.clone()), }; - if let Some(task) = task { - hasher.update(b"wac_task:"); - hasher.update(task.as_bytes()); + if let Some(step) = step { + hasher.update(b"wac_step:"); + hasher.update(step.as_bytes()); } hash_args( db, @@ -1598,7 +1599,7 @@ pub async fn cached_result_path( Ok(format!("g/results/{:064x}", hasher.finalize())) } -/// The task name and call arguments a workflow-as-code parent seeded in this +/// The step key and call arguments a workflow-as-code parent seeded in this /// child's checkpoint at push time; `None` for any job that is not such a child. async fn wac_task_identity( db: &DB, @@ -1609,7 +1610,7 @@ async fn wac_task_identity( } let identity: Option<(Option, Option>>>)> = sqlx::query_as( - "SELECT workflow_as_code_status->'_checkpoint'->>'_executing_task', \ + "SELECT workflow_as_code_status->'_checkpoint'->>'_executing_key', \ workflow_as_code_status->'_checkpoint'->'_executing_args' \ FROM v2_job_status WHERE id = $1", ) @@ -1617,7 +1618,7 @@ async fn wac_task_identity( .fetch_optional(db) .await?; Ok(match identity { - Some((Some(task), Some(Json(args)))) => Some((task, args)), + Some((Some(step), Some(Json(args)))) => Some((step, args)), _ => None, }) }