Files
windmill/backend/windmill-worker/src/worker_flow.rs

4197 lines
156 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::common::{cached_result_path, save_in_cache};
use crate::js_eval::{eval_timeout, IdContext};
use crate::worker_utils::get_tag_and_concurrency;
use crate::{
JobCompletedSender, PreviousResult, SameWorkerSender, SendResult, UpdateFlow, KEEP_JOB_DIR,
};
use anyhow::Context;
use futures::TryFutureExt;
use mappable_rc::Marc;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use serde_json::{json, Value};
use sqlx::types::Json;
use sqlx::{FromRow, Postgres, Transaction};
use tracing::instrument;
use uuid::Uuid;
use windmill_common::auth::JobPerms;
#[cfg(feature = "benchmark")]
use windmill_common::bench::BenchmarkIter;
use windmill_common::cache::{self, RawData};
use windmill_common::client::AuthedClient;
use windmill_common::db::Authed;
use windmill_common::flow_status::{
ApprovalConditions, FlowStatusModuleWParent, Iterator as FlowIterator, JobResult,
};
use windmill_common::flows::{add_virtual_items_if_necessary, Branch, FlowNodeId, StopAfterIf};
use windmill_common::jobs::{
script_path_to_payload, JobKind, JobPayload, OnBehalfOf, RawCode, ENTRYPOINT_OVERRIDE,
};
use windmill_common::scripts::ScriptHash;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::to_raw_value;
use windmill_common::{
add_time, get_latest_flow_version_info_for_path, get_script_info_for_hash, FlowVersionInfo,
ScriptHashInfo,
};
use windmill_common::{
error::{self, to_anyhow, Error},
flow_status::{
Approval, BranchAllStatus, BranchChosen, FlowStatus, FlowStatusModule, RetryStatus,
MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL,
},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
};
use windmill_queue::flow_status::Step;
use windmill_queue::schedule::get_schedule_opt;
use windmill_queue::{
add_completed_job, add_completed_job_error, append_logs, get_mini_pulled_job,
handle_maybe_scheduled_job, insert_concurrency_key, interpolate_args, CanceledBy,
MiniPulledJob, PushArgs, PushIsolationLevel, SameWorkerPayload, WrappedError,
};
type DB = sqlx::Pool<sqlx::Postgres>;
use windmill_audit::audit_oss::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_queue::{canceled_job_to_result, push};
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion(
db: &DB,
client: &AuthedClient,
flow: uuid::Uuid,
job_id_for_status: &Uuid,
w_id: &str,
success: bool,
result: Arc<Box<RawValue>>,
unrecoverable: bool,
same_worker_tx: &SameWorkerSender,
worker_dir: &str,
stop_early_override: Option<bool>,
worker_name: &str,
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) -> error::Result<Option<Arc<MiniPulledJob>>> {
// this is manual tailrecursion because async_recursion blows up the stack
potentially_crash_for_testing();
let mut rec = RecUpdateFlowStatusAfterJobCompletion {
flow,
job_id_for_status: job_id_for_status.clone(),
success,
result,
stop_early_override,
skip_error_handler: false,
};
let mut unrecoverable = unrecoverable;
loop {
potentially_crash_for_testing();
let nrec = match update_flow_status_after_job_completion_internal(
db,
client,
rec.flow,
&rec.job_id_for_status,
w_id,
rec.success,
rec.result,
unrecoverable,
same_worker_tx,
worker_dir,
rec.stop_early_override,
rec.skip_error_handler,
worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
bench,
)
.await
{
Ok(j) => j,
Err(e) => {
tracing::error!("Error while updating flow status of {} after completion of {}, updating flow status again with error: {e:#}", rec.flow, &rec.job_id_for_status);
update_flow_status_after_job_completion_internal(
db,
client,
rec.flow,
&rec.job_id_for_status,
w_id,
false,
Arc::new(to_raw_value(&Json(&WrappedError {
error: json!(e.to_string()),
}))),
true,
same_worker_tx,
worker_dir,
rec.stop_early_override,
rec.skip_error_handler,
worker_name,
job_completed_tx.clone(),
#[cfg(feature = "benchmark")]
bench,
)
.await?
}
};
unrecoverable = false;
match nrec {
UpdateFlowStatusAfterJobCompletion::Done(job) => {
add_time!(bench, "update flow status internal END");
return Ok(Some(job));
}
UpdateFlowStatusAfterJobCompletion::Rec(nrec) => {
rec = nrec;
}
UpdateFlowStatusAfterJobCompletion::NonLastParallelBranch => {
add_time!(bench, "update flow status internal END");
return Ok(None);
}
UpdateFlowStatusAfterJobCompletion::NotDone => {
add_time!(bench, "update flow status internal END");
return Ok(None);
}
UpdateFlowStatusAfterJobCompletion::PreprocessingStep => {
add_time!(bench, "update flow status preprocessing step END");
return Ok(None);
}
}
}
}
pub enum UpdateFlowStatusAfterJobCompletion {
Rec(RecUpdateFlowStatusAfterJobCompletion),
Done(Arc<MiniPulledJob>),
NotDone,
NonLastParallelBranch,
PreprocessingStep,
}
pub struct RecUpdateFlowStatusAfterJobCompletion {
flow: uuid::Uuid,
job_id_for_status: Uuid,
success: bool,
result: Arc<Box<RawValue>>,
stop_early_override: Option<bool>,
skip_error_handler: bool,
}
#[derive(Deserialize)]
struct RecoveryObject {
recover: Option<bool>,
}
fn get_stop_after_if_data(
stop_early: bool,
stop_after_if: Option<&StopAfterIf>,
) -> (bool, Option<String>) {
if let Some(stop_after_if) = stop_after_if {
let err_msg = stop_early
.then(|| {
let err_msg = stop_after_if.error_message.as_ref().and_then(|message| {
let err_start_msg = "Flow early stop";
let s = if message.is_empty() {
format!("{}: {}", err_start_msg, &stop_after_if.expr)
} else {
format!("{}: {}", err_start_msg, message)
};
Some(s)
});
err_msg
})
.flatten();
return (stop_after_if.skip_if_stopped, err_msg);
}
return (false, None);
}
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion_internal(
db: &DB,
client: &AuthedClient,
flow: uuid::Uuid,
job_id_for_status: &Uuid,
w_id: &str,
mut success: bool,
result: Arc<Box<RawValue>>,
unrecoverable: bool,
same_worker_tx: &SameWorkerSender,
worker_dir: &str,
stop_early_override: Option<bool>,
skip_error_handler: bool,
worker_name: &str,
job_completed_tx: JobCompletedSender,
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
) -> error::Result<UpdateFlowStatusAfterJobCompletion> {
add_time!(bench, "update flow status internal START");
let (
should_continue_flow,
flow_job,
flow_data,
stop_early,
stop_early_err_msg,
skip_if_stop_early,
nresult,
is_failure_step,
_cleanup_module,
) = {
// tracing::debug!("UPDATE FLOW STATUS: {flow:?} {success} {result:?} {w_id} {depth}");
let (job_kind, script_hash, old_status, raw_flow) = sqlx::query!(
"SELECT
kind AS \"job_kind!: JobKind\",
runnable_id AS \"script_hash: ScriptHash\",
flow_status AS \"flow_status!: Json<Box<RawValue>>\",
raw_flow AS \"raw_flow: Json<Box<RawValue>>\"
FROM v2_job INNER JOIN v2_job_status ON v2_job.id = v2_job_status.id WHERE v2_job.id = $1 AND v2_job.workspace_id = $2 LIMIT 1",
flow,
w_id
)
.fetch_one(db)
.await
.map_err(|e| {
Error::internal_err(format!(
"fetching flow status {flow} while reporting {success} {result:?}: {e:#}"
))
})
.and_then(|record| {
Ok((
record.job_kind,
record.script_hash,
serde_json::from_str::<FlowStatus>(record.flow_status.0.get()).map_err(|e| {
Error::internal_err(format!(
"requiring current module to be parsable as FlowStatus: {e:?}"
))
})?,
record.raw_flow,
))
})?;
let flow_data = cache::job::fetch_flow(db, job_kind, script_hash)
.or_else(|_| cache::job::fetch_preview_flow(db, &flow, raw_flow))
.await?;
let flow_value = flow_data.value();
let module_step = Step::from_i32_and_len(old_status.step, old_status.modules.len());
let current_module = match module_step {
Step::Step(i) => flow_value.modules.get(i),
_ => None,
};
let module_status = match module_step {
Step::PreprocessorStep => old_status
.preprocessor_module
.as_ref()
.ok_or_else(|| Error::internal_err(format!("preprocessor module not found")))?,
Step::FailureStep => &old_status.failure_module.module_status,
Step::Step(i) => old_status
.modules
.get(i as usize)
.ok_or_else(|| Error::internal_err(format!("module {i} not found")))?,
};
// tracing::debug!(
// "UPDATE FLOW STATUS 2: {module_step:#?} {module_status:#?} {old_status:#?} "
// );
let (is_loop, skip_loop_failures, parallelism) = if matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
) {
let value = current_module
.as_ref()
.and_then(|x| x.get_value_with_skip_failures().ok());
(
true,
value
.as_ref()
.and_then(|x| x.skip_failures)
.unwrap_or(false),
value.as_ref().and_then(|x| x.parallelism),
)
} else {
(false, false, None)
};
let is_branch_all = matches!(
module_status,
FlowStatusModule::InProgress { branchall: Some(_), .. }
);
// 0 length flows are not failure steps
let is_failure_step =
old_status.step >= old_status.modules.len() as i32 && old_status.modules.len() > 0;
let (mut stop_early, mut stop_early_err_msg, mut skip_if_stop_early, continue_on_error) =
if let Some(se) = stop_early_override {
//do not stop early if module is a flow step
let step = match module_step {
Step::PreprocessorStep => None,
Step::FailureStep => None,
Step::Step(i) => Some(i),
};
let is_flow = if let Some(_) = step {
#[derive(Deserialize)]
struct GetType<'j> {
r#type: &'j str,
}
current_module
.map(|module| {
serde_json::from_str::<GetType>(module.value.get())
.map(|v| v.r#type == "flow")
})
.unwrap_or(Ok(false))
.unwrap_or(false)
} else {
false
};
if is_flow {
(false, None, false, false)
} else {
(true, None, se, false)
}
} else if is_failure_step || matches!(module_step, Step::PreprocessorStep) {
(false, None, false, false)
} else if let Some(current_module) = current_module {
let stop_early = success
&& !is_branch_all
&& if let Some(expr) = current_module
.stop_after_if
.as_ref()
.map(|x| x.expr.as_str())
{
let all_iters =
match &module_status {
FlowStatusModule::InProgress {
flow_jobs: Some(flow_jobs), ..
} if expr.contains("all_iters") => Some(Arc::new(
retrieve_flow_jobs_results(db, w_id, flow_jobs).await?,
)),
_ => None,
};
let args = sqlx::query_scalar!(
"SELECT
args AS \"args: Json<HashMap<String, Box<RawValue>>>\"
FROM v2_job
WHERE id = $1",
flow
)
.fetch_one(db)
.await
.map_err(|e| {
Error::internal_err(format!("retrieval of args from state: {e:#}"))
})?;
compute_bool_from_expr(
&expr,
Marc::new(args.unwrap_or_default().0),
result.clone(),
all_iters,
None,
Some(client),
None,
None,
)
.await?
} else {
false
};
let (skip_if_stopped, stop_early_err_msg) =
get_stop_after_if_data(stop_early, current_module.stop_after_if.as_ref());
(
stop_early,
stop_early_err_msg.filter(|_| !(is_loop || is_branch_all)),
skip_if_stopped,
current_module.continue_on_error.unwrap_or(false),
)
} else {
(false, None, false, false)
};
let skip_seq_branch_failure = match module_status {
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, .. }),
parallel: false,
..
} => {
compute_skip_branchall_failure(branch.to_owned(), false, current_module, &None)
.await?
}
_ => false,
};
let mut tx = db.begin().await?;
add_time!(bench, "process module status START");
let (inc_step_counter, new_status) = match module_status {
FlowStatusModule::InProgress {
iterator,
branchall,
parallel,
flow_jobs: Some(jobs),
flow_jobs_success,
..
} if *parallel => {
let (nindex, len) = match (iterator, branchall) {
(Some(FlowIterator { itered, .. }), _) => {
let position = if flow_jobs_success.is_some() {
find_flow_job_index(jobs, job_id_for_status)
} else {
None
};
let nindex = if let Some(position) = position {
sqlx::query_scalar!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'flow_jobs_success', $3::TEXT], $4),
ARRAY['modules', $1::TEXT, 'iterator', 'index'],
((flow_status->'modules'->$1::int->'iterator'->>'index')::int + 1)::text::jsonb
)
WHERE id = $2
RETURNING (flow_status->'modules'->$1::int->'iterator'->>'index')::int",
old_status.step,
flow,
position as i32,
json!(success)
)
} else {
sqlx::query_scalar!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
flow_status,
ARRAY['modules', $1::TEXT, 'iterator', 'index'],
((flow_status->'modules'->$1::int->'iterator'->>'index')::int + 1)::text::jsonb
)
WHERE id = $2
RETURNING (flow_status->'modules'->$1::int->'iterator'->>'index')::int",
old_status.step,
flow
)
}
.fetch_one(&mut *tx)
.await.map_err(|e| {
Error::internal_err(format!(
"error while fetching iterator index: {e:#}"
))
})?
.ok_or_else(|| Error::internal_err(format!("requiring an index in InProgress")))?;
tracing::info!(
"parallel iteration {job_id_for_status} of flow {flow} update nindex: {nindex} len: {len}",
nindex = nindex,
len = itered.len()
);
(nindex, itered.len() as i32)
}
(_, Some(BranchAllStatus { len, .. })) => {
let position = if flow_jobs_success.is_some() {
find_flow_job_index(jobs, job_id_for_status)
} else {
None
};
let nindex = if let Some(position) = position {
sqlx::query_scalar!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'flow_jobs_success', $3::TEXT], $4),
ARRAY['modules', $1::TEXT, 'branchall', 'branch'],
((flow_status->'modules'->$1::int->'branchall'->>'branch')::int + 1)::text::jsonb
)
WHERE id = $2
RETURNING (flow_status->'modules'->$1::int->'branchall'->>'branch')::int",
old_status.step,
flow,
position as i32,
json!(success)
)
} else {
sqlx::query_scalar!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
flow_status,
ARRAY['modules', $1::TEXT, 'branchall', 'branch'],
((flow_status->'modules'->$1::int->'branchall'->>'branch')::int + 1)::text::jsonb
)
WHERE id = $2
RETURNING (flow_status->'modules'->$1::int->'branchall'->>'branch')::int",
old_status.step,
flow
)
}
.fetch_one(&mut *tx)
.await
.map_err(|e| {
Error::internal_err(format!(
"error while fetching branchall index: {e:#}"
))
})?
.ok_or_else(|| Error::internal_err(format!("requiring an index in InProgress")))?;
(nindex, *len as i32)
}
_ => Err(Error::internal_err(format!(
"unexpected status for parallel module"
)))?,
};
// reset ping after updating flow status:
let _ = sqlx::query!("UPDATE v2_job_runtime SET ping = NULL WHERE id = $1", flow)
.execute(&mut *tx)
.await?;
if nindex == len {
let mut flow_jobs_success = flow_jobs_success.clone();
if let Some(flow_job_success) = flow_jobs_success.as_mut() {
let position = jobs.iter().position(|x| x == job_id_for_status);
if let Some(position) = position {
if position < flow_job_success.len() {
flow_job_success[position] = Some(success);
}
}
}
let new_status = if skip_loop_failures
|| sqlx::query_scalar!(
"SELECT status = 'success' OR status = 'skipped' AS \"success!\" FROM v2_job_completed WHERE id = ANY($1)",
jobs.as_slice()
)
.fetch_all(&mut *tx)
.await
.map_err(|e| {
Error::internal_err(format!(
"error while fetching sucess from completed_jobs: {e:#}"
))
})?
.into_iter()
.all(|x| x)
{
success = true;
FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
flow_jobs_success: flow_jobs_success.clone(),
branch_chosen: None,
approvers: vec![],
failed_retries: vec![],
skipped: false,
}
} else {
success = false;
FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
flow_jobs_success: flow_jobs_success.clone(),
branch_chosen: None,
failed_retries: vec![],
}
};
let r = sqlx::query_scalar!(
"DELETE FROM parallel_monitor_lock WHERE parent_flow_id = $1 RETURNING last_ping",
flow,
).fetch_optional(db).await.map_err(|e| {
Error::internal_err(format!(
"error while deleting parallel_monitor_lock: {e:#}"
))
})?;
if r.is_some() {
tracing::info!(
"parallel flow has removed lock on its parent, last ping was {:?}",
r.unwrap()
);
}
tracing::info!(
"parallel iteration {job_id_for_status} of flow {flow} has finished",
);
(true, Some(new_status))
} else {
add_time!(bench, "handle parallel flow start");
tx.commit().await?;
if parallelism.is_some() {
sqlx::query!(
"UPDATE v2_job_queue q SET suspend = 0
FROM v2_job j, v2_job_status f
WHERE q.workspace_id = $1 AND q.suspend = $3 AND j.parent_job = $2
AND f.id = j.id AND q.id = j.id
AND (f.flow_status->'step')::int = 0",
w_id,
flow,
nindex
)
.execute(db)
.await
.map_err(|e| {
Error::internal_err(format!(
"error resuming job at suspend {nindex} and parent {flow}: {e:#}"
))
})?;
}
let r = sqlx::query_scalar!(
"DELETE FROM parallel_monitor_lock WHERE parent_flow_id = $1 and job_id = $2 RETURNING last_ping",
flow,
job_id_for_status
).fetch_optional(db).await.map_err(|e| {
Error::internal_err(format!("error while removing parallel_monitor_lock: {e:#}"))
})?;
if r.is_some() {
tracing::info!(
"parallel flow has removed lock on its parent, last ping was {:?}",
r.unwrap()
);
}
add_time!(bench, "non final parallel flow finished");
return Ok(UpdateFlowStatusAfterJobCompletion::NonLastParallelBranch);
}
}
FlowStatusModule::InProgress {
iterator: Some(FlowIterator { index, itered, .. }),
flow_jobs_success,
flow_jobs,
while_loop,
..
} if (*while_loop
|| (*index + 1 < itered.len()) && (success || skip_loop_failures))
&& !stop_early =>
{
if let Some(jobs) = flow_jobs {
set_success_in_flow_job_success(
flow_jobs_success,
jobs,
job_id_for_status,
&old_status,
flow,
success,
&mut tx,
)
.await?;
}
(false, None)
}
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, len, .. }),
flow_jobs_success,
flow_jobs,
..
} if branch.to_owned() < len - 1 && (success || skip_seq_branch_failure) => {
if let Some(jobs) = flow_jobs {
set_success_in_flow_job_success(
flow_jobs_success,
jobs,
job_id_for_status,
&old_status,
flow,
success,
&mut tx,
)
.await?;
}
(false, None)
}
_ => {
if stop_early
&& matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
)
{
// if we're stopping early inside a loop, we just want to break the loop instead
stop_early = false;
}
let flow_jobs = module_status.flow_jobs();
let branch_chosen = module_status.branch_chosen();
let mut flow_jobs_success = module_status.flow_jobs_success();
if let (Some(flow_job_success), Some(flow_jobs)) =
(flow_jobs_success.as_mut(), flow_jobs.as_ref())
{
let position = flow_jobs.iter().position(|x| x == job_id_for_status);
if let Some(position) = position {
if position < flow_job_success.len() {
flow_job_success[position] = Some(success);
}
}
}
if success
|| (flow_jobs.is_some() && (skip_loop_failures || skip_seq_branch_failure))
{
let is_skipped = if current_module.as_ref().is_some_and(|m| m.skip_if.is_some())
{
sqlx::query_scalar!(
"SELECT kind = 'identity' FROM v2_job WHERE id = $1",
job_id_for_status
)
.fetch_one(db)
.await
.map_err(|e| {
Error::internal_err(format!("error during skip check: {e:#}"))
})?
.unwrap_or(false)
} else {
false
};
success = true;
(
true,
Some(FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
flow_jobs_success,
branch_chosen,
approvers: vec![],
failed_retries: old_status.retry.failed_jobs.clone(),
skipped: is_skipped,
}),
)
} else {
let inc = if continue_on_error {
let retry = current_module
.as_ref()
.and_then(|x| x.retry.clone())
.unwrap_or_default();
tracing::info!("update flow status on rety: {retry:#?} ");
next_retry(&retry, &old_status.retry).is_none()
} else {
false
};
(
inc,
Some(FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
flow_jobs_success,
branch_chosen,
failed_retries: old_status.retry.failed_jobs.clone(),
}),
)
}
}
};
let skip_parallel_branchall_failure = match (module_status, new_status.as_ref()) {
(
FlowStatusModule::InProgress { branchall: Some(_), parallel: true, .. },
Some(FlowStatusModule::Success { flow_jobs_success, .. }),
) => compute_skip_branchall_failure(0, true, current_module, flow_jobs_success).await?,
(
FlowStatusModule::InProgress { branchall: Some(_), parallel: true, .. },
Some(FlowStatusModule::Failure { flow_jobs_success, .. }),
) => compute_skip_branchall_failure(0, true, current_module, flow_jobs_success).await?,
_ => false,
};
let step_counter = if inc_step_counter {
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)
WHERE id = $2",
json!(old_status.step + 1),
flow
)
.execute(&mut *tx)
.await
.map_err(|e| {
Error::internal_err(format!("error while setting flow index for {flow}: {e:#}"))
})?;
old_status.step + 1
} else {
old_status.step
};
// tracing::error!(
// "step_counter: {:?} {} {inc_step_counter} {flow}",
// step_counter,
// old_status.step,
// );
// panic!("stop");
/* is_last_step is true when the step_counter (the next step index) is an invalid index */
let is_last_step = usize::try_from(step_counter)
.map(|i| !(..old_status.modules.len()).contains(&i))
.unwrap_or(true);
if let Some(new_status) = new_status.as_ref() {
if is_failure_step {
let parent_module = sqlx::query_scalar!(
"SELECT flow_status->'failure_module'->>'parent_module' FROM v2_job_status WHERE id = $1",
flow
)
.fetch_one(&mut *tx)
.await.map_err(|e| {
Error::internal_err(format!(
"error while fetching failure module: {e:#}"
))
})?;
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['failure_module'], $1)
WHERE id = $2",
json!(FlowStatusModuleWParent {
parent_module,
module_status: new_status.clone()
}),
flow
)
.execute(&mut *tx)
.await
.map_err(|e| {
Error::internal_err(format!(
"error while setting flow status in failure step: {e:#}"
))
})?;
} else if matches!(module_step, Step::PreprocessorStep) {
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['preprocessor_module'], $1)
WHERE id = $2",
json!(new_status),
flow
)
.execute(&mut *tx)
.await
.map_err(|e| {
Error::internal_err(format!(
"error while setting flow status in preprocessing step: {e:#}"
))
})?;
} else {
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2)
WHERE id = $3",
old_status.step.to_string(),
json!(new_status),
flow
)
.execute(&mut *tx)
.await
.map_err(|e| {
Error::internal_err(format!("error while setting new flow status: {e:#}"))
})?;
if let Some(job_result) = new_status.job_result() {
sqlx::query!(
"UPDATE v2_job_status
SET flow_leaf_jobs = JSONB_SET(coalesce(flow_leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)
WHERE COALESCE((SELECT flow_innermost_root_job FROM v2_job WHERE id = $3), $3) = id",
new_status.id(),
json!(job_result),
flow
)
.execute(&mut *tx)
.await.map_err(|e| {
Error::internal_err(format!(
"error while setting leaf jobs: {e:#}"
))
})?;
}
}
}
let mut nresult = if let Some(stop_early_err_msg) = stop_early_err_msg.as_ref() {
Arc::new(to_raw_value(stop_early_err_msg))
} else {
match &new_status {
Some(FlowStatusModule::Success { flow_jobs: Some(jobs), .. })
| Some(FlowStatusModule::Failure { flow_jobs: Some(jobs), .. }) => {
Arc::new(retrieve_flow_jobs_results(db, w_id, jobs).await?)
}
_ => result.clone(),
}
};
match &new_status {
Some(FlowStatusModule::Success { .. }) if is_loop || is_branch_all => {
if let Some(stop_after_all_iters_if) = current_module
.as_ref()
.and_then(|m| m.stop_after_all_iters_if.as_ref())
{
let args = sqlx::query_scalar!(
"SELECT args AS \"args: Json<HashMap<String, Box<RawValue>>>\"
FROM v2_job WHERE id = $1",
flow
)
.fetch_one(db)
.await
.map_err(|e| {
Error::internal_err(format!("retrieval of args from state: {e:#}"))
})?;
let should_stop = compute_bool_from_expr(
&stop_after_all_iters_if.expr,
Marc::new(args.unwrap_or_default().0),
nresult.clone(),
None,
None,
Some(client),
None,
None,
)
.await?;
if should_stop {
stop_early = true;
let (skip_if_stopped, err_msg_internal) =
get_stop_after_if_data(should_stop, Some(stop_after_all_iters_if));
skip_if_stop_early = skip_if_stopped;
if err_msg_internal.is_some() {
stop_early_err_msg = err_msg_internal;
nresult = Arc::new(to_raw_value(&stop_early_err_msg));
}
}
}
}
_ => {}
}
if old_status.retry.fail_count > 0
&& matches!(&new_status, Some(FlowStatusModule::Success { .. }))
{
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = flow_status - 'retry'
WHERE id = $1",
flow
)
.execute(&mut *tx)
.await
.context("remove flow status retry")?;
}
let flow_job = get_mini_pulled_job(&mut *tx, &flow)
.await?
.ok_or_else(|| Error::internal_err(format!("requiring flow to be in the queue")))?;
tx.commit().await?;
if matches!(module_step, Step::PreprocessorStep) {
let tag_and_concurrency_key = get_tag_and_concurrency(&flow, db).await;
let require_args = tag_and_concurrency_key.as_ref().is_some_and(|x| {
x.tag.as_ref().is_some_and(|t| t.contains("$args"))
|| x.concurrency_key
.as_ref()
.is_some_and(|ck| ck.contains("$args"))
});
let mut tag = tag_and_concurrency_key
.as_ref()
.map(|x| x.tag.clone())
.flatten();
let concurrency_key = tag_and_concurrency_key
.as_ref()
.map(|x| x.concurrency_key.clone())
.flatten();
let concurrent_limit = tag_and_concurrency_key
.as_ref()
.map(|x| x.concurrent_limit)
.flatten();
let concurrency_time_window_s = tag_and_concurrency_key
.as_ref()
.map(|x| x.concurrency_time_window_s)
.flatten();
if require_args {
let args = sqlx::query_scalar!(
"SELECT result as \"result: Json<HashMap<String, Box<RawValue>>>\"
FROM v2_job_completed
WHERE id = $1",
job_id_for_status
)
.fetch_one(db)
.await
.map_err(|e| {
Error::internal_err(format!("error while fetching preprocessing args: {e:#}"))
})?;
let args_hm = args.unwrap_or_default().0;
let args = PushArgs::from(&args_hm);
if let Some(ck) = concurrency_key {
let mut tx = db.begin().await?;
insert_concurrency_key(
&flow_job.workspace_id,
&args,
&flow_job.runnable_path,
JobKind::Flow,
Some(ck),
&mut tx,
flow,
)
.await?;
tx.commit().await?;
}
if let Some(t) = tag {
tag = Some(interpolate_args(t, &args, &flow_job.workspace_id));
}
} else if let Some(ck) = concurrency_key {
let mut tx = db.begin().await?;
insert_concurrency_key(
&flow_job.workspace_id,
&PushArgs::from(&HashMap::new()),
&flow_job.runnable_path,
JobKind::Flow,
Some(ck),
&mut tx,
flow,
)
.await?;
tx.commit().await?;
}
// let tag = tag_and_concurrency_key.and_then(|tc| tc.tag.map(|t| interpolate_args(t.clone(), &args, &workspace_id)));
// let concurrency_key = tag_and_concurrency_key.and_then(|tc| tc.concurrency_key.map(|ck| interpolate_args(&ck, &args, &workspace_id)));
sqlx::query!(
"WITH job_result AS (
SELECT result
FROM v2_job_completed
WHERE id = $1
),
updated_queue AS (
UPDATE v2_job_queue
SET running = false,
tag = COALESCE($3, tag)
WHERE id = $2
)
UPDATE v2_job
SET
tag = COALESCE($3, tag),
concurrent_limit = COALESCE($4, concurrent_limit),
concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),
args = COALESCE(
CASE
WHEN job_result.result IS NULL THEN NULL
WHEN jsonb_typeof(job_result.result) = 'object'
THEN job_result.result
WHEN jsonb_typeof(job_result.result) = 'null'
THEN NULL
ELSE jsonb_build_object('value', job_result.result)
END,
'{}'::jsonb
),
preprocessed = TRUE
FROM job_result
WHERE v2_job.id = $2;
",
job_id_for_status,
flow,
tag,
concurrent_limit,
concurrency_time_window_s,
)
.execute(db)
.await
.map_err(|e| {
Error::internal_err(format!(
"error while updating args in preprocessing step: {e:#}"
))
})?;
if success {
return Ok(UpdateFlowStatusAfterJobCompletion::PreprocessingStep);
}
}
let job_root = flow_job
.flow_innermost_root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
let should_continue_flow = match success {
_ if stop_early => false,
_ if flow_job.is_canceled() => false,
true => !is_last_step,
false if unrecoverable => false,
false
if skip_seq_branch_failure
|| skip_parallel_branchall_failure
|| skip_loop_failures
|| continue_on_error =>
{
!is_last_step
}
false
if next_retry(
match module_step {
Step::PreprocessorStep => flow_value
.preprocessor_module
.as_ref()
.and_then(|m| m.retry.as_ref()),
Step::Step(i) => flow_value
.modules
.get(i)
.as_ref()
.and_then(|m| m.retry.as_ref()),
Step::FailureStep => flow_value
.failure_module
.as_ref()
.and_then(|m| m.retry.as_ref()),
}
.unwrap_or(&Retry::default()),
&old_status.retry,
)
.is_some() =>
{
true
}
false
if !is_failure_step
&& !skip_error_handler
&& flow_value.failure_module.is_some() =>
{
true
}
false => false,
};
tracing::debug!(id = %flow_job.id, root_id = %job_root, "flow status updated");
(
should_continue_flow,
flow_job,
flow_data,
stop_early,
stop_early_err_msg,
skip_if_stop_early,
nresult,
is_failure_step,
old_status.cleanup_module,
)
};
let flow_job = Arc::new(flow_job);
let done = if !should_continue_flow {
{
let logs = if flow_job.is_canceled() {
"Flow job canceled\n".to_string()
} else if stop_early {
format!("Flow job stopped early because of a stop early predicate returning true\n")
} else if success {
"Flow job completed with success\n".to_string()
} else {
"Flow job completed with error\n".to_string()
};
append_logs(&flow_job.id, w_id, logs, &db.into()).await;
}
#[cfg(feature = "enterprise")]
if flow_job.parent_job.is_none() {
// run the cleanup step only when the root job is complete
if !_cleanup_module.flow_jobs_to_clean.is_empty() {
tracing::debug!(
"Cleaning up jobs arguments, result and logs as they were marked as delete_after_use {:?}",
_cleanup_module.flow_jobs_to_clean
);
sqlx::query!(
"UPDATE v2_job SET args = '{}'::jsonb WHERE id = ANY($1)",
&_cleanup_module.flow_jobs_to_clean,
)
.execute(db)
.await
.map_err(|e| {
Error::InternalErr(format!("error while cleaning up completed job: {e:#}"))
})?;
sqlx::query!(
"UPDATE v2_job_completed SET result = '{}'::jsonb WHERE id = ANY($1)",
&_cleanup_module.flow_jobs_to_clean,
)
.execute(db)
.await
.map_err(|e| {
Error::internal_err(format!("error while cleaning up completed job: {e:#}"))
})?;
}
}
if flow_job.is_canceled() {
add_completed_job_error(
db,
&flow_job,
0,
Some(CanceledBy {
username: flow_job.canceled_by.clone(),
reason: flow_job.canceled_reason.clone(),
}),
canceled_job_to_result(&flow_job),
worker_name,
true,
None,
)
.await?;
} else {
if flow_job.cache_ttl.is_some() && success {
let flow = RawData::Flow(flow_data.clone());
let cached_res_path = cached_result_path(db, client, &flow_job, Some(&flow)).await;
save_in_cache(db, client, &flow_job, cached_res_path, nresult.clone()).await;
}
fn result_has_recover_true(nresult: Arc<Box<RawValue>>) -> bool {
let recover = serde_json::from_str::<RecoveryObject>(nresult.get());
return recover.map(|r| r.recover.unwrap_or(false)).unwrap_or(false);
}
let success = success
&& (!is_failure_step || result_has_recover_true(nresult.clone()))
&& !skip_error_handler
&& stop_early_err_msg.is_none();
add_time!(bench, "flow status update 1");
if success {
add_completed_job(
db,
&flow_job,
true,
stop_early && skip_if_stop_early,
Json(&nresult),
None,
0,
None,
true,
None,
)
.await?;
} else {
add_completed_job(
db,
&flow_job,
false,
stop_early && skip_if_stop_early,
Json(
&serde_json::from_str::<Value>(nresult.get()).unwrap_or_else(
|e| json!({"error": format!("Impossible to serialize error: {e:#}")}),
),
),
None,
0,
None,
true,
None,
)
.await?;
}
}
true
} else {
tracing::debug!(id = %flow_job.id, "start handle flow");
match handle_flow(
flow_job.clone(),
&flow_data,
db,
client,
Some(nresult.clone()),
same_worker_tx,
worker_dir,
job_completed_tx,
worker_name,
)
.warn_after_seconds(10)
.await
{
Err(err) => {
let e = json!({"message": err.to_string(), "name": "InternalError"});
append_logs(
&flow_job.id,
w_id,
format!("Unexpected error during flow chaining:\n{:#?}", e),
&db.into(),
)
.await;
let _ = add_completed_job_error(db, &flow_job, 0, None, e, worker_name, true, None)
.await;
true
}
Ok(_) => false,
}
};
if done {
if flow_job.same_worker && !KEEP_JOB_DIR.load(Ordering::Relaxed) {
let _ = tokio::fs::remove_dir_all(format!("{worker_dir}/{}", flow_job.id)).await;
}
if flow_job.is_flow_step() {
if let Some(parent_job) = flow_job.parent_job {
tracing::info!(subflow_id = %flow_job.id, parent_id = %parent_job, "subflow is finished, updating parent flow status");
return Ok(UpdateFlowStatusAfterJobCompletion::Rec(
RecUpdateFlowStatusAfterJobCompletion {
flow: parent_job,
job_id_for_status: flow,
success: success && !is_failure_step,
result: nresult.clone(),
stop_early_override: if stop_early {
Some(skip_if_stop_early)
} else {
None
},
skip_error_handler: skip_error_handler || is_failure_step,
},
));
}
}
Ok(UpdateFlowStatusAfterJobCompletion::Done(flow_job))
} else {
Ok(UpdateFlowStatusAfterJobCompletion::NotDone)
}
}
fn find_flow_job_index(flow_jobs: &Vec<Uuid>, job_id_for_status: &Uuid) -> Option<usize> {
flow_jobs.iter().position(|x| x == job_id_for_status)
}
async fn set_success_in_flow_job_success<'c>(
flow_jobs_success: &Option<Vec<Option<bool>>>,
flow_jobs: &Vec<Uuid>,
job_id_for_status: &Uuid,
old_status: &FlowStatus,
flow: Uuid,
success: bool,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<()> {
if flow_jobs_success.is_some() {
let position = find_flow_job_index(flow_jobs, job_id_for_status);
if let Some(position) = position {
sqlx::query!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
flow_status,
ARRAY['modules', $1::TEXT, 'flow_jobs_success', $3::TEXT],
$4
)
WHERE id = $2",
old_status.step as i32,
flow,
position as i32,
json!(success)
)
.execute(&mut **tx)
.await
.map_err(|e| {
Error::internal_err(format!("error while setting flow_jobs_success: {e:#}"))
})?;
}
}
Ok(())
}
async fn retrieve_flow_jobs_results(
db: &DB,
w_id: &str,
job_uuids: &Vec<Uuid>,
) -> error::Result<Box<RawValue>> {
let results = sqlx::query!(
"SELECT result, id
FROM v2_job_completed
WHERE id = ANY($1) AND workspace_id = $2",
job_uuids.as_slice(),
w_id
)
.fetch_all(db)
.await?
.into_iter()
.map(|br| (br.id, br.result))
.collect::<HashMap<_, _>>();
let results = job_uuids
.iter()
.map(|j| {
results
.get(j)
.ok_or_else(|| Error::internal_err(format!("missing job result for {}", j)))
})
.collect::<Result<Vec<_>, _>>()?;
tracing::debug!("Retrieved results for flow jobs {:?}", results);
Ok(to_raw_value(&results))
}
async fn compute_skip_branchall_failure<'c>(
branch: usize,
parallel: bool,
flow_module: Option<&FlowModule>,
successes: &Option<Vec<Option<bool>>>,
) -> windmill_common::error::Result<bool> {
let branches = flow_module
.and_then(|x| x.get_branches_skip_failures().ok())
.map(|x| {
x.branches
.iter()
.map(|b| b.skip_failure.unwrap_or(false))
.collect::<Vec<_>>()
});
if parallel {
if let Some(successes) = successes {
for (i, success) in successes.iter().enumerate() {
if branches
.as_ref()
.and_then(|x| x.get(i))
.unwrap_or(&false)
.to_owned()
{
continue;
}
if !(success.unwrap_or(false)) {
return Ok(false);
}
}
Ok(true)
} else {
Ok(false)
}
} else {
Ok(branches
.and_then(|x| x.get(branch as usize).map(|b| b.to_owned()))
.unwrap_or(false))
}
}
// async fn retrieve_cleanup_module<'c>(flow_uuid: Uuid, db: &DB) -> Result<FlowCleanupModule, Error> {
// tracing::warn!("Retrieving cleanup module of flow {}", flow_uuid);
// let raw_value = sqlx::query_scalar!(
// "SELECT flow_status->'cleanup_module' as cleanup_module
// FROM queue
// WHERE id = $1",
// flow_uuid,
// )
// .fetch_one(db)
// .await
// .map_err(|e| Error::internal_err(format!("error during retrieval of cleanup module: {e:#}")))?;
// raw_value
// .clone()
// .and_then(|rv| serde_json::from_value::<FlowCleanupModule>(rv).ok())
// .ok_or(Error::internal_err(format!(
// "Unable to parse flow cleanup module {:?}",
// raw_value
// )))
// }
fn next_retry(retry: &Retry, status: &RetryStatus) -> Option<(u32, Duration)> {
(status.fail_count <= MAX_RETRY_ATTEMPTS)
.then(|| &retry)
.and_then(|retry| retry.interval(status.fail_count, false))
.map(|d| (status.fail_count + 1, std::cmp::min(d, MAX_RETRY_INTERVAL)))
}
async fn compute_bool_from_expr(
expr: &str,
flow_args: Marc<HashMap<String, Box<RawValue>>>,
result: Arc<Box<RawValue>>,
all_iters: Option<Arc<Box<RawValue>>>,
by_id: Option<&IdContext>,
client: Option<&AuthedClient>,
resumes: Option<(Arc<Box<RawValue>>, Arc<Box<RawValue>>, Arc<Box<RawValue>>)>,
ctx: Option<Vec<(String, String)>>,
) -> error::Result<bool> {
let mut context = HashMap::with_capacity(if resumes.is_some() { 7 } else { 3 });
context.insert("result".to_string(), result.clone());
if let Some(all_iters) = all_iters {
context.insert("all_iters".to_string(), all_iters);
}
context.insert("previous_result".to_string(), result.clone());
if let Some(resumes) = resumes {
context.insert("resume".to_string(), resumes.1);
context.insert("resumes".to_string(), resumes.0);
context.insert("approvers".to_string(), resumes.2);
}
match eval_timeout(
format!("Boolean({expr})"),
context,
Some(flow_args),
client,
by_id,
ctx,
)
.await?
.get()
{
"true" => Ok(true),
"false" => Ok(false),
a @ _ => Err(Error::ExecutionErr(format!(
"Expected a boolean value, found: {a:?}"
))),
}
}
/// resumes should be in order of timestamp ascending, so that more recent are at the end
#[instrument(level = "trace", skip_all)]
async fn transform_input(
flow_args: Marc<HashMap<String, Box<RawValue>>>,
last_result: Arc<Box<RawValue>>,
input_transforms: &HashMap<String, InputTransform>,
resumes: Arc<Box<RawValue>>,
resume: Arc<Box<RawValue>>,
approvers: Arc<Box<RawValue>>,
by_id: &IdContext,
client: &AuthedClient,
) -> windmill_common::error::Result<HashMap<String, Box<RawValue>>> {
let mut mapped = HashMap::new();
let mut env = HashMap::new();
if input_transforms
.iter()
.any(|x| matches!(x.1, InputTransform::Javascript { .. }))
{
env.insert("params".to_string(), Arc::new(to_raw_value(&mapped)));
env.insert("previous_result".to_string(), last_result);
env.insert("resume".to_string(), resume);
env.insert("resumes".to_string(), resumes);
env.insert("approvers".to_string(), approvers);
}
for (key, val) in input_transforms.into_iter() {
match val {
InputTransform::Static { value } => {
mapped.insert(key.to_string(), to_raw_value(&value));
}
InputTransform::Javascript { expr } => {
let v = eval_timeout(
expr.to_string(),
env.clone(),
Some(flow_args.clone()),
Some(client),
Some(by_id),
None,
)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"Error during isolated evaluation of expression `{expr}`:\n{e:#}"
))
})?;
mapped.insert(key.to_string(), v);
}
}
}
Ok(mapped)
}
#[instrument(level = "trace", skip_all)]
pub async fn handle_flow(
flow_job: Arc<MiniPulledJob>,
flow_data: &cache::FlowData,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_result: Option<Arc<Box<RawValue>>>,
same_worker_tx: &SameWorkerSender,
worker_dir: &str,
job_completed_tx: JobCompletedSender,
worker_name: &str,
) -> anyhow::Result<()> {
let flow = flow_data.value();
let status = flow_job
.parse_flow_status()
.with_context(|| "Unable to parse flow status")?;
let schedule_path = flow_job.schedule_path();
if !flow_job.is_flow_step()
&& status.retry.fail_count == 0
&& schedule_path.is_some()
&& flow_job.runnable_path.is_some()
&& status.step == 0
{
let schedule_path = schedule_path.as_ref().unwrap();
let schedule = get_schedule_opt(db, &flow_job.workspace_id, schedule_path)
.warn_after_seconds(5)
.await?;
if let Some(schedule) = schedule {
if let Err(err) = handle_maybe_scheduled_job(
db,
&flow_job,
&schedule,
flow_job.runnable_path.as_ref().unwrap(),
&flow_job.workspace_id,
)
.warn_after_seconds(5)
.await
{
match err {
Error::QuotaExceeded(_) => return Err(err.into()),
// scheduling next job failed and could not disable schedule => make zombie job to retry
_ => return Ok(()),
}
};
} else {
tracing::error!(
"Schedule {schedule_path} in {} not found. Impossible to schedule again",
&flow_job.workspace_id
);
}
}
let mut rec = PushNextFlowJobRec { flow_job: flow_job, status: status };
loop {
let PushNextFlowJobRec { flow_job, status } = rec;
let next = push_next_flow_job(
flow_job,
status,
flow,
db,
client,
last_result.clone(),
same_worker_tx,
worker_dir,
worker_name,
)
.warn_after_seconds(10)
.await?;
match next {
PushNextFlowJob::Rec(nrec) => {
tracing::info!("recursively pushing next flow job {}", nrec.flow_job.id);
rec = nrec;
}
PushNextFlowJob::Done(update_flow) => {
if let Some(update_flow) = update_flow {
tracing::info!(
"sending flow status update {} with success {} to job completed channel",
update_flow.flow,
update_flow.success
);
job_completed_tx
.send(SendResult::UpdateFlow(update_flow), false)
.warn_after_seconds(3)
.await
.map_err(|e| {
Error::internal_err(format!(
"error sending update flow message to job completed channel: {e:#}"
))
})?;
}
break;
}
}
}
Ok(())
}
#[derive(Serialize, Debug)]
pub struct Iter {
index: i32,
value: Box<RawValue>,
}
#[derive(FromRow)]
pub struct ResumeRow {
pub value: Json<Box<RawValue>>,
pub approver: Option<String>,
pub approved: bool,
pub resume_id: i32,
}
lazy_static::lazy_static! {
static ref CRASH_FORCEFULLY_AT_STEP: Option<usize> = std::env::var("CRASH_FORCEFULLY_AT_STEP")
.ok()
.and_then(|x| x.parse::<usize>().ok());
static ref CRASH_STEP_COUNTER: AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
static ref BRANCHALL_INDEX_RE: regex::Regex = regex::Regex::new(r"/branchall-(\d+)$").unwrap();
}
#[inline(always)]
fn potentially_crash_for_testing() {
#[cfg(feature = "flow_testing")]
if let Some(crash_at) = CRASH_FORCEFULLY_AT_STEP.as_ref() {
let counter = CRASH_STEP_COUNTER.fetch_add(1, Ordering::SeqCst);
if &counter == crash_at {
panic!("CRASH#1 - expected crash for testing at step {}", crash_at);
}
}
}
// static
lazy_static::lazy_static! {
pub static ref EHM: HashMap<String, Box<RawValue>> = HashMap::new();
}
enum PushNextFlowJob {
Rec(PushNextFlowJobRec),
Done(Option<UpdateFlow>),
}
struct PushNextFlowJobRec {
flow_job: Arc<MiniPulledJob>,
status: FlowStatus,
}
// #[async_recursion]
// #[instrument(level = "trace", skip_all)]
async fn push_next_flow_job(
flow_job: Arc<MiniPulledJob>,
mut status: FlowStatus,
flow: &FlowValue,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_job_result: Option<Arc<Box<RawValue>>>,
same_worker_tx: &SameWorkerSender,
worker_dir: &str,
worker_name: &str,
) -> error::Result<PushNextFlowJob> {
let job_root = flow_job
.flow_innermost_root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "pushing next flow job");
let mut step = Step::from_i32_and_len(status.step, flow.modules.len());
let mut status_module = match step {
Step::Step(i) => status
.modules
.get(i)
.cloned()
.unwrap_or_else(|| status.failure_module.module_status.clone()),
Step::PreprocessorStep => status
.preprocessor_module
.clone()
.unwrap_or_else(|| status.failure_module.module_status.clone()),
Step::FailureStep => status.failure_module.module_status.clone(),
};
let fj: mappable_rc::Marc<MiniPulledJob> = flow_job.clone().into();
let arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>> = Marc::map(fj, |x| {
if let Some(args) = &x.args {
&args.0
} else {
&EHM
}
});
// if this is an empty module of if the module has already been completed, successfully, update the parent flow
if flow.modules.is_empty() || matches!(status_module, FlowStatusModule::Success { .. }) {
return Ok(PushNextFlowJob::Done(Some(UpdateFlow {
flow: flow_job.id,
success: true,
result: if flow.modules.is_empty() {
to_raw_value(arc_flow_job_args.as_ref())
} else {
// it has to be an empty for loop event
serde_json::from_str("[]").unwrap()
},
stop_early_override: None,
w_id: flow_job.workspace_id.clone(),
worker_dir: worker_dir.to_string(),
token: client.token.clone(),
})));
}
if matches!(step, Step::Step(0)) {
if !flow_job.is_flow_step() && flow_job.schedule_path().is_some() {
let schedule_path = flow_job.schedule_path();
let no_flow_overlap = sqlx::query_scalar!(
"SELECT no_flow_overlap FROM schedule WHERE path = $1 AND workspace_id = $2",
schedule_path.as_ref().unwrap(),
flow_job.workspace_id.as_str()
)
.fetch_one(db)
.warn_after_seconds(3)
.await?;
if no_flow_overlap {
let overlapping = sqlx::query_scalar!(
// Query plan:
// - use of the `ix_v2_job_root_by_path` index; hence the `parent_job IS NULL`
// clause.
// - select from `v2_job` first, then join with `v2_job_queue` to avoid a full
// table scan on `running = true`.
"SELECT id
FROM v2_job j JOIN v2_job_queue USING (id)
WHERE j.workspace_id = $2 AND trigger_kind = 'schedule' AND trigger = $1 AND runnable_path = $4
AND parent_job IS NULL
AND j.id != $3
AND running = true",
schedule_path.as_ref().unwrap(),
flow_job.workspace_id.as_str(),
flow_job.id,
flow_job.runnable_path()
)
.fetch_all(db)
.warn_after_seconds(3)
.await?;
if overlapping.len() > 0 {
let overlapping_str = overlapping
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(", ");
return Ok(PushNextFlowJob::Done(Some(
UpdateFlow {
flow: flow_job.id,
success: true,
result: serde_json::from_str(
&format!("\"not allowed to overlap with {overlapping_str}, scheduling next iteration\""),
)
.unwrap(),
stop_early_override: Some(true),
w_id: flow_job.workspace_id.clone(),
worker_dir: worker_dir.to_string(),
token: client.token.clone(),
}
)));
}
}
}
if let Some(skip_expr) = &flow.skip_expr {
let skip = compute_bool_from_expr(
&skip_expr,
arc_flow_job_args.clone(),
Arc::new(to_raw_value(&json!("{}"))),
None,
None,
Some(client),
None,
Some(vec![(
windmill_common::variables::WM_SCHEDULED_FOR.to_string(),
flow_job.scheduled_for.to_string(),
)]),
)
.warn_after_seconds(3)
.await?;
if skip {
return Ok(PushNextFlowJob::Done(Some(UpdateFlow {
flow: flow_job.id,
success: true,
result: serde_json::from_str("\"stopped early\"").unwrap(),
stop_early_override: Some(true),
w_id: flow_job.workspace_id.clone(),
worker_dir: worker_dir.to_string(),
token: client.token.clone(),
})));
}
}
}
// Compute and initialize last_job_result
let arc_last_job_result = if status_module.is_failure() {
// if job is being retried, pass the result of its previous failure
last_job_result.unwrap_or_else(|| Arc::new(to_raw_value(&json!("{}"))))
} else if matches!(step, Step::Step(0)) || matches!(step, Step::PreprocessorStep) {
// if it's the first job executed in the flow, pass the flow args
Arc::new(to_raw_value(&flow_job.args))
} else {
// else pass the last job result. Either from the function arg if it's set, or manually fetch it from the previous job
// having last_job_result empty can happen either when the job was suspended and is being restarted, or if it's a
// flow restart from a specific step
if last_job_result.is_some() {
last_job_result.unwrap()
} else {
match get_previous_job_result(db, flow_job.workspace_id.as_str(), &status)
.warn_after_seconds(3)
.await?
{
None => Arc::new(to_raw_value(&json!("{}"))),
Some(previous_job_result) => Arc::new(previous_job_result),
}
}
};
let mut resume_messages: Vec<Box<RawValue>> = vec![];
let mut approvers: Vec<String> = vec![];
/* (suspend / resume), when starting a module, if previous module has a
* non-zero `suspend` value, collect `resume_job`s for the previous module job.
*
* If there aren't enough, try again later. */
if matches!(
&status_module,
FlowStatusModule::WaitingForPriorSteps { .. } | FlowStatusModule::WaitingForEvents { .. }
) {
if let Some((suspend, last)) = needs_resume(&flow, &status) {
let mut tx = db.begin().warn_after_seconds(3).await?;
/* Lock this row to prevent the suspend column getting out out of sync
* if a resume message arrives after we fetch and count them here.
*
* This only works because jobs::resume_job does the same thing. */
sqlx::query_scalar!(
"SELECT null FROM v2_job_queue WHERE id = $1 FOR UPDATE",
flow_job.id
)
.fetch_one(&mut *tx)
.warn_after_seconds(3)
.await
.context("lock flow in queue")?;
let resumes = sqlx::query_as::<_, ResumeRow>(
"SELECT value, approver, resume_id, approved FROM resume_job WHERE job = $1 ORDER BY created_at ASC",
)
.bind(last)
.fetch_all(&mut *tx)
.warn_after_seconds(3)
.await
?
.into_iter()
.collect::<Vec<_>>();
resume_messages.extend(resumes.iter().map(|r| to_raw_value(&r.value)));
approvers.extend(resumes.iter().map(|r| {
r.approver
.clone()
.as_deref()
.unwrap_or_else(|| "anonymous")
.to_string()
}));
// Persist approval user groups conditions, if any. Requires runnning the InputTransform
let required_events = suspend.required_events.unwrap() as u16;
let user_auth_required = suspend.user_auth_required.unwrap_or(false);
if user_auth_required {
let self_approval_disabled = suspend.self_approval_disabled.unwrap_or(false);
let mut user_groups_required: Vec<String> = Vec::new();
if suspend.user_groups_required.is_some() {
match suspend.user_groups_required.unwrap() {
InputTransform::Static { value } => {
user_groups_required = serde_json::from_str::<Vec<String>>(value.get())
.expect("Unable to deserialize group names");
}
InputTransform::Javascript { expr } => {
let mut context = HashMap::with_capacity(2);
context.insert("result".to_string(), arc_last_job_result.clone());
context
.insert("previous_result".to_string(), arc_last_job_result.clone());
let eval_result = serde_json::from_str::<Vec<String>>(
eval_timeout(
expr.to_string(),
context,
Some(arc_flow_job_args.clone()),
None,
None,
None
)
.warn_after_seconds(3)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"Error during isolated evaluation of expression `{expr}`:\n{e:#}"
))
})?
.get(),
);
if eval_result.is_ok() {
user_groups_required = eval_result.ok().unwrap_or(Vec::new())
} else {
let e = eval_result.err().unwrap();
return Err(Error::ExecutionErr(format!(
"Result returned by input transform invalid `{e:#}`"
)));
}
}
}
}
let approval_conditions = ApprovalConditions {
user_auth_required,
user_groups_required,
self_approval_disabled,
};
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['approval_conditions'], $1)
WHERE id = $2",
json!(approval_conditions),
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
}
let is_disapproved = resumes.iter().find(|x| !x.approved);
let can_be_resumed =
is_disapproved.is_none() && resume_messages.len() >= required_events as usize;
let disapproved_or_timeout_but_continue = !can_be_resumed
&& (is_disapproved.is_some()
|| !matches!(
&status_module,
FlowStatusModule::WaitingForPriorSteps { .. }
))
&& suspend.continue_on_disapprove_timeout.unwrap_or(false);
let audit_author = AuditAuthor {
username: flow_job
.permissioned_as
.trim_start_matches("u/")
.to_string(),
email: flow_job.permissioned_as_email.clone(),
username_override: None,
};
if can_be_resumed || disapproved_or_timeout_but_continue {
if disapproved_or_timeout_but_continue {
let js = if let Some(disapproved) = is_disapproved.as_ref() {
json!({"error": {"message": format!("Disapproved by {}", disapproved.approver.clone().unwrap_or_else( || "unknown".to_string())), "name": "SuspendedDisapproved"}})
} else {
json!({"error": {"message": "Timed out waiting to be resumed", "name": "SuspendedTimedOut"}})
};
resume_messages.push(to_raw_value(&js));
audit_log(
&mut *tx,
&audit_author,
"jobs.suspend_resume",
ActionKind::Update,
&flow_job.workspace_id,
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval but can continue".to_string()}).to_string()),
None,
)
.warn_after_seconds(3)
.await?;
}
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'approvers'], $2)
WHERE id = $3",
(status.step - 1).to_string(),
json!(resumes
.into_iter()
.map(|r| Approval {
resume_id: r.resume_id as u16,
approver: r
.approver.clone()
.unwrap_or_else(|| "unknown".to_string())
})
.collect::<Vec<_>>()
),
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
// Remove the approval conditions from the flow status
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = flow_status - 'approval_conditions'
WHERE id = $1",
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
/* continue on and run this job! */
tx.commit().warn_after_seconds(3).await?;
/* not enough messages to do this job, "park"/suspend until there are */
} else if matches!(
&status_module,
FlowStatusModule::WaitingForPriorSteps { .. }
) && is_disapproved.is_none()
{
sqlx::query!(
"WITH suspend AS (
UPDATE v2_job_queue SET suspend = $2, suspend_until = now() + $3
WHERE id = $4
RETURNING id
) UPDATE v2_job_status SET flow_status = JSONB_SET(
flow_status,
ARRAY['modules', flow_status->>'step'::TEXT],
$1
) WHERE id = (SELECT id FROM suspend)",
json!(FlowStatusModule::WaitingForEvents {
id: status_module.id(),
count: required_events,
job: last
}),
(required_events - resume_messages.len() as u16) as i32,
Duration::from_secs(
suspend.timeout.map(|t| t.into()).unwrap_or_else(|| 30 * 60)
) as Duration,
flow_job.id,
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
sqlx::query!(
"UPDATE v2_job_runtime SET ping = NULL
WHERE id = $1",
flow_job.id,
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
tx.commit().warn_after_seconds(3).await?;
return Ok(PushNextFlowJob::Done(None));
/* cancelled or we're WaitingForEvents but we don't have enough messages (timed out) */
} else {
if is_disapproved.is_none() {
audit_log(
&mut *tx,
&audit_author,
"jobs.suspend_resume",
ActionKind::Update,
&flow_job.workspace_id,
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval and is cancelled".to_string()}).to_string()),
None,
)
.warn_after_seconds(3)
.await?;
}
tx.commit().warn_after_seconds(3).await?;
let (logs, error_name) = if let Some(disapprover) = is_disapproved {
(
format!(
"Disapproved by {}",
disapprover
.approver
.clone()
.unwrap_or_else(|| "unknown".to_string())
),
"SuspendedDisapproved",
)
} else {
(
"Timed out waiting to be resumed".to_string(),
"SuspendedTimedOut",
)
};
let result: Value = json!({ "error": {"message": logs, "name": error_name}});
append_logs(
&flow_job.id,
&flow_job.workspace_id,
logs.clone(),
&db.into(),
)
.warn_after_seconds(3)
.await;
return Ok(PushNextFlowJob::Done(Some(UpdateFlow {
flow: flow_job.id,
success: false,
result: to_raw_value(&result),
stop_early_override: None,
w_id: flow_job.workspace_id.clone(),
worker_dir: worker_dir.to_string(),
token: client.token.clone(),
})));
}
}
}
let mut module = match step {
Step::Step(i) => flow
.modules
.get(i)
.with_context(|| format!("no module at index {}", i))?,
Step::PreprocessorStep => flow
.preprocessor_module
.as_ref()
.with_context(|| format!("no preprocessor module"))?,
Step::FailureStep => flow
.failure_module
.as_deref()
.with_context(|| format!("no failure module"))?,
};
let current_id = &module.id;
let mut previous_id = match step {
Step::Step(i) if i >= 1 => flow.modules.get(i - 1).map(|m| m.id.clone()).unwrap(),
_ => String::new(),
};
// calculate sleep if any
let mut scheduled_for_o = {
// avoid branchall sleeping on every iteration if sleep is on module prior
if !matches!(
&status_module,
FlowStatusModule::WaitingForPriorSteps { .. }
| FlowStatusModule::WaitingForEvents { .. }
) {
None
} else {
let sleep_input_transform = if let Step::Step(i) = step {
i.checked_sub(1)
.and_then(|i| flow.modules.get(i))
.and_then(|m| m.sleep.clone())
} else {
None
};
if let Some(it) = sleep_input_transform {
let json_value = match it {
InputTransform::Static { value } => Ok(value),
InputTransform::Javascript { expr } => {
let mut context = HashMap::with_capacity(2);
context.insert("result".to_string(), arc_last_job_result.clone());
context.insert("previous_result".to_string(), arc_last_job_result.clone());
serde_json::from_str(
eval_timeout(
expr.to_string(),
context,
Some(arc_flow_job_args.clone()),
None,
None,
None,
)
.warn_after_seconds(3)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"Error during isolated evaluation of expression `{expr}`:\n{e:#}"
))
})?
.get(),
)
}
};
match json_value.and_then(|x| serde_json::from_str::<serde_json::Value>(x.get())) {
Ok(serde_json::Value::Number(n)) => {
if n.is_f64() {
n.as_f64()
.map(|x: f64| from_now(Duration::from_millis((x * 1000.0) as u64)))
} else if n.is_u64() {
n.as_u64().map(|x: u64| from_now(Duration::from_secs(x)))
} else {
return Err(Error::ExecutionErr(format!(
"Expected an integer, found: {n}"
)));
}
}
Ok(x @ _) => Err(Error::ExecutionErr(format!(
"Expected an integer, found: {x:?}"
)))?,
Err(e) => Err(Error::ExecutionErr(format!(
"Expected a number value, had error instead: {e:?}",
)))?,
}
} else {
None
}
}
};
let retry = if matches!(&status_module, FlowStatusModule::Failure { .. },) {
let retry = &module.retry.clone().unwrap_or_default();
next_retry(retry, &status.retry)
} else {
None
};
let get_args_from_id = match &status_module {
FlowStatusModule::Failure { job, .. }
if retry.as_ref().is_some() || !module.continue_on_error.is_some_and(|x| x) =>
{
if let Some((fail_count, retry_in)) = retry {
tracing::debug!(
retry_in_seconds = retry_in.as_secs(),
fail_count = fail_count,
"retrying"
);
scheduled_for_o = Some(from_now(retry_in));
status.retry.failed_jobs.push(job.clone());
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(JSONB_SET(flow_status, ARRAY['retry'], $1), ARRAY['modules', $3::TEXT, 'failed_retries'], $4)
WHERE id = $2",
json!(RetryStatus { fail_count, ..status.retry.clone() }),
flow_job.id,
status.step.to_string(),
json!(status.retry.failed_jobs)
)
.execute(db)
.warn_after_seconds(2)
.await
.context("update flow retry")?;
status_module = FlowStatusModule::WaitingForPriorSteps { id: status_module.id() };
// we get the args from the last failed job
status.retry.failed_jobs.last()
/* Start the failure module ... */
} else {
/* push_next_flow_job is called with the current step on FlowStatusModule::Failure.
* This must update the step index to the end so that no subsequent steps are run after
* the failure module.
*
* The failure module may also run again if it fails and the retry feature is used.
* In that case, `i` will index past `flow.modules`. The above should handle that and
* re-run the failure module. */
step = Step::FailureStep;
previous_id = current_id.clone();
module = flow
.failure_module
.as_ref()
/* If this fails, it's a update_flow_status_after_job_completion shouldn't have called
* handle_flow to get here. */
.context("missing failure module")?;
status_module = status.failure_module.module_status.clone();
if module.retry.as_ref().is_some_and(|x| x.has_attempts()) {
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['retry'], $1)
WHERE id = $2",
json!(RetryStatus { fail_count: 0, failed_jobs: vec![] }),
flow_job.id
)
.execute(db)
.warn_after_seconds(3)
.await
.context("update flow retry")?;
};
None
}
}
_ => None,
};
let mut transform_context: Option<IdContext> = None;
let approvers = Arc::new(to_raw_value(&approvers));
let resume = Arc::new(to_raw_value(&resume_messages.last()));
let resumes = Arc::new(to_raw_value(&resume_messages));
drop(resume_messages);
let is_skipped = if let Some(skip_if) = &module.skip_if {
let idcontext = get_transform_context(&flow_job, previous_id.as_str(), &status)
.warn_after_seconds(3)
.await?;
compute_bool_from_expr(
&skip_if.expr,
arc_flow_job_args.clone(),
arc_last_job_result.clone(),
None,
Some(&idcontext),
Some(client),
Some((resumes.clone(), resume.clone(), approvers.clone())),
None,
)
.warn_after_seconds(3)
.await?
} else {
false
};
let args: windmill_common::error::Result<_> =
if module.mock.is_some() && module.mock.as_ref().unwrap().enabled {
let mut hm = HashMap::new();
hm.insert(
"previous_result".to_string(),
to_raw_value(
&module
.mock
.as_ref()
.unwrap()
.return_value
.clone()
.unwrap_or_else(|| serde_json::from_str("null").unwrap()),
),
);
Ok(Marc::new(hm))
} else if let Some(id) = get_args_from_id {
let args = sqlx::query_scalar!(
"SELECT args AS \"args: Json<HashMap<String, Box<RawValue>>>\"
FROM v2_job WHERE id = $1 AND workspace_id = $2",
id,
&flow_job.workspace_id
)
.fetch_optional(db)
.warn_after_seconds(3)
.await?;
if let Some(args) = args {
Ok(Marc::new(args.map(|x| x.0).unwrap_or_else(HashMap::new)))
} else {
Ok(Marc::new(HashMap::new()))
}
} else if matches!(step, Step::PreprocessorStep) {
let mut hm = (*arc_flow_job_args).clone();
hm.insert(
ENTRYPOINT_OVERRIDE.to_string(),
to_raw_value(&"preprocessor"),
);
Ok(Marc::new(hm))
} else {
let value = module.get_value();
match &value {
Ok(_) if matches!(value, Ok(FlowModuleValue::Identity)) || is_skipped => {
serde_json::from_str(
&serde_json::to_string(&PreviousResult {
previous_result: Some(&arc_last_job_result),
})
.unwrap(),
)
.map(Marc::new)
.map_err(|e| error::Error::internal_err(format!("identity: {e:#}")))
}
Ok(
FlowModuleValue::Script { input_transforms, .. }
| FlowModuleValue::RawScript { input_transforms, .. }
| FlowModuleValue::FlowScript { input_transforms, .. }
| FlowModuleValue::Flow { input_transforms, .. },
) => {
let ctx = get_transform_context(&flow_job, &previous_id, &status)
.warn_after_seconds(3)
.await?;
transform_context = Some(ctx);
let by_id = transform_context.as_ref().unwrap();
transform_input(
arc_flow_job_args.clone(),
arc_last_job_result.clone(),
input_transforms,
resumes.clone(),
resume.clone(),
approvers.clone(),
by_id,
client,
)
.warn_after_seconds(3)
.await
.map(Marc::new)
}
Ok(_) => Ok(arc_flow_job_args.clone()),
Err(e) => {
return Err(error::Error::internal_err(format!(
"module was not convertible to acceptable value {e:?}"
)))
}
}
};
tracing::debug!(id = %flow_job.id, root_id = %job_root, "flow job args computed");
let next_flow_transform = compute_next_flow_transform(
arc_flow_job_args.clone(),
arc_last_job_result.clone(),
&flow_job,
&flow,
transform_context,
db,
&module,
&status,
&status_module,
&previous_id,
client,
resumes.clone(),
resume.clone(),
approvers.clone(),
is_skipped,
)
.warn_after_seconds(3)
.await?;
tracing::info!(id = %flow_job.id, root_id = %job_root, "next flow transform computed");
let (job_payloads, next_status) = match next_flow_transform {
NextFlowTransform::Continue(job_payload, next_state) => (job_payload, next_state),
NextFlowTransform::EmptyInnerFlows { branch_chosen } => {
let raw_status = sqlx::query_scalar!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2)
WHERE id = $3
RETURNING flow_status AS \"flow_status: Json<Box<RawValue>>\"",
status.step.to_string(),
json!(FlowStatusModule::Success {
id: status_module.id(),
job: Uuid::nil(),
flow_jobs: Some(vec![]),
flow_jobs_success: Some(vec![]),
branch_chosen: branch_chosen,
approvers: vec![],
failed_retries: vec![],
skipped: false,
}),
flow_job.id
)
.fetch_optional(db)
.warn_after_seconds(3)
.await?
.flatten();
let status = raw_status
.as_ref()
.and_then(|v| serde_json::from_str::<FlowStatus>((**v).get()).ok());
if let Some(status) = status {
// // flow is reprocessed by the worker in a state where the module has completed successfully.
return Ok(PushNextFlowJob::Rec(PushNextFlowJobRec {
flow_job: flow_job,
status: status,
}));
} else {
return Err(Error::BadRequest(
"impossible to parse new flow status after applying inner flows".to_string(),
));
}
}
};
// Also check `flow_job.same_worker` for [`JobKind::Flow`] jobs as it's no
// more reflected to the flow value on push.
let job_same_worker = flow_job.same_worker
&& matches!(flow_job.kind, JobKind::Flow)
&& flow_job.runnable_id.is_some();
let continue_on_same_worker =
(flow.same_worker || job_same_worker) && module.suspend.is_none() && module.sleep.is_none();
/* Finally, push the job into the queue */
let mut uuids = vec![];
let job_payloads = match job_payloads {
ContinuePayload::SingleJob(payload) => vec![payload],
ContinuePayload::ParallelJobs(payloads) => payloads,
};
let len = job_payloads.len();
let mut tx = db.begin().warn_after_seconds(3).await?;
let nargs = args.as_ref();
for (i, payload_tag) in job_payloads.into_iter().enumerate() {
if i % 100 == 0 && i != 0 {
tracing::info!(id = %flow_job.id, root_id = %job_root, "pushed (non-commited yet) first {i} subflows of {len}");
sqlx::query!(
"UPDATE v2_job_runtime SET ping = now() WHERE id = $1 AND ping < now()",
flow_job.id,
)
.execute(db)
.warn_after_seconds(3)
.await?;
}
tracing::debug!(id = %flow_job.id, root_id = %job_root, "pushing job {i} of {len}");
// compute job-to-be-pushed priority
// The job definition itself might have its own priority, but as we're running
// it from a flow here, it inherits first the flow step priority and second the
// flow priority.
let new_job_priority_override = if module.priority.is_some() {
module.priority
} else if flow_job.priority.is_some() {
flow_job.priority
} else {
None
};
let marc;
let me;
let args = match &next_status {
NextStatus::AllFlowJobs {
branchall: Some(BranchAllStatus { .. }),
iterator: None,
..
} => nargs,
NextStatus::NextLoopIteration {
next: ForloopNextIteration { new_args, .. },
simple_input_transforms,
} => {
let mut args = if let Ok(args) = nargs {
args.as_ref().clone()
} else {
HashMap::new()
};
insert_iter_arg(&mut args, "iter".to_string(), to_raw_value(new_args));
args.insert("iter".to_string(), to_raw_value(new_args));
if let Some(input_transforms) = simple_input_transforms {
//previous id is none because we do not want to use previous id if we are in a for loop
let ctx = get_transform_context(&flow_job, "", &status)
.warn_after_seconds(3)
.await?;
let ti = transform_input(
Marc::new(args),
arc_last_job_result.clone(),
input_transforms,
resumes.clone(),
resume.clone(),
approvers.clone(),
&ctx,
client,
)
.warn_after_seconds(3)
.await
.map_err(|e| {
Error::ExecutionErr(
format!("could not transform input using an expr: {e}",),
)
})
.map(Marc::new);
if let Ok(ti) = ti {
marc = ti;
Ok(&marc)
} else {
me = ti.unwrap_err();
Err(&me)
}
} else {
marc = Marc::new(args);
Ok(&marc)
}
}
NextStatus::AllFlowJobs {
branchall: None,
iterator: Some(FlowIterator { itered, .. }),
simple_input_transforms,
} => {
if let Ok(args) = args.as_ref() {
let mut hm = HashMap::new();
for (k, v) in args.iter() {
hm.insert(k.to_string(), v.to_owned());
}
insert_iter_arg(
&mut hm,
"iter".to_string(),
to_raw_value(&json!({ "index": i as i32, "value": itered[i]})),
);
if let Some(input_transforms) = simple_input_transforms {
let ctx = get_transform_context(&flow_job, &previous_id, &status)
.warn_after_seconds(3)
.await?;
let ti = transform_input(
Marc::new(hm),
arc_last_job_result.clone(),
input_transforms,
resumes.clone(),
resume.clone(),
approvers.clone(),
&ctx,
client,
)
.warn_after_seconds(3)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"could not transform input using an expr: {e}"
))
})
.map(Marc::new);
if let Ok(ti) = ti {
marc = ti;
Ok(&marc)
} else {
me = ti.unwrap_err();
Err(&me)
}
} else {
marc = Marc::new(hm);
Ok(&marc)
}
} else {
nargs
}
}
_ => nargs,
};
let push_args;
let err;
let ov;
match args {
Ok(v) => {
ov = v;
push_args = PushArgs::from(ov.as_ref());
err = None;
}
Err(e) => {
push_args = PushArgs::from(&*EHM);
err = Some(e);
}
};
tracing::debug!(id = %flow_job.id, root_id = %job_root, "computed args for job {i} of {len}");
let value_with_parallel = module.get_value_with_parallel()?;
let root_job = if {
value_with_parallel.type_ == "flow"
|| (value_with_parallel.type_ == "forloopflow"
&& value_with_parallel.parallel.is_some_and(|x| x))
} {
None
} else {
flow_job
.flow_innermost_root_job
.or_else(|| Some(flow_job.id))
};
// forward root job permissions to the new job
let job_perms: Option<Authed> = {
if let Some(root_job) = &flow_job
.flow_innermost_root_job
.or_else(|| Some(flow_job.id))
{
sqlx::query_as!(
JobPerms,
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
root_job,
flow_job.workspace_id,
)
.fetch_optional(&mut *tx)
.warn_after_seconds(3)
.await?
.map(|x| x.into())
} else {
None
}
};
tracing::debug!(id = %flow_job.id, root_id = %job_root, "computed perms for job {i} of {len}");
let tag = if !matches!(step, Step::PreprocessorStep)
&& (flow_job.tag == "flow" || flow_job.tag == format!("flow-{}", flow_job.workspace_id))
{
payload_tag.tag.clone()
} else {
Some(flow_job.tag.clone())
};
let (email, permissioned_as) = if let Some(on_behalf_of) = payload_tag.on_behalf_of.as_ref()
{
(&on_behalf_of.email, on_behalf_of.permissioned_as.clone())
} else {
(
&flow_job.permissioned_as_email,
flow_job.permissioned_as.to_owned(),
)
};
let tx2 = PushIsolationLevel::Transaction(tx);
let (uuid, mut inner_tx) = push(
&db,
tx2,
&flow_job.workspace_id,
payload_tag.payload.clone(),
push_args,
&flow_job.created_by,
email,
permissioned_as,
scheduled_for_o,
flow_job.schedule_path(),
Some(flow_job.id),
root_job,
None,
true,
continue_on_same_worker,
err,
flow_job.visible_to_owner,
tag,
payload_tag.timeout,
Some(module.id.clone()),
new_job_priority_override,
job_perms.as_ref(),
)
.warn_after_seconds(2)
.await?;
if continue_on_same_worker {
let _ = sqlx::query!(
"UPDATE v2_job_queue SET worker = $2 WHERE id = $1",
uuid,
worker_name
)
.execute(&mut *inner_tx)
.warn_after_seconds(3)
.await;
}
tracing::debug!(id = %flow_job.id, root_id = %job_root, "pushed next flow job: {uuid}");
if value_with_parallel.type_ == "forloopflow" {
if let Some(p) = value_with_parallel.parallelism {
tracing::debug!(id = %flow_job.id, root_id = %job_root, "updating suspend for forloopflow job {uuid}");
if i as u16 >= p {
sqlx::query!(
"UPDATE v2_job_queue SET
suspend = $1,
suspend_until = now() + interval '14 day',
running = true
WHERE id = $2",
(i as u16 - p + 1) as i32,
uuid,
)
.execute(&mut *inner_tx)
.warn_after_seconds(3)
.await?;
}
tracing::debug!(id = %flow_job.id, root_id = %job_root, "updated suspend for {uuid}");
}
}
if payload_tag.delete_after_use {
let uuid_singleton_json = serde_json::to_value(&[uuid]).map_err(|e| {
error::Error::internal_err(format!("Unable to serialize uuid: {e:#}"))
})?;
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = JSONB_SET(flow_status, ARRAY['cleanup_module', 'flow_jobs_to_clean'], COALESCE(flow_status->'cleanup_module'->'flow_jobs_to_clean', '[]'::jsonb) || $1)
WHERE id = $2",
uuid_singleton_json,
root_job.unwrap_or(flow_job.id)
)
.execute(&mut *inner_tx)
.warn_after_seconds(3)
.await?;
}
tx = inner_tx;
uuids.push(uuid);
}
let is_one_uuid = uuids.len() == 1;
let one_uuid = if is_one_uuid {
Ok(uuids[0].clone())
} else {
Err(Error::BadRequest("Expected only one uuid".to_string()))
};
if !is_one_uuid {
for uuid in &uuids {
sqlx::query!(
"INSERT INTO parallel_monitor_lock (parent_flow_id, job_id)
VALUES ($1, $2)",
flow_job.id,
uuid
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
tracing::debug!(id = %flow_job.id, root_id = %job_root, "updated parallel monitor lock for {uuid}");
}
}
let first_uuid = uuids[0];
let new_status = match next_status {
NextStatus::NextLoopIteration {
next:
ForloopNextIteration {
index,
itered,
mut flow_jobs,
while_loop,
mut flow_jobs_success,
..
},
..
} => {
let uuid = one_uuid?;
flow_jobs.push(uuid);
if let Some(flow_jobs_success) = &mut flow_jobs_success {
flow_jobs_success.push(None);
}
FlowStatusModule::InProgress {
job: uuid,
iterator: Some(FlowIterator { index, itered }),
flow_jobs: Some(flow_jobs),
flow_jobs_success,
branch_chosen: None,
branchall: None,
id: status_module.id(),
parallel: false,
while_loop,
progress: None,
}
}
NextStatus::AllFlowJobs { iterator, branchall, .. } => FlowStatusModule::InProgress {
job: flow_job.id,
iterator,
flow_jobs_success: Some(vec![None; uuids.len()]),
flow_jobs: Some(uuids.clone()),
branch_chosen: None,
branchall,
id: status_module.id(),
parallel: true,
while_loop: false,
progress: None,
},
NextStatus::NextBranchStep(NextBranch {
mut flow_jobs,
status,
mut flow_jobs_success,
..
}) => {
let uuid = one_uuid?;
flow_jobs.push(uuid);
if let Some(flow_jobs_success) = &mut flow_jobs_success {
flow_jobs_success.push(None);
}
FlowStatusModule::InProgress {
job: uuid,
iterator: None,
flow_jobs: Some(flow_jobs),
flow_jobs_success,
branch_chosen: None,
branchall: Some(status),
id: status_module.id(),
parallel: false,
while_loop: false,
progress: None,
}
}
NextStatus::BranchChosen(branch) => FlowStatusModule::InProgress {
job: one_uuid?,
iterator: None,
flow_jobs: None,
flow_jobs_success: None,
branch_chosen: Some(branch),
branchall: None,
id: status_module.id(),
parallel: false,
while_loop: false,
progress: None,
},
NextStatus::NextStep => {
FlowStatusModule::WaitingForExecutor { id: status_module.id(), job: one_uuid? }
}
};
tracing::debug!("STATUS STEP: {:?} {step:?} {:#?}", status.step, new_status);
match step {
Step::FailureStep => {
sqlx::query!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
JSONB_SET(flow_status, ARRAY['failure_module'], $1),
ARRAY['step'],
$2
)
WHERE id = $3",
json!(FlowStatusModuleWParent {
parent_module: Some(current_id.clone()),
module_status: new_status
}),
json!(flow.modules.len()),
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
}
Step::PreprocessorStep => {
sqlx::query!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
JSONB_SET(flow_status, ARRAY['preprocessor_module'], $1),
ARRAY['step'],
$2
)
WHERE id = $3",
json!(new_status),
json!(-1),
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
}
Step::Step(i) => {
sqlx::query!(
"UPDATE v2_job_status SET
flow_status = JSONB_SET(
JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2),
ARRAY['step'],
$3
)
WHERE id = $4",
i as i32,
json!(new_status),
json!(i),
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
}
};
potentially_crash_for_testing();
sqlx::query!(
"UPDATE v2_job_runtime SET ping = null WHERE id = $1",
flow_job.id
)
.execute(&mut *tx)
.warn_after_seconds(3)
.await?;
if continue_on_same_worker {
if !is_one_uuid {
return Err(Error::BadRequest(
"Cannot continue on same worker with multiple jobs, parallel cannot be used in conjunction with same_worker".to_string(),
));
}
}
tx.commit().warn_after_seconds(3).await?;
tracing::info!(id = %flow_job.id, root_id = %job_root, "all next flow jobs pushed: {uuids:?}");
if continue_on_same_worker {
same_worker_tx
.send(SameWorkerPayload { job_id: first_uuid, recoverable: true })
.warn_after_seconds(3)
.await
.map_err(to_anyhow)?;
}
return Ok(PushNextFlowJob::Done(None));
}
// async fn jump_to_next_step(
// status_step: i32,
// i: usize,
// job_id: &Uuid,
// flow: FlowValue,
// db: &DB,
// client: &windmill_api_client::Client,
// status_module: FlowStatusModule,
// last_result: serde_json::Value,
// same_worker_tx: Sender<Uuid>,
// base_internal_url: &str,
// ) -> error::Result<()> {
// let mut tx = db.begin().await?;
// let next_step = i
// .checked_add(1)
// .filter(|i| (..flow.modules.len()).contains(i));
// let new_job = sqlx::query_as::<_, QueuedJob>(
// r#"
// UPDATE queue
// SET flow_status = JSONB_SET(
// JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2),
// ARRAY['step'], $3)
// WHERE id = $4
// RETURNING *
// "#,
// )
// .bind(status_step)
// .bind(json!(status_module))
// .bind(json!(next_step.unwrap_or(i)))
// .bind(job_id)
// .fetch_one(&mut *tx)
// .await?;
// tx.commit().await?;
// let new_status = new_job.parse_flow_status().ok_or_else(|| {
// Error::ExecutionErr("Impossible to parse new status after jump".to_string())
// })?;
// if next_step.is_some() {
// tracing::debug!("Jumping to next step with flow {flow:#?}");
// return push_next_flow_job(
// &new_job,
// new_status,
// flow,
// db,
// client,
// last_result,
// same_worker_tx,
// base_internal_url,
// )
// .await;
// } else {
// let success = true;
// let skipped = false;
// let logs = "Forloop completed without iteration".to_string();
// let _uuid =
// add_completed_job(db, client, &new_job, success, skipped, json!([]), logs).await?;
// return Ok(());
// }
// }
/// Some state about the current/last forloop FlowStatusModule used to initialized the next
/// iteration's FlowStatusModule after pushing a job
#[derive(Debug)]
struct ForloopNextIteration {
index: usize,
itered: Vec<Box<RawValue>>,
flow_jobs: Vec<Uuid>,
flow_jobs_success: Option<Vec<Option<bool>>>,
new_args: Iter,
while_loop: bool,
}
enum ForLoopStatus {
ParallelIteration { itered: Vec<Box<RawValue>> },
NextIteration(ForloopNextIteration),
EmptyIterator,
}
#[derive(Debug)]
struct NextBranch {
status: BranchAllStatus,
flow_jobs: Vec<Uuid>,
flow_jobs_success: Option<Vec<Option<bool>>>,
}
#[derive(Debug)]
enum NextStatus {
NextStep,
BranchChosen(BranchChosen),
NextBranchStep(NextBranch),
NextLoopIteration {
next: ForloopNextIteration,
simple_input_transforms: Option<HashMap<String, InputTransform>>,
},
AllFlowJobs {
branchall: Option<BranchAllStatus>,
iterator: Option<FlowIterator>,
simple_input_transforms: Option<HashMap<String, InputTransform>>,
},
}
#[derive(Clone)]
struct JobPayloadWithTag {
payload: JobPayload,
tag: Option<String>,
delete_after_use: bool,
timeout: Option<i32>,
on_behalf_of: Option<OnBehalfOf>,
}
enum ContinuePayload {
SingleJob(JobPayloadWithTag),
ParallelJobs(Vec<JobPayloadWithTag>),
}
enum NextFlowTransform {
EmptyInnerFlows { branch_chosen: Option<BranchChosen> },
Continue(ContinuePayload, NextStatus),
}
fn insert_iter_arg(
args: &mut HashMap<String, Box<RawValue>>,
desired_key: String,
value: Box<RawValue>,
) {
let conflicting_parent_maybe = args.get(desired_key.as_str());
if let Some(conflicting_parent) = conflicting_parent_maybe {
// we change the key of the parent to {desired_key}_parent.
// we do it recursively b/c it's possible there's another conflict
let conflicting_parent_new_key = format!("{}_parent", desired_key.as_str());
insert_iter_arg(
args,
conflicting_parent_new_key,
conflicting_parent.to_owned(),
);
}
args.insert(desired_key, value);
}
fn payload_from_modules<'a>(
mut modules: Vec<FlowModule>,
modules_node: Option<FlowNodeId>,
failure_module: Option<&Box<FlowModule>>,
same_worker: bool,
id: impl FnOnce() -> String,
path: impl FnOnce() -> String,
opt_empty_inner_flows: bool,
) -> Option<JobPayload> {
if opt_empty_inner_flows && modules.is_empty() && modules_node.is_none() {
return None;
}
if let Some(id) = modules_node {
return Some(JobPayload::FlowNode { id, path: path() });
}
add_virtual_items_if_necessary(&mut modules);
let mut failure_module = failure_module.cloned();
if let Some(failure_module) = failure_module.as_mut() {
failure_module.id_append(&id());
}
Some(JobPayload::RawFlow {
value: FlowValue { modules, failure_module, same_worker, ..Default::default() },
path: Some(path()),
restarted_from: None,
})
}
fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String {
if status
.preprocessor_module
.as_ref()
.is_some_and(|x| x.id() == module.id)
{
format!("{}/preprocessor", flow_job.runnable_path())
} else {
format!("{}/{}", flow_job.runnable_path(), module.id)
}
}
async fn compute_next_flow_transform(
arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>>,
arc_last_job_result: Arc<Box<RawValue>>,
flow_job: &MiniPulledJob,
flow: &FlowValue,
by_id: Option<IdContext>,
db: &DB,
module: &FlowModule,
status: &FlowStatus,
status_module: &FlowStatusModule,
previous_id: &str,
client: &AuthedClient,
resumes: Arc<Box<RawValue>>,
resume: Arc<Box<RawValue>>,
approvers: Arc<Box<RawValue>>,
is_skipped: bool,
) -> error::Result<NextFlowTransform> {
if module.mock.is_some() && module.mock.as_ref().unwrap().enabled {
return Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::Identity,
tag: None,
delete_after_use: false,
timeout: None,
on_behalf_of: None,
}),
NextStatus::NextStep,
));
}
let trivial_next_job = |payload| {
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload,
tag: None,
delete_after_use: false,
timeout: None,
on_behalf_of: None,
}),
NextStatus::NextStep,
))
};
let delete_after_use = module.delete_after_use.unwrap_or(false);
tracing::debug!(id = %flow_job.id, "computing next flow transform for {:?}", &module.value);
if is_skipped {
return trivial_next_job(JobPayload::Identity);
}
match module.get_value()? {
FlowModuleValue::Identity => trivial_next_job(JobPayload::Identity),
FlowModuleValue::Flow { path, .. } => {
let payload =
flow_to_payload(path, delete_after_use, &flow_job.workspace_id, db).await?;
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(payload),
NextStatus::NextStep,
))
}
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
let payload =
script_to_payload(script_hash, script_path, db, flow_job, module, tag_override)
.await?;
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(payload),
NextStatus::NextStep,
))
}
FlowModuleValue::RawScript {
path,
content,
language,
lock,
tag,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
..
} => {
let path = path.unwrap_or_else(|| get_path(flow_job, status, module));
let payload = raw_script_to_payload(
path,
content,
language,
lock,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
module,
tag,
delete_after_use,
);
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(payload),
NextStatus::NextStep,
))
}
FlowModuleValue::FlowScript {
id, // flow_node(id).
tag,
language,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
..
} => {
let path = get_path(flow_job, status, module);
let payload = JobPayloadWithTag {
payload: JobPayload::FlowScript {
id,
language,
custom_concurrency_key: custom_concurrency_key.clone(),
concurrent_limit,
concurrency_time_window_s,
cache_ttl: module.cache_ttl.map(|x| x as i32),
dedicated_worker: None,
path,
},
tag: tag.clone(),
delete_after_use,
timeout: module.timeout,
on_behalf_of: None,
};
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(payload),
NextStatus::NextStep,
))
}
FlowModuleValue::WhileloopFlow { modules, modules_node, .. } => {
// if it's a simple single step flow, we will collapse it as an optimization and need to pass flow_input as an arg
let is_simple = is_simple_modules(&modules, flow.failure_module.as_ref());
let (flow_jobs, flow_jobs_success) = match status_module {
FlowStatusModule::InProgress {
flow_jobs: Some(flow_jobs),
flow_jobs_success,
..
} => (flow_jobs.clone(), flow_jobs_success.clone()),
_ => (vec![], Some(vec![])),
};
let next_loop_idx = flow_jobs.len();
next_loop_iteration(
flow,
status,
ForloopNextIteration {
index: next_loop_idx,
itered: vec![],
flow_jobs: flow_jobs,
flow_jobs_success: flow_jobs_success,
new_args: Iter {
index: next_loop_idx as i32,
value: windmill_common::worker::to_raw_value(&next_loop_idx),
},
while_loop: true,
},
modules,
modules_node,
flow_job,
is_simple,
db,
module,
delete_after_use,
)
.await
}
/* forloop modules are expected set `iter: { value: Value, index: usize }` as job arguments */
FlowModuleValue::ForloopFlow { modules, modules_node, iterator, parallel, .. } => {
// if it's a simple single step flow, we will collapse it as an optimization and need to pass flow_input as an arg
let is_simple = !matches!(flow_job.kind, JobKind::FlowPreview)
&& !parallel
&& is_simple_modules(&modules, flow.failure_module.as_ref());
// if is_simple {
// match value {
// FlowModuleValue::Script { input_transforms, .. }
// | FlowModuleValue::RawScript { input_transforms, .. }
// | FlowModuleValue::FlowScript { input_transforms, .. }
// | FlowModuleValue::Flow { input_transforms, .. } => {
// Some(input_transforms.clone())
// }
// _ => None,
// }
let next_loop_status = next_forloop_status(
status_module,
by_id,
flow_job,
previous_id,
status,
&iterator,
arc_last_job_result,
resumes,
resume,
approvers,
arc_flow_job_args,
client,
&parallel,
)
.await?;
match next_loop_status {
ForLoopStatus::EmptyIterator => {
Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: None })
}
ForLoopStatus::NextIteration(ns) => {
next_loop_iteration(
flow,
status,
ns,
modules,
modules_node,
flow_job,
is_simple,
db,
module,
delete_after_use,
)
.await
}
ForLoopStatus::ParallelIteration { itered, .. } => {
// let inner_path = Some(format!("{}/loop-parallel", flow_job.script_path(),));
// let value = &modules[0].get_value()?;
// we removed the is_simple_case
// if is_simple {
// let payload =
// payload_from_simple_module(value, db, flow_job, module, inner_path)
// .await?;
// ContinuePayload::ForloopJobs { n: itered.len(), payload: payload }
// } else {
let payloads = (0..itered.len())
.into_iter()
.filter_map(|i| {
let Some(payload) = payload_from_modules(
modules.clone(),
modules_node,
flow.failure_module.as_ref(),
flow.same_worker,
|| format!("{}-{i}", status.step),
|| format!("{}/forloop-{i}", flow_job.runnable_path()),
true,
) else {
return None;
};
Some(JobPayloadWithTag {
payload,
tag: None,
delete_after_use,
timeout: None,
on_behalf_of: None,
})
})
.collect::<Vec<_>>();
if payloads.is_empty() {
return Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: None });
}
Ok(NextFlowTransform::Continue(
ContinuePayload::ParallelJobs(payloads),
NextStatus::AllFlowJobs {
branchall: None,
iterator: Some(FlowIterator { index: 0, itered }),
// we removed the is_simple_case for simple_input_transforms
// if is_simple {
// match value {
// FlowModuleValue::Script { input_transforms, .. }
// | FlowModuleValue::RawScript { input_transforms, .. }
// | FlowModuleValue::FlowScript { input_transforms, .. }
// | FlowModuleValue::Flow { input_transforms, .. } => {
// Some(input_transforms.clone())
// }
// _ => None,
// }
simple_input_transforms: None,
},
))
}
}
}
FlowModuleValue::BranchOne { branches, default, default_node } => {
let branch = match status_module {
FlowStatusModule::WaitingForPriorSteps { .. }
| FlowStatusModule::WaitingForEvents { .. }
| FlowStatusModule::WaitingForExecutor { .. } => {
let mut branch_chosen = BranchChosen::Default;
let idcontext = get_transform_context(&flow_job, previous_id, &status).await?;
for (i, b) in branches.iter().enumerate() {
let pred = compute_bool_from_expr(
&b.expr,
arc_flow_job_args.clone(),
arc_last_job_result.clone(),
None,
Some(&idcontext),
Some(client),
Some((resumes.clone(), resume.clone(), approvers.clone())),
None,
)
.await?;
if pred {
branch_chosen = BranchChosen::Branch { branch: i };
break;
}
}
branch_chosen
}
_ => Err(Error::BadRequest(format!(
"Unrecognized module status for BranchOne {status_module:?}"
)))?,
};
let (modules, modules_node, branch_idx) = match branch {
BranchChosen::Default => (default, default_node, 0),
BranchChosen::Branch { branch } => branches
.into_iter()
.nth(branch)
.map(|Branch { modules, modules_node, .. }| (modules, modules_node, branch + 1))
.ok_or_else(|| {
Error::BadRequest(format!(
"Unrecognized branch for BranchOne {status_module:?}"
))
})?,
};
let Some(payload) = payload_from_modules(
modules,
modules_node,
flow.failure_module.as_ref(),
flow.same_worker,
|| status.step.to_string(),
|| format!("{}/branchone-{}", flow_job.runnable_path(), branch_idx),
true,
) else {
return Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: Some(branch) });
};
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload,
tag: None,
delete_after_use,
timeout: None,
on_behalf_of: None,
}),
NextStatus::BranchChosen(branch),
))
}
FlowModuleValue::BranchAll { branches, parallel, .. } => {
let (branch_status, flow_jobs, flow_jobs_success) = match status_module {
FlowStatusModule::WaitingForPriorSteps { .. }
| FlowStatusModule::WaitingForEvents { .. }
| FlowStatusModule::WaitingForExecutor { .. } => {
if branches.is_empty() {
return Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: None });
} else if parallel {
let len = branches.len();
let payloads: Vec<JobPayloadWithTag> = branches
.into_iter()
.enumerate()
.filter_map(|(i, Branch { modules, modules_node, .. })| {
let Some(payload) = payload_from_modules(
modules,
modules_node,
flow.failure_module.as_ref(),
flow.same_worker,
|| format!("{}-{i}", status.step),
|| format!("{}/branchall-{}", flow_job.runnable_path(), i),
false,
) else {
return None;
};
Some(JobPayloadWithTag {
payload,
tag: None,
delete_after_use,
timeout: None,
on_behalf_of: None,
})
})
.collect::<Vec<_>>();
if payloads.is_empty() {
return Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: None });
}
return Ok(NextFlowTransform::Continue(
ContinuePayload::ParallelJobs(payloads),
NextStatus::AllFlowJobs {
branchall: Some(BranchAllStatus { branch: 0, len }),
iterator: None,
simple_input_transforms: None,
},
));
} else {
(
BranchAllStatus { branch: 0, len: branches.len() },
vec![],
Some(vec![]),
)
}
}
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, len }),
flow_jobs: Some(flow_jobs),
flow_jobs_success,
..
} if !parallel => (
BranchAllStatus { branch: branch + 1, len: len.clone() },
flow_jobs.clone(),
flow_jobs_success.clone(),
),
_ => Err(Error::BadRequest(format!(
"Unrecognized module status for BranchAll {status_module:?}"
)))?,
};
let Branch { modules, modules_node, .. } = branches
.into_iter()
.nth(branch_status.branch)
.ok_or_else(|| {
Error::BadRequest(format!(
"Unrecognized branch for BranchAll {status_module:?}"
))
})?;
let Some(payload) = payload_from_modules(
modules,
modules_node,
flow.failure_module.as_ref(),
flow.same_worker,
|| format!("{}-{}", status.step, branch_status.branch),
|| {
format!(
"{}/branchall-{}",
flow_job.runnable_path(),
branch_status.branch
)
},
false,
) else {
return Ok(NextFlowTransform::EmptyInnerFlows {
branch_chosen: Some(BranchChosen::Default),
});
};
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload,
tag: None,
delete_after_use,
timeout: None,
on_behalf_of: None,
}),
NextStatus::NextBranchStep(NextBranch {
status: branch_status,
flow_jobs,
flow_jobs_success,
}),
))
}
}
}
async fn next_loop_iteration(
flow: &FlowValue,
status: &FlowStatus,
ns: ForloopNextIteration,
modules: Vec<FlowModule>,
modules_node: Option<FlowNodeId>,
flow_job: &MiniPulledJob,
is_simple: bool,
db: &sqlx::Pool<sqlx::Postgres>,
module: &FlowModule,
delete_after_use: bool,
) -> Result<NextFlowTransform, Error> {
let inner_path = || format!("{}/loop-{}", flow_job.runnable_path(), ns.index);
if is_simple {
let mut value = modules[0].get_value()?;
let simple_input_transforms = match &mut value {
FlowModuleValue::Script { input_transforms, .. }
| FlowModuleValue::RawScript { input_transforms, .. }
| FlowModuleValue::FlowScript { input_transforms, .. }
| FlowModuleValue::Flow { input_transforms, .. } => {
Some(std::mem::take(input_transforms))
}
_ => None,
};
return Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(
payload_from_simple_module(value, db, flow_job, module, inner_path()).await?,
),
NextStatus::NextLoopIteration { next: ns, simple_input_transforms },
));
}
let Some(payload) = payload_from_modules(
modules,
modules_node,
flow.failure_module.as_ref(),
flow.same_worker,
|| format!("{}-{}", status.step, ns.index),
inner_path,
true,
) else {
return Ok(NextFlowTransform::EmptyInnerFlows { branch_chosen: None });
};
Ok(NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload,
tag: None,
delete_after_use,
timeout: None,
on_behalf_of: None,
}),
NextStatus::NextLoopIteration { next: ns, simple_input_transforms: None },
))
}
pub(super) fn is_simple_modules(
modules: &Vec<FlowModule>,
failure_module: Option<&Box<FlowModule>>,
) -> bool {
let is_simple = modules.len() == 1
&& modules[0].is_simple()
&& modules[0].sleep.is_none()
&& modules[0].suspend.is_none()
&& modules[0].cache_ttl.is_none()
&& modules[0].retry.is_none()
&& modules[0].stop_after_if.is_none()
&& modules[0].stop_after_all_iters_if.is_none()
&& modules[0].skip_if.is_none()
&& (modules[0].mock.is_none() || modules[0].mock.as_ref().is_some_and(|m| !m.enabled))
&& failure_module.is_none();
is_simple
}
async fn next_forloop_status(
status_module: &FlowStatusModule,
by_id: Option<IdContext>,
flow_job: &MiniPulledJob,
previous_id: &str,
status: &FlowStatus,
iterator: &InputTransform,
arc_last_job_result: Arc<Box<RawValue>>,
resumes: Arc<Box<RawValue>>,
resume: Arc<Box<RawValue>>,
approvers: Arc<Box<RawValue>>,
arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>>,
client: &AuthedClient,
parallel: &bool,
) -> Result<ForLoopStatus, Error> {
let next_loop_status = match status_module {
FlowStatusModule::WaitingForPriorSteps { .. }
| FlowStatusModule::WaitingForEvents { .. }
| FlowStatusModule::WaitingForExecutor { .. } => {
let by_id = if let Some(x) = by_id {
x
} else {
get_transform_context(&flow_job, previous_id, &status).await?
};
/* Iterator is an InputTransform, evaluate it into an array. */
let itered_raw = match iterator {
InputTransform::Static { value } => to_raw_value(value),
InputTransform::Javascript { expr } => {
let mut context = HashMap::with_capacity(5);
context.insert("result".to_string(), arc_last_job_result.clone());
context.insert("previous_result".to_string(), arc_last_job_result);
context.insert("resumes".to_string(), resumes);
context.insert("resume".to_string(), resume);
context.insert("approvers".to_string(), approvers);
eval_timeout(
expr.to_string(),
context,
Some(arc_flow_job_args),
Some(client),
Some(&by_id),
None,
)
.await?
}
};
let itered = serde_json::from_str::<Vec<Box<RawValue>>>(itered_raw.get()).map_err(
|not_array| {
Error::ExecutionErr(format!(
"Expected an array value in the iterator expression, found: {not_array}"
))
},
)?;
if itered.is_empty() {
ForLoopStatus::EmptyIterator
} else if *parallel {
ForLoopStatus::ParallelIteration { itered }
} else if let Some(first) = itered.first() {
let iter = Iter { index: 0 as i32, value: first.to_owned() };
ForLoopStatus::NextIteration(ForloopNextIteration {
index: 0,
itered,
flow_jobs: vec![],
flow_jobs_success: Some(vec![]),
new_args: iter,
while_loop: false,
})
} else {
panic!("itered cannot be empty")
}
}
FlowStatusModule::InProgress {
iterator: Some(FlowIterator { itered, index }),
flow_jobs: Some(flow_jobs),
flow_jobs_success,
..
} if !*parallel => {
let itered_new = if itered.is_empty() {
// it's possible we need to re-compute the iterator Input Transforms here, in particular if the flow is being restarted inside the loop
let by_id = if let Some(x) = by_id {
x
} else {
get_transform_context(&flow_job, previous_id, &status).await?
};
let itered_raw = match iterator {
InputTransform::Static { value } => to_raw_value(value),
InputTransform::Javascript { expr } => {
let mut context = HashMap::with_capacity(5);
context.insert("result".to_string(), arc_last_job_result.clone());
context.insert("previous_result".to_string(), arc_last_job_result);
context.insert("resumes".to_string(), resumes);
context.insert("resume".to_string(), resume);
context.insert("approvers".to_string(), approvers);
eval_timeout(
expr.to_string(),
context,
Some(arc_flow_job_args),
Some(client),
Some(&by_id),
None,
)
.await?
}
};
serde_json::from_str::<Vec<Box<RawValue>>>(itered_raw.get()).map_err(
|not_array| {
Error::ExecutionErr(format!("Expected an array value, found: {not_array}"))
},
)?
} else {
itered.clone()
};
let (index, next) = index
.checked_add(1)
.and_then(|i| itered_new.get(i).map(|next| (i, next)))
.with_context(|| {
format!("Could not find iteration number {index} restarting inside the for-loop flow. It's possible the itered-array has changed and this value isn't available anymore.")
})?;
ForLoopStatus::NextIteration(ForloopNextIteration {
index,
itered: itered_new.clone(),
flow_jobs: flow_jobs.clone(),
flow_jobs_success: flow_jobs_success.clone(),
new_args: Iter { index: index as i32, value: next.to_owned() },
while_loop: false,
})
}
_ => Err(Error::BadRequest(format!(
"Unrecognized module status for ForloopFlow {status_module:?}"
)))?,
};
Ok(next_loop_status)
}
async fn payload_from_simple_module(
value: FlowModuleValue,
db: &sqlx::Pool<sqlx::Postgres>,
flow_job: &MiniPulledJob,
module: &FlowModule,
inner_path: String,
) -> Result<JobPayloadWithTag, Error> {
let delete_after_use = module.delete_after_use.unwrap_or(false);
Ok(match value {
FlowModuleValue::Flow { path, .. } => {
flow_to_payload(path, delete_after_use, &flow_job.workspace_id, db).await?
}
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
script_to_payload(script_hash, script_path, db, flow_job, module, tag_override).await?
}
FlowModuleValue::RawScript {
path,
content,
language,
lock,
tag,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
..
} => raw_script_to_payload(
path.unwrap_or_else(|| inner_path),
content,
language,
lock,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
module,
tag,
delete_after_use,
),
FlowModuleValue::FlowScript {
id, // flow_node(id).
tag,
language,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
..
} => JobPayloadWithTag {
payload: JobPayload::FlowScript {
id,
language,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
cache_ttl: module.cache_ttl.map(|x| x as i32),
dedicated_worker: None,
path: inner_path,
},
tag,
delete_after_use,
timeout: module.timeout,
on_behalf_of: None,
},
_ => unreachable!("is simple flow"),
})
}
fn raw_script_to_payload(
path: String,
content: String,
language: windmill_common::scripts::ScriptLang,
lock: Option<String>,
custom_concurrency_key: Option<String>,
concurrent_limit: Option<i32>,
concurrency_time_window_s: Option<i32>,
module: &FlowModule,
tag: Option<String>,
delete_after_use: bool,
) -> JobPayloadWithTag {
JobPayloadWithTag {
payload: JobPayload::Code(RawCode {
hash: None,
path: Some(path),
content,
language,
lock,
custom_concurrency_key,
concurrent_limit,
concurrency_time_window_s,
cache_ttl: module.cache_ttl.map(|x| x as i32),
dedicated_worker: None,
}),
tag,
delete_after_use,
timeout: module.timeout,
on_behalf_of: None,
}
}
async fn flow_to_payload(
path: String,
delete_after_use: bool,
w_id: &str,
db: &DB,
) -> Result<JobPayloadWithTag, Error> {
let FlowVersionInfo { version, on_behalf_of_email, edited_by, tag, .. } =
get_latest_flow_version_info_for_path(db, w_id, &path, true).await?;
let on_behalf_of = if let Some(email) = on_behalf_of_email {
Some(OnBehalfOf { email, permissioned_as: username_to_permissioned_as(&edited_by) })
} else {
None
};
let payload =
JobPayload::Flow { path, dedicated_worker: None, apply_preprocessor: false, version };
Ok(JobPayloadWithTag { payload, tag, delete_after_use, timeout: None, on_behalf_of })
}
async fn script_to_payload(
script_hash: Option<windmill_common::scripts::ScriptHash>,
script_path: String,
db: &sqlx::Pool<sqlx::Postgres>,
flow_job: &MiniPulledJob,
module: &FlowModule,
tag_override: Option<String>,
) -> Result<JobPayloadWithTag, Error> {
let tag_override = if tag_override.as_ref().is_some_and(|x| x.trim().is_empty()) {
None
} else {
tag_override
};
let (payload, tag, delete_after_use, script_timeout, on_behalf_of) = if script_hash.is_none() {
let (jp, tag, delete_after_use, script_timeout, on_behalf_of) =
script_path_to_payload(&script_path, db, &flow_job.workspace_id, Some(true)).await?;
(
jp,
tag_override.to_owned().or(tag),
delete_after_use,
script_timeout,
on_behalf_of,
)
} else {
let hash = script_hash.unwrap();
let mut tx: sqlx::Transaction<'_, sqlx::Postgres> = db.begin().await?;
let ScriptHashInfo {
tag,
concurrency_key,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
priority,
delete_after_use,
timeout,
on_behalf_of_email,
created_by,
..
} = get_script_info_for_hash(&mut *tx, &flow_job.workspace_id, hash.0).await?;
let on_behalf_of = if let Some(email) = on_behalf_of_email {
Some(OnBehalfOf { email, permissioned_as: username_to_permissioned_as(&created_by) })
} else {
None
};
(
JobPayload::ScriptHash {
hash,
path: script_path,
custom_concurrency_key: concurrency_key,
concurrent_limit,
concurrency_time_window_s,
cache_ttl: module.cache_ttl.map(|x| x as i32).ok_or(cache_ttl).ok(),
language,
dedicated_worker,
priority,
apply_preprocessor: false,
},
tag_override.to_owned().or(tag),
delete_after_use,
timeout,
on_behalf_of,
)
};
// the module value overrides the value set at the script level. Defaults to false if both are unset.
let final_delete_after_user =
module.delete_after_use.unwrap_or(false) || delete_after_use.unwrap_or(false);
let flow_step_timeout = module.timeout.or(script_timeout);
Ok(JobPayloadWithTag {
payload,
tag,
delete_after_use: final_delete_after_user,
timeout: flow_step_timeout,
on_behalf_of,
})
}
async fn get_transform_context(
flow_job: &MiniPulledJob,
previous_id: &str,
status: &FlowStatus,
) -> error::Result<IdContext> {
let steps_results: HashMap<String, JobResult> = status
.modules
.iter()
.filter_map(|x| x.job_result().map(|y| (x.id(), y)))
.collect();
Ok(IdContext { flow_job: flow_job.id, steps_results, previous_id: previous_id.to_string() })
}
// trait IntoArray: Sized {
// fn into_array(self) -> Result<Vec<Value>, Self>;
// }
// impl IntoArray for Value {
// fn into_array(self) -> Result<Vec<Value>, Self> {
// match self {
// Value::Array(array) => Ok(array),
// not_array => Err(not_array),
// }
// }
// }
fn from_now(duration: Duration) -> chrono::DateTime<chrono::Utc> {
// "This function errors when original duration is larger than
// the maximum value supported for this type."
chrono::Duration::from_std(duration)
.ok()
.and_then(|d| chrono::Utc::now().checked_add_signed(d))
.unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC)
}
/// returns previous module non-zero suspend count and job, if relevant
fn needs_resume(flow: &FlowValue, status: &FlowStatus) -> Option<(Suspend, Uuid)> {
// for a restarted job, if the restarted step is just after a suspend, don't run the suspend
if status.restarted_from.is_some() {
let current_step_id = flow.modules.get(status.step as usize)?.id.clone();
if status.restarted_from.as_ref().unwrap().step_id == current_step_id {
return None;
}
}
let prev = usize::try_from(status.step)
.ok()
.and_then(|s| s.checked_sub(1))?;
let suspend = flow.modules.get(prev)?.suspend.clone();
if suspend
.as_ref()
.and_then(|s| s.required_events)
.unwrap_or(0)
== 0
{
return None;
}
if let &FlowStatusModule::Success { job, .. } = status.modules.get(prev)? {
Some((suspend.unwrap(), job))
} else {
None
}
}
// returns the result of the previous step of a running flow (if the job was successful)
async fn get_previous_job_result(
db: &sqlx::Pool<sqlx::Postgres>,
w_id: &str,
flow_status: &FlowStatus,
) -> error::Result<Option<Box<RawValue>>> {
let prev = usize::try_from(flow_status.step)
.ok()
.and_then(|s| s.checked_sub(1))
.with_context(|| "No step preceding the current one")?;
match flow_status.modules.get(prev) {
Some(FlowStatusModule::Success { flow_jobs: Some(flow_jobs), .. }) => {
Ok(Some(retrieve_flow_jobs_results(db, w_id, flow_jobs).await?))
}
Some(FlowStatusModule::Success { job, .. }) => Ok(Some(
sqlx::query_scalar!(
"SELECT result AS \"result!: Json<Box<RawValue>>\"
FROM v2_job_completed WHERE id = $1 AND workspace_id = $2",
job,
w_id
)
.fetch_one(db)
.await?
.0,
)),
_ => Ok(None),
}
}