Files
windmill/backend/windmill-worker/src/worker_flow.rs
T
Ruben Fiszel 02ba82064f feat(backend): worker groups (#1452)
* tagged scripts

* tagged scripts

* fix

* fix

* fix

* all

* update sqlx
2023-04-23 16:57:23 +02:00

2091 lines
73 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::time::Duration;
use crate::jobs::{add_completed_job, add_completed_job_error, schedule_again_if_scheduled};
use crate::js_eval::{eval_timeout, IdContext};
use crate::{worker, AuthedClient, KEEP_JOB_DIR};
use anyhow::Context;
use async_recursion::async_recursion;
use serde_json::{json, Map, Value};
use tokio::sync::mpsc::Sender;
use tracing::instrument;
use uuid::Uuid;
use windmill_common::flow_status::{FlowStatusModuleWParent, Iterator, JobResult};
use windmill_common::jobs::{
script_hash_to_tag, script_path_to_payload, JobPayload, QueuedJob, RawCode,
};
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},
};
type DB = sqlx::Pool<sqlx::Postgres>;
use windmill_queue::{canceled_job_to_result, get_queued_job, push, QueueTransaction};
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion<
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
>(
db: &DB,
client: &AuthedClient,
flow: uuid::Uuid,
job_id_for_status: &Uuid,
w_id: &str,
success: bool,
result: serde_json::Value,
metrics: Option<worker::Metrics>,
unrecoverable: bool,
same_worker_tx: Sender<Uuid>,
worker_dir: &str,
stop_early_override: Option<bool>,
base_internal_url: &str,
rsmq: Option<R>,
) -> error::Result<()> {
// this is manual tailrecursion because async_recursion blows up the stack
let mut depth = 0;
let mut rec = update_flow_status_after_job_completion_internal(
db,
client,
flow,
job_id_for_status,
w_id,
success,
result,
metrics.clone(),
unrecoverable,
same_worker_tx.clone(),
worker_dir,
stop_early_override,
base_internal_url,
depth,
rsmq.clone(),
)
.await?;
while let Some(nrec) = rec {
depth += 1;
rec = update_flow_status_after_job_completion_internal(
db,
client,
nrec.flow,
&nrec.job_id_for_status,
w_id,
nrec.success,
nrec.result,
metrics.clone(),
false,
same_worker_tx.clone(),
worker_dir,
nrec.stop_early_override,
base_internal_url,
depth,
rsmq.clone(),
)
.await?;
}
Ok(())
}
pub struct RecUpdateFlowStatusAfterJobCompletion {
flow: uuid::Uuid,
job_id_for_status: Uuid,
success: bool,
result: serde_json::Value,
stop_early_override: Option<bool>,
}
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion_internal<
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
>(
db: &DB,
client: &AuthedClient,
flow: uuid::Uuid,
job_id_for_status: &Uuid,
w_id: &str,
success: bool,
result: serde_json::Value,
metrics: Option<worker::Metrics>,
unrecoverable: bool,
same_worker_tx: Sender<Uuid>,
worker_dir: &str,
stop_early_override: Option<bool>,
base_internal_url: &str,
depth: u8,
rsmq: Option<R>,
) -> error::Result<Option<RecUpdateFlowStatusAfterJobCompletion>> {
let (should_continue_flow, flow_job, stop_early, skip_if_stop_early, nresult) = {
tracing::debug!("UPDATE FLOW STATUS: {flow:?} {success} {result:?} {w_id} {depth}");
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), db.begin().await?).into();
let old_status_json = sqlx::query_scalar!(
"SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2",
flow,
w_id
)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!(
"fetching flow status {flow} while reporting {success} {result:?}: {e}"
))
})?
.ok_or_else(|| Error::InternalErr(format!("requiring a previous status")))?;
let old_status = serde_json::from_value::<FlowStatus>(old_status_json)
.ok()
.ok_or_else(|| {
Error::InternalErr(format!("requiring status to be parsabled as FlowStatus"))
})?;
let module_index = usize::try_from(old_status.step).ok();
let module_status = module_index
.and_then(|i| old_status.modules.get(i))
.unwrap_or(&old_status.failure_module.module_status);
tracing::debug!(
"UPDATE FLOW STATUS 2: {module_index:#?} {module_status:#?} {old_status:#?} "
);
let skip_loop_failures = if matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
) {
compute_skip_loop_failures(flow, old_status.step, tx.transaction_mut())
.await?
.unwrap_or(false)
} else {
false
};
let is_failure_step = old_status.step >= old_status.modules.len() as i32;
let (mut stop_early, skip_if_stop_early) = if let Some(se) = stop_early_override {
(true, se)
} else if is_failure_step {
(false, false)
} else {
let r = sqlx::query!(
"
SELECT raw_flow->'modules'->$1::int->'stop_after_if'->>'expr' as stop_early_expr,
(raw_flow->'modules'->$1::int->'stop_after_if'->>'skip_if_stopped')::bool as skip_if_stopped,
args
FROM queue
WHERE id = $2
",
old_status.step,
flow
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("retrieval of stop_early_expr from state: {e}")))?;
let stop_early = success
&& if let Some(expr) = r.stop_early_expr.clone() {
compute_bool_from_expr(expr, &r.args, result.clone(), None, Some(client))
.await?
} else {
false
};
(stop_early, r.skip_if_stopped.unwrap_or(false))
};
let skip_branch_failure = match module_status {
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, .. }),
..
} => {
compute_skip_branchall_failure(flow, old_status.step, *branch, tx.transaction_mut())
.await?
.unwrap_or(false)
}
_ => false,
};
let skip_failure = skip_branch_failure || skip_loop_failures;
let (inc_step_counter, new_status) = match module_status {
FlowStatusModule::InProgress {
iterator,
branchall,
parallel,
flow_jobs: Some(jobs),
..
} if *parallel => {
let (nindex, len) = match (iterator, branchall) {
(Some(Iterator { itered, .. }), _) => {
let nindex = sqlx::query_scalar!(
"
UPDATE queue
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?
.ok_or_else(|| Error::InternalErr(format!("requiring an index in InProgress")))?;
(nindex, itered.len() as i32)
}
(_, Some(BranchAllStatus { len, .. })) => {
let nindex = sqlx::query_scalar!(
"
UPDATE queue
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?
.ok_or_else(|| Error::InternalErr(format!("requiring an index in InProgress")))?;
(nindex, *len as i32)
}
_ => Err(Error::InternalErr(format!(
"unexpected status for parallel module"
)))?,
};
if nindex == len {
let new_status = if skip_loop_failures
|| sqlx::query_scalar!(
"
SELECT success
FROM completed_job
WHERE id = ANY($1)
",
jobs.as_slice(),
)
.fetch_all(&mut tx)
.await?
.into_iter()
.all(|x| x)
{
FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
branch_chosen: None,
approvers: vec![],
}
} else {
FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
branch_chosen: None,
}
};
(true, Some(new_status))
} else {
tx.commit().await?;
return Ok(None);
}
}
FlowStatusModule::InProgress {
iterator: Some(windmill_common::flow_status::Iterator { index, itered, .. }),
..
} if (*index + 1 < itered.len() && (success || skip_loop_failures)) && !stop_early => {
(false, None)
}
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, len, .. }),
..
} if branch.to_owned() < len - 1 && (success || skip_branch_failure) => (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, branch_chosen) = match module_status {
FlowStatusModule::InProgress { flow_jobs, branch_chosen, .. } => {
(flow_jobs.clone(), branch_chosen.clone())
}
_ => (None, None),
};
if success || (flow_jobs.is_some() && (skip_loop_failures || skip_branch_failure)) {
(
true,
Some(FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
branch_chosen,
approvers: vec![],
}),
)
} else {
(
false,
Some(FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
branch_chosen,
}),
)
}
}
};
let step_counter = if inc_step_counter {
sqlx::query!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)
WHERE id = $2
",
json!(old_status.step + 1),
flow
)
.execute(&mut tx)
.await?;
old_status.step + 1
} else {
old_status.step
};
/* 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 queue WHERE id = $1",
flow
)
.fetch_one(&mut tx)
.await?;
sqlx::query!(
"
UPDATE queue
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?;
} else {
sqlx::query!(
"
UPDATE queue
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?;
if let Some(job_result) = new_status.job_result() {
sqlx::query!(
"
UPDATE queue
SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)
WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id
",
new_status.id(),
json!(job_result),
flow
)
.execute(&mut tx)
.await?;
}
}
}
let nresult = match &new_status {
Some(FlowStatusModule::Success { flow_jobs: Some(jobs), .. })
| Some(FlowStatusModule::Failure { flow_jobs: Some(jobs), .. }) => {
let results = sqlx::query!(
"
SELECT result, id
FROM completed_job
WHERE id = ANY($1)
AND workspace_id = $2
",
jobs.as_slice(),
w_id
)
.fetch_all(&mut tx)
.await?
.into_iter()
.map(|r| (r.id, r.result))
.collect::<HashMap<_, _>>();
let results = jobs
.iter()
.map(|j| {
results.get(j).ok_or_else(|| {
Error::InternalErr(format!("missing job result for {}", j))
})
})
.collect::<Result<Vec<_>, _>>()?;
json!(results)
}
_ => result,
};
if matches!(&new_status, Some(FlowStatusModule::Success { .. })) {
sqlx::query(
"
UPDATE queue
SET flow_status = flow_status - 'retry'
WHERE id = $1
RETURNING flow_status
",
)
.bind(flow)
.execute(&mut tx)
.await
.context("remove flow status retry")?;
}
let flow_job = get_queued_job(flow, w_id, tx.transaction_mut())
.await?
.ok_or_else(|| Error::InternalErr(format!("requiring flow to be in the queue")))?;
let job_root = flow_job
.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 module = {
let raw_flow = flow_job.parse_raw_flow();
if let Some(raw_flow) = raw_flow {
if let Some(i) = module_index {
if let Some(module) = raw_flow.modules.get(i) {
Some(module.clone())
} else {
raw_flow.failure_module
}
} else {
None
}
} else {
None
}
};
let should_continue_flow = match success {
_ if stop_early => false,
_ if flow_job.canceled => false,
true => !is_last_step,
false if unrecoverable => false,
false if skip_failure => !is_last_step,
false
if next_retry(
&module.and_then(|m| m.retry.clone()).unwrap_or_default(),
&old_status.retry,
)
.is_some() =>
{
true
}
false if has_failure_module(flow, tx.transaction_mut()).await? && !is_failure_step => {
true
}
false => false,
};
if old_status.step == 0
&& !flow_job.is_flow_step
&& flow_job.schedule_path.is_some()
&& flow_job.script_path.is_some()
{
tx = schedule_again_if_scheduled(
tx,
db,
flow_job.schedule_path.as_ref().unwrap(),
flow_job.script_path.as_ref().unwrap(),
&w_id,
)
.await?;
}
tx.commit().await?;
(
should_continue_flow,
flow_job,
stop_early,
skip_if_stop_early,
nresult,
)
};
let done = if !should_continue_flow {
let logs = if flow_job.canceled {
"Flow job canceled".to_string()
} else if stop_early {
format!("Flow job stopped early because of a stop early predicate returning true")
} else if success {
"Flow job completed with success".to_string()
} else {
"Flow job completed with error".to_string()
};
if flow_job.canceled {
add_completed_job_error(
db,
&flow_job,
logs,
canceled_job_to_result(&flow_job),
metrics.clone(),
rsmq.clone(),
)
.await?;
} else {
add_completed_job(
db,
&flow_job,
success,
stop_early && skip_if_stop_early,
nresult.clone(),
logs,
rsmq.clone(),
)
.await?;
}
true
} else {
match handle_flow(
&flow_job,
db,
client,
nresult.clone(),
same_worker_tx.clone(),
worker_dir,
base_internal_url,
rsmq.clone(),
)
.await
{
Err(err) => {
let _ = add_completed_job_error(
db,
&flow_job,
"Unexpected error during flow chaining:\n".to_string(),
json!({"message": err.to_string(), "name": "InternalError"}),
metrics.clone(),
rsmq.clone(),
)
.await;
true
}
Ok(_) => false,
}
};
if done {
if flow_job.same_worker && !*KEEP_JOB_DIR {
let _ = tokio::fs::remove_dir_all(format!("{worker_dir}/{}", flow_job.id)).await;
}
if let Some(parent_job) = flow_job.parent_job {
return Ok(Some(RecUpdateFlowStatusAfterJobCompletion {
flow: parent_job,
job_id_for_status: flow,
success,
result: nresult,
stop_early_override: if stop_early {
Some(skip_if_stop_early)
} else {
None
},
}));
}
Ok(None)
} else {
Ok(None)
}
}
async fn compute_skip_loop_failures<'c>(
flow: Uuid,
step: i32,
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
) -> Result<Option<bool>, Error> {
sqlx::query_as(
"
SELECT (raw_flow->'modules'->$1->'value'->>'skip_failures')::bool
FROM queue
WHERE id = $2
",
)
.bind(step)
.bind(flow)
.fetch_one(tx)
.await
.map(|(v,)| v)
.map_err(|e| Error::InternalErr(format!("error during retrieval of skip_loop_failures: {e}")))
}
async fn compute_skip_branchall_failure<'c>(
flow: Uuid,
step: i32,
branch: usize,
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
) -> Result<Option<bool>, Error> {
sqlx::query_as(
"
SELECT (raw_flow->'modules'->$1->'value'->'branches'->$2->>'skip_failure')::bool
FROM queue
WHERE id = $3
",
)
.bind(step)
.bind(branch as i32)
.bind(flow)
.fetch_one(tx)
.await
.map(|(v,)| v)
.map_err(|e| Error::InternalErr(format!("error during retrieval of skip_loop_failures: {e}")))
}
async fn has_failure_module<'c>(
flow: Uuid,
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
) -> Result<bool, Error> {
sqlx::query_scalar::<_, Option<bool>>(
"
SELECT raw_flow->'failure_module' != 'null'::jsonb
FROM queue
WHERE id = $1
",
)
.bind(flow)
.fetch_one(tx)
.await
.map_err(|e| Error::InternalErr(format!("error during retrieval of has_failure_module: {e}")))
.map(|v| v.unwrap_or(false))
}
fn next_retry(retry: &Retry, status: &RetryStatus) -> Option<(u16, Duration)> {
(status.fail_count <= MAX_RETRY_ATTEMPTS)
.then(|| &retry)
.and_then(|retry| retry.interval(status.fail_count))
.map(|d| (status.fail_count + 1, std::cmp::min(d, MAX_RETRY_INTERVAL)))
}
async fn compute_bool_from_expr(
expr: String,
flow_args: &Option<serde_json::Value>,
result: serde_json::Value,
by_id: Option<IdContext>,
client: Option<&AuthedClient>,
) -> error::Result<bool> {
let flow_input = flow_args.clone().unwrap_or_else(|| json!({}));
match eval_timeout(
expr,
[
("flow_input".to_string(), flow_input),
("result".to_string(), result.clone()),
("previous_result".to_string(), result),
]
.into(),
client,
by_id,
)
.await?
{
serde_json::Value::Bool(true) => Ok(true),
serde_json::Value::Bool(false) => Ok(false),
a @ _ => Err(Error::ExecutionErr(format!(
"Expected a boolean value, found: {a:?}"
))),
}
}
pub async fn update_flow_status_in_progress(
db: &DB,
w_id: &str,
flow: Uuid,
job_in_progress: Uuid,
) -> error::Result<()> {
let step = get_step_of_flow_status(db, flow).await?;
if let Step::Step(step) = step {
sqlx::query(&format!(
"UPDATE queue
SET flow_status = jsonb_set(jsonb_set(flow_status, '{{modules, {step}, job}}', $1), '{{modules, {step}, type}}', $2)
WHERE id = $3 AND workspace_id = $4",
))
.bind(json!(job_in_progress.to_string()))
.bind(json!("InProgress"))
.bind(flow)
.bind(w_id)
.execute(db)
.await?;
} else {
sqlx::query(&format!(
"UPDATE queue
SET flow_status = jsonb_set(jsonb_set(flow_status, '{{failure_module, job}}', $1), '{{failure_module, type}}', $2)
WHERE id = $3 AND workspace_id = $4",
))
.bind(json!(job_in_progress.to_string()))
.bind(json!("InProgress"))
.bind(flow)
.bind(w_id)
.execute(db)
.await?;
}
Ok(())
}
pub enum Step {
Step(i32),
FailureStep,
}
#[instrument(level = "trace", skip_all)]
pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
let r = sqlx::query!(
"SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len FROM queue WHERE id = $1",
id
)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching step flow status: {e}")))?;
if r.step < r.len {
Ok(Step::Step(r.step.ok_or_else(|| {
Error::InternalErr("step is null".to_string())
})?))
} else {
Ok(Step::FailureStep)
}
}
/// 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: &Option<serde_json::Value>,
last_result: serde_json::Value,
input_transforms: &HashMap<String, InputTransform>,
resumes: &[Value],
approvers: Vec<String>,
by_id: &IdContext,
client: &AuthedClient,
) -> windmill_common::error::Result<Map<String, serde_json::Value>> {
let mut mapped = serde_json::Map::new();
for (key, val) in input_transforms.into_iter() {
if let InputTransform::Static { value } = val {
mapped.insert(key.to_string(), value.to_owned());
}
}
let lresult = last_result.clone();
let error = if let Some(error) = lresult.as_object() {
error.get("error")
} else {
None
};
for (key, val) in input_transforms.into_iter() {
match val {
InputTransform::Static { value: _ } => (),
InputTransform::Javascript { expr } => {
let flow_input = flow_args.clone().unwrap_or_else(|| json!({}));
let previous_result = last_result.clone();
let mut context = vec![
("params".to_string(), json!(mapped)),
("previous_result".to_string(), previous_result),
("flow_input".to_string(), flow_input),
(
"resume".to_string(),
resumes.last().map(|v| json!(v)).unwrap_or_default(),
),
("resumes".to_string(), resumes.clone().into()),
("approvers".to_string(), json!(approvers.clone())),
];
if error.is_some() {
context.push(("error".to_string(), error.unwrap().clone()));
}
let v = eval_timeout(expr.to_string(), context, Some(client), Some(by_id.clone()))
.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<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
flow_job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_result: serde_json::Value,
same_worker_tx: Sender<Uuid>,
worker_dir: &str,
base_internal_url: &str,
rsmq: Option<R>,
) -> anyhow::Result<()> {
let value = flow_job
.raw_flow
.as_ref()
.ok_or_else(|| Error::InternalErr(format!("requiring a raw flow value")))?
.to_owned();
let flow = serde_json::from_value::<FlowValue>(value)?;
let status: FlowStatus =
serde_json::from_value::<FlowStatus>(flow_job.flow_status.clone().unwrap_or_default())
.with_context(|| format!("parse flow status {}", flow_job.id))?;
tracing::debug!("handle_flow: {:#?}", flow_job.flow_status);
push_next_flow_job(
flow_job,
status,
flow,
db,
client,
last_result,
same_worker_tx,
worker_dir,
base_internal_url,
rsmq,
)
.await?;
Ok(())
}
#[async_recursion]
// #[instrument(level = "trace", skip_all)]
async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
flow_job: &QueuedJob,
mut status: FlowStatus,
flow: FlowValue,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
mut last_result: serde_json::Value,
same_worker_tx: Sender<Uuid>,
worker_dir: &str,
base_internal_url: &str,
rsmq: Option<R>,
) -> error::Result<()> {
let job_root = flow_job
.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 i = usize::try_from(status.step)
.with_context(|| format!("invalid module index {}", status.step))?;
let mut status_module: FlowStatusModule = status
.modules
.get(i)
.cloned()
.unwrap_or_else(|| status.failure_module.module_status.clone());
// if this is an empty module of if the module has aleady been completed, successfully, update the parent flow
if flow.modules.is_empty() || matches!(status_module, FlowStatusModule::Success { .. }) {
return update_flow_status_after_job_completion(
db,
client,
flow_job.id,
&Uuid::nil(),
flow_job.workspace_id.as_str(),
true,
if flow.modules.is_empty() {
json!({})
} else {
json!([])
},
None,
true,
same_worker_tx,
worker_dir,
None,
base_internal_url,
rsmq,
)
.await;
}
let mut module: &FlowModule = flow
.modules
.get(i)
.or_else(|| flow.failure_module.as_ref())
.with_context(|| format!("no module at index {}", status.step))?;
let current_id = &module.id;
let previous_id = if i >= 1 {
flow.modules.get(i - 1).map(|m| m.id.clone()).unwrap()
} else {
String::new()
};
// calculate sleep if any
let mut scheduled_for_o = {
let sleep_input_transform = i
.checked_sub(1)
.and_then(|i| flow.modules.get(i))
.and_then(|m| m.sleep.clone());
if let Some(it) = sleep_input_transform {
let json_value = match it {
InputTransform::Static { value } => value,
InputTransform::Javascript { expr } => {
let flow_input = flow_job.args.clone().unwrap_or_else(|| json!({}));
eval_timeout(
expr.to_string(),
[
("result".to_string(), last_result.clone()),
("flow_input".to_string(), flow_input),
]
.into(),
None,
None,
)
.await
.map_err(|e| {
Error::ExecutionErr(format!(
"Error during isolated evaluation of expression `{expr}`:\n{e}"
))
})?
}
};
match json_value {
serde_json::Value::Number(n) => {
n.as_u64().map(|x| from_now(Duration::from_secs(x)))
}
_ => Err(Error::ExecutionErr(format!(
"Expected a number value, found: {json_value}"
)))?,
}
} else {
None
}
};
let mut resume_messages: Vec<Value> = 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().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 queue WHERE id = $1 FOR UPDATE",
flow_job.id
)
.fetch_one(&mut tx)
.await
.context("lock flow in queue")?;
let resumes = sqlx::query!(
"SELECT value, approver, resume_id FROM resume_job WHERE job = $1 ORDER BY created_at ASC",
last
)
.fetch_all(&mut tx)
.await?;
resume_messages.extend(resumes.iter().map(|r| r.value.clone()));
approvers.extend(resumes.iter().map(|r| {
r.approver
.as_deref()
.unwrap_or_else(|| "anonymous")
.to_string()
}));
let required_events = suspend.required_events.unwrap() as u16;
if resume_messages.len() >= required_events as usize {
sqlx::query(
"
UPDATE queue
SET flow_status =
JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'approvers'], $2)
WHERE id = $3
",
)
.bind(status.step - 1)
.bind(json!(resumes
.into_iter()
.map(|r| Approval {
resume_id: r.resume_id as u16,
approver: r.approver.unwrap_or_else(|| "unknown".to_string())
})
.collect::<Vec<_>>()))
.bind(flow_job.id)
.execute(&mut tx)
.await?;
/* If we are woken up after suspending, last_result will be the flow args, but we
* should use the result from the last job */
if let FlowStatusModule::WaitingForEvents { .. } = &status_module {
last_result =
sqlx::query_scalar!("SELECT result FROM completed_job WHERE id = $1", last)
.fetch_one(&mut tx)
.await?
.context("previous job result")?;
}
/* continue on and run this job! */
tx.commit().await?;
/* not enough messages to do this job, "park"/suspend until there are */
} else if matches!(
&status_module,
FlowStatusModule::WaitingForPriorSteps { .. }
) {
sqlx::query(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', flow_status->>'step'::text], $1)
, suspend = $2
, suspend_until = now() + $3
WHERE id = $4
",
)
.bind(json!(FlowStatusModule::WaitingForEvents { id: status_module.id(), count: required_events, job: last }))
.bind((required_events - resume_messages.len() as u16) as i32)
.bind(Duration::from_secs(suspend.timeout.map(|t| t.into()).unwrap_or_else(|| 30 * 60)))
.bind(flow_job.id)
.execute(&mut tx)
.await?;
tx.commit().await?;
return Ok(());
/* cancelled or we're WaitingForEvents but we don't have enough messages (timed out) */
} else {
tx.commit().await?;
let success = false;
let skipped = false;
let logs = "Timed out waiting to be resumed".to_string();
let result = json!({ "error": {"message": logs, "name": "SuspendedTimeout"}});
let _uuid =
add_completed_job(db, &flow_job, success, skipped, result, logs, rsmq).await?;
return Ok(());
}
}
}
match &status_module {
FlowStatusModule::Failure { job, .. } => {
let retry = &module.retry.clone().unwrap_or_default();
if let Some((fail_count, retry_in)) = next_retry(retry, &status.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 queue
SET flow_status = JSONB_SET(flow_status, ARRAY['retry'], $1)
WHERE id = $2
",
)
.bind(json!(RetryStatus { fail_count, ..status.retry.clone() }))
.bind(flow_job.id)
.execute(db)
.await
.context("update flow retry")?;
/* it might be better to retry the job using the previous args instead of determining
* them again from the last result, but that seemed to not play well with the forloop
* logic and I couldn't figure out why. */
if let Some(v) = &status.retry.previous_result {
last_result = v.clone();
}
status_module = FlowStatusModule::WaitingForPriorSteps { id: status_module.id() };
/* 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. */
i = flow.modules.len();
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();
/* (retry feature) save the previous_result the first time this step is run */
let retry = &module.retry.clone().unwrap_or_default();
if retry.has_attempts() {
sqlx::query(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['retry'], $1)
WHERE id = $2
",
)
.bind(json!(RetryStatus {
previous_result: Some(last_result.clone()),
fail_count: 0,
failed_jobs: vec![],
}))
.bind(flow_job.id)
.execute(db)
.await
.context("update flow retry")?;
};
}
/* (retry feature) save the previous_result the first time this step is run */
}
FlowStatusModule::WaitingForPriorSteps { .. }
if module
.retry
.as_ref()
.map(|x| x.has_attempts())
.unwrap_or(false)
&& status.retry.fail_count == 0 =>
{
sqlx::query(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['retry'], $1)
WHERE id = $2
",
)
.bind(json!(RetryStatus {
previous_result: Some(last_result.clone()),
fail_count: 0,
failed_jobs: vec![],
}))
.bind(flow_job.id)
.execute(db)
.await
.context("update flow retry")?;
}
_ => (),
}
let mut transform_context: Option<IdContext> = None;
let args: windmill_common::error::Result<_> = match &module.value {
FlowModuleValue::Script { input_transforms, .. }
| FlowModuleValue::RawScript { input_transforms, .. }
| FlowModuleValue::Flow { input_transforms, .. } => {
let ctx = get_transform_context(&flow_job, previous_id.clone(), &status).await?;
transform_context = Some(ctx);
let by_id = transform_context.as_ref().unwrap();
transform_input(
&flow_job.args,
last_result.clone(),
input_transforms,
resume_messages.as_slice(),
approvers,
by_id,
client,
)
.await
}
FlowModuleValue::Identity => match last_result.clone() {
Value::Object(m) => Ok(m),
v @ _ => {
let mut m = Map::new();
m.insert("previous_result".to_string(), v);
Ok(m)
}
},
_ => {
/* embedded flow input is augmented with embedding flow input */
if let Some(value) = &flow_job.args {
Ok(value
.as_object()
.ok_or_else(|| {
Error::BadRequest(format!("Expected an object value, found: {value:?}"))
})?
.clone())
} else {
Ok(Map::new())
}
}
};
let tx = db.begin().await?;
let (tx, next_flow_transform) = compute_next_flow_transform(
flow_job,
&flow,
transform_context,
tx,
&module,
&status,
&status_module,
last_result.clone(),
previous_id,
client,
)
.await?;
tx.commit().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 => {
sqlx::query(
r#"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2)
WHERE id = $3
"#,
)
.bind(status.step)
.bind(json!(FlowStatusModule::Success { id: status_module.id(), job: Uuid::nil(), flow_jobs: None, branch_chosen: None, approvers: vec![] }))
.bind(flow_job.id)
.execute(db)
.await?;
// flow is reprocessed by the worker in a state where the module has completed succesfully.
// The next steps are pull -> handle flow -> push next flow job -> update flow status since module status is success
same_worker_tx
.send(flow_job.id)
.await
.expect("send to same worker");
return Ok(());
}
};
let continue_on_same_worker =
flow.same_worker && module.suspend.is_none() && module.sleep.is_none();
/* Finally, push the job into the queue */
let mut tx = (rsmq.clone(), db.begin().await?).into();
let mut uuids = vec![];
let len = match &job_payloads {
ContinuePayload::SingleJob(_) => 1,
ContinuePayload::BranchAllJobs(payloads) => payloads.len(),
ContinuePayload::ForloopJobs { n, .. } => *n,
};
for i in (0..len).into_iter() {
let payload_tag = match &job_payloads {
ContinuePayload::SingleJob(payload) => payload.clone(),
ContinuePayload::BranchAllJobs(payloads) => payloads[i].clone(),
ContinuePayload::ForloopJobs { modules, .. } => {
let mut fm = flow.failure_module.clone();
if let Some(mut failure_module) = flow.failure_module.clone() {
failure_module.id_append(&format!("{}/{}", status.step, i));
fm = Some(failure_module);
}
JobPayloadWithTag {
payload: JobPayload::RawFlow {
value: FlowValue {
modules: (*modules).clone(),
failure_module: fm.clone(),
same_worker: flow.same_worker,
},
path: Some(format!("{}/loop-{}", flow_job.script_path(), i)),
},
tag: None,
}
}
};
let args = match &next_status {
NextStatus::AllFlowJobs {
branchall: Some(BranchAllStatus { .. }),
iterator: None,
..
} => args.as_ref().map(|args| args.clone()),
NextStatus::NextLoopIteration(NextIteration { new_args, .. }) => {
args.as_ref().map(|args| {
let mut args = args.clone();
args.extend(new_args.clone());
args
})
}
NextStatus::AllFlowJobs {
branchall: None,
iterator: Some(Iterator { itered, .. }),
..
} => args.as_ref().map(|args| {
let mut new_args = args.clone();
new_args.insert(
"iter".to_string(),
json!({ "index": i, "value": itered[i] }),
);
new_args
}),
_ => args.as_ref().map(|args| args.clone()),
};
let (ok, err) = match args {
Ok(v) => (Some(v), None),
Err(e) => (None, Some(e)),
};
let root_job = if matches!(module.value, FlowModuleValue::Flow { .. }) {
None
} else {
flow_job.root_job.or_else(|| Some(flow_job.id))
};
let (uuid, inner_tx) = push(
tx,
&flow_job.workspace_id,
payload_tag.payload,
ok.unwrap_or_else(|| Map::new()),
&flow_job.created_by,
&flow_job.email,
flow_job.permissioned_as.to_owned(),
scheduled_for_o,
flow_job.schedule_path.clone(),
Some(flow_job.id),
root_job,
true,
continue_on_same_worker,
err,
flow_job.visible_to_owner,
payload_tag.tag,
)
.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()))
};
let first_uuid = uuids[0];
let new_status = match next_status {
NextStatus::NextLoopIteration(NextIteration { index, itered, mut flow_jobs, .. }) => {
let uuid = one_uuid?;
flow_jobs.push(uuid);
FlowStatusModule::InProgress {
job: uuid,
iterator: Some(windmill_common::flow_status::Iterator { index, itered }),
flow_jobs: Some(flow_jobs),
branch_chosen: None,
branchall: None,
id: status_module.id(),
parallel: false,
}
}
NextStatus::AllFlowJobs { iterator, branchall } => FlowStatusModule::InProgress {
job: flow_job.id,
iterator,
flow_jobs: Some(uuids),
branch_chosen: None,
branchall,
id: status_module.id(),
parallel: true,
},
NextStatus::NextBranchStep(NextBranch { mut flow_jobs, status, .. }) => {
let uuid = one_uuid?;
flow_jobs.push(uuid);
FlowStatusModule::InProgress {
job: uuid,
iterator: None,
flow_jobs: Some(flow_jobs),
branch_chosen: None,
branchall: Some(status),
id: status_module.id(),
parallel: false,
}
}
NextStatus::BranchChosen(branch) => FlowStatusModule::InProgress {
job: one_uuid?,
iterator: None,
flow_jobs: None,
branch_chosen: Some(branch),
branchall: None,
id: status_module.id(),
parallel: false,
},
NextStatus::NextStep => {
FlowStatusModule::WaitingForExecutor { id: status_module.id(), job: one_uuid? }
}
};
tracing::debug!("STATUS STEP: {:?} {i} {:#?}", status.step, new_status);
if i >= flow.modules.len() {
sqlx::query!(
"
UPDATE queue
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.clone()
}),
json!(i),
flow_job.id
)
.execute(db)
.await?;
} else {
sqlx::query!(
"
UPDATE queue
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(db)
.await?;
};
tx.commit().await?;
tracing::info!(id = %flow_job.id, root_id = %job_root, "all next flow jobs pushed");
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(),
));
}
same_worker_tx.send(first_uuid).await.map_err(to_anyhow)?;
}
return Ok(());
}
// 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 NextIteration {
index: usize,
itered: Vec<Value>,
flow_jobs: Vec<Uuid>,
new_args: Map<String, serde_json::Value>,
}
enum LoopStatus {
ParallelIteration { itered: Vec<Value> },
NextIteration(NextIteration),
EmptyIterator,
}
#[derive(Debug)]
struct NextBranch {
status: BranchAllStatus,
flow_jobs: Vec<Uuid>,
}
#[derive(Debug)]
enum NextStatus {
NextStep,
BranchChosen(BranchChosen),
NextBranchStep(NextBranch),
NextLoopIteration(NextIteration),
AllFlowJobs {
branchall: Option<BranchAllStatus>,
iterator: Option<windmill_common::flow_status::Iterator>,
},
}
#[derive(Clone)]
struct JobPayloadWithTag {
payload: JobPayload,
tag: Option<String>,
}
enum ContinuePayload {
SingleJob(JobPayloadWithTag),
ForloopJobs { n: usize, modules: Vec<FlowModule> },
BranchAllJobs(Vec<JobPayloadWithTag>),
}
enum NextFlowTransform {
EmptyInnerFlows,
Continue(ContinuePayload, NextStatus),
}
async fn compute_next_flow_transform<'c>(
flow_job: &QueuedJob,
flow: &FlowValue,
by_id: Option<IdContext>,
mut tx: sqlx::Transaction<'c, sqlx::Postgres>,
module: &FlowModule,
status: &FlowStatus,
status_module: &FlowStatusModule,
last_result: serde_json::Value,
previous_id: String,
client: &AuthedClient,
) -> error::Result<(sqlx::Transaction<'c, sqlx::Postgres>, NextFlowTransform)> {
match &module.value {
FlowModuleValue::Identity => Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::Identity,
tag: None,
}),
NextStatus::NextStep,
),
)),
FlowModuleValue::Flow { path, .. } => {
let payload = JobPayload::Flow(path.to_string());
Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag { payload, tag: None }),
NextStatus::NextStep,
),
))
}
FlowModuleValue::Script { path: script_path, hash: script_hash, .. } => {
let (payload, tag) = if script_hash.is_none() {
script_path_to_payload(script_path, &mut tx, &flow_job.workspace_id).await?
} else {
let hash = script_hash.clone().unwrap();
let tag = script_hash_to_tag(&hash, &mut tx, &flow_job.workspace_id).await?;
(
JobPayload::ScriptHash { hash, path: script_path.to_owned() },
tag,
)
};
Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag { payload, tag }),
NextStatus::NextStep,
),
))
}
FlowModuleValue::RawScript { path, content, language, lock, tag, .. } => {
let path = path
.clone()
.or_else(|| Some(format!("{}/step-{}", flow_job.script_path(), status.step)));
Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::Code(RawCode {
path,
content: content.clone(),
language: language.clone(),
lock: lock.clone(),
}),
tag: tag.clone(),
}),
NextStatus::NextStep,
),
))
}
/* forloop modules are expected set `iter: { value: Value, index: usize }` as job arguments */
FlowModuleValue::ForloopFlow { modules, iterator, parallel, .. } => {
let new_args: &mut Map<String, serde_json::Value> = &mut Map::new();
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?
};
let flow_input = flow_job.args.clone().unwrap_or_else(|| json!({}));
/* Iterator is an InputTransform, evaluate it into an array. */
let itered = evaluate_with(
iterator.clone(),
|| {
vec![
("flow_input".to_string(), flow_input),
("result".to_string(), last_result.clone()),
("previous_result".to_string(), last_result.clone()),
]
},
Some(client),
Some(by_id),
)
.await?
.into_array()
.map_err(|not_array| {
Error::ExecutionErr(format!("Expected an array value, found: {not_array}"))
})?;
if itered.is_empty() {
LoopStatus::EmptyIterator
} else if *parallel {
LoopStatus::ParallelIteration { itered }
} else if let Some(first) = itered.first() {
new_args.insert("iter".to_string(), json!({ "index": 0, "value": first }));
LoopStatus::NextIteration(NextIteration {
index: 0,
itered,
flow_jobs: vec![],
new_args: new_args.clone(),
})
} else {
panic!("itered cannot be empty")
}
}
FlowStatusModule::InProgress {
iterator: Some(windmill_common::flow_status::Iterator { itered, index }),
flow_jobs: Some(flow_jobs),
..
} if !*parallel => {
let (index, next) = index
.checked_add(1)
.and_then(|i| itered.get(i).map(|next| (i, next)))
/* we shouldn't get here because update_flow_status_after_job_completion
* should leave this state if there iteration is complete, but also it should
* be reasonable to just enter a completed state instead of failing, similar to
* iterating an empty list above */
.with_context(|| {
format!("could not iterate index {index} of {itered:?}")
})?;
new_args.insert("iter".to_string(), json!({ "index": index, "value": next }));
LoopStatus::NextIteration(NextIteration {
index,
itered: itered.clone(),
flow_jobs: flow_jobs.clone(),
new_args: new_args.clone(),
})
}
_ => Err(Error::BadRequest(format!(
"Unrecognized module status for ForloopFlow {status_module:?}"
)))?,
};
match next_loop_status {
LoopStatus::EmptyIterator => Ok((tx, NextFlowTransform::EmptyInnerFlows)),
LoopStatus::NextIteration(ns) => {
let mut fm = flow.failure_module.clone();
if let Some(mut failure_module) = flow.failure_module.clone() {
failure_module.id_append(&format!("{}/{}", status.step, ns.index));
fm = Some(failure_module);
}
Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::RawFlow {
value: FlowValue {
modules: (*modules).clone(),
failure_module: fm,
same_worker: flow.same_worker,
},
path: Some(format!(
"{}/loop-{}",
flow_job.script_path(),
ns.index
)),
},
tag: None,
}),
NextStatus::NextLoopIteration(ns),
),
))
}
LoopStatus::ParallelIteration { itered } => Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::ForloopJobs {
n: itered.len(),
modules: (*modules).clone(),
},
NextStatus::AllFlowJobs {
branchall: None,
iterator: Some(windmill_common::flow_status::Iterator {
index: 0,
itered,
}),
},
),
)),
}
}
FlowModuleValue::BranchOne { branches, default, .. } => {
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.to_string(),
&flow_job.args,
last_result.clone(),
Some(idcontext.clone()),
Some(client),
)
.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 = if let BranchChosen::Branch { branch } = branch {
branches
.get(branch)
.map(|b| b.modules.clone())
.ok_or_else(|| {
Error::BadRequest(format!(
"Unrecognized branch for BranchAll {status_module:?}"
))
})?
} else {
default.clone()
};
let mut fm = flow.failure_module.clone();
if let Some(mut failure_module) = flow.failure_module.clone() {
failure_module.id_append(&status.step.to_string());
fm = Some(failure_module);
}
Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::RawFlow {
value: FlowValue {
modules,
failure_module: fm,
same_worker: flow.same_worker,
},
path: Some(format!(
"{}/branchone-{}",
flow_job.script_path(),
status.step
)),
},
tag: None,
}),
NextStatus::BranchChosen(branch),
),
))
}
FlowModuleValue::BranchAll { branches, parallel, .. } => {
let (branch_status, flow_jobs) = match status_module {
FlowStatusModule::WaitingForPriorSteps { .. }
| FlowStatusModule::WaitingForEvents { .. }
| FlowStatusModule::WaitingForExecutor { .. } => {
if branches.is_empty() {
return Ok((tx, NextFlowTransform::EmptyInnerFlows));
} else if *parallel {
return Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::BranchAllJobs(
branches
.iter()
.enumerate()
.map(|(i, b)| {
let mut fm = flow.failure_module.clone();
if let Some(mut failure_module) =
flow.failure_module.clone()
{
failure_module
.id_append(&format!("{}/{i}", status.step));
fm = Some(failure_module);
}
JobPayloadWithTag {
payload: JobPayload::RawFlow {
value: FlowValue {
modules: b.modules.clone(),
failure_module: fm.clone(),
same_worker: flow.same_worker,
},
path: Some(format!(
"{}/branchall-{}",
flow_job.script_path(),
i
)),
},
tag: None,
}
})
.collect(),
),
NextStatus::AllFlowJobs {
branchall: Some(BranchAllStatus {
branch: 0,
previous_result: last_result,
len: branches.len(),
}),
iterator: None,
},
),
));
} else {
(
BranchAllStatus {
branch: 0,
previous_result: last_result,
len: branches.len(),
},
vec![],
)
}
}
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, previous_result, len }),
flow_jobs: Some(flow_jobs),
..
} if !*parallel => (
BranchAllStatus {
branch: branch + 1,
previous_result: previous_result.clone(),
len: len.clone(),
},
flow_jobs.clone(),
),
_ => Err(Error::BadRequest(format!(
"Unrecognized module status for BranchAll {status_module:?}"
)))?,
};
let modules = branches
.get(branch_status.branch)
.map(|b| b.modules.clone())
.ok_or_else(|| {
Error::BadRequest(format!(
"Unrecognized branch for BranchAll {status_module:?}"
))
})?;
let mut fm = flow.failure_module.clone();
if let Some(mut failure_module) = flow.failure_module.clone() {
failure_module.id_append(&format!("{}/{}", status.step, branch_status.branch));
fm = Some(failure_module);
}
Ok((
tx,
NextFlowTransform::Continue(
ContinuePayload::SingleJob(JobPayloadWithTag {
payload: JobPayload::RawFlow {
value: FlowValue {
modules,
failure_module: fm.clone(),
same_worker: flow.same_worker,
},
path: Some(format!(
"{}/branchall-{}",
flow_job.script_path(),
branch_status.branch
)),
},
tag: None,
}),
NextStatus::NextBranchStep(NextBranch { status: branch_status, flow_jobs }),
),
))
}
}
}
async fn get_transform_context(
flow_job: &QueuedJob,
previous_id: String,
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 })
}
async fn evaluate_with<F>(
transform: InputTransform,
vars: F,
client: Option<&AuthedClient>,
by_id: Option<IdContext>,
) -> anyhow::Result<serde_json::Value>
where
F: FnOnce() -> Vec<(String, serde_json::Value)>,
{
match transform {
InputTransform::Static { value } => Ok(value),
InputTransform::Javascript { expr } => eval_timeout(expr, vars(), client, by_id).await,
}
}
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
fn needs_resume(flow: &FlowValue, status: &FlowStatus) -> Option<(Suspend, Uuid)> {
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
}
}