diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 91f09fc025..a8beb68395 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -5032,6 +5032,23 @@ paths: mem_peak: type: integer + /w/{workspace}/jobs_u/get_flow_debug_info/{id}: + get: + summary: get flow debug info + operationId: getFlowDebugInfo + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + responses: + "200": + description: flow debug info details + content: + application/json: + schema: {} + + /w/{workspace}/jobs_u/completed/get/{id}: get: summary: get completed job diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 48d995bccd..d6e9e4aa4c 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -11,7 +11,7 @@ use serde_json::value::RawValue; use std::collections::HashMap; use std::sync::atomic::Ordering; use tokio::time::Instant; -use windmill_common::flow_status::RestartedFrom; +use windmill_common::flow_status::{JobResult, RestartedFrom}; use windmill_common::variables::get_workspace_key; use crate::db::ApiAuthed; @@ -206,6 +206,7 @@ pub fn global_service() -> Router { ) .route("/get/:id", get(get_job)) .route("/get_logs/:id", get(get_job_logs)) + .route("/get_flow_debug_info/:id", get(get_flow_job_debug_info)) .route("/completed/get/:id", get(get_completed_job)) .route("/completed/get_result/:id", get(get_completed_job_result)) .route( @@ -436,6 +437,59 @@ pub async fn get_path_tag_limits_cache_for_hash( )) } +async fn get_flow_job_debug_info( + Extension(db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, +) -> error::Result { + let job = get_queued_job(id, w_id.as_str(), &db).await?; + if let Some(job) = job { + let is_flow = &job.job_kind == &JobKind::FlowPreview || &job.job_kind == &JobKind::Flow; + if job.is_flow_step || !is_flow { + return Err(error::Error::BadRequest( + "This endpoint is only for root flow jobs".to_string(), + )); + } + let mut jobs = HashMap::new(); + jobs.insert("root_job".to_string(), job.clone()); + + let mut job_ids = vec![]; + let jobs_with_root = sqlx::query_scalar!( + "SELECT id FROM queue WHERE workspace_id = $1 and root_job = $2", + &w_id, + &job.id, + ) + .fetch_all(&db) + .await?; + + for job in jobs_with_root { + job_ids.push(job); + } + + let leaf_jobs: HashMap = job + .leaf_jobs + .and_then(|x| serde_json::from_value(x).ok()) + .unwrap_or_else(HashMap::new); + for job in leaf_jobs.iter() { + match job.1 { + JobResult::ListJob(jobs) => job_ids.extend(jobs.to_owned()), + JobResult::SingleJob(job) => job_ids.push(job.clone()), + } + } + for job_id in job_ids { + let job = get_queued_job(job_id, w_id.as_str(), &db).await?; + if let Some(job) = job { + jobs.insert(job.id.to_string(), job); + } + } + Ok(Json(jobs).into_response()) + } else { + Err(error::Error::NotFound(format!( + "QueuedJob {} not found", + id + ))) + } +} + async fn get_job( Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, @@ -749,9 +803,7 @@ async fn cancel_all( for j in jobs.iter() { if !j.running && !j.is_flow_step.unwrap_or(false) { let e = serde_json::json!({"message": format!("Job canceled: cancel_all by {username}"), "name": "Canceled", "reason": "cancel_all", "canceler": username}); - let mut tx = db.begin().await?; - let job_running = get_queued_job(j.id, &w_id, &mut tx).await?; - tx.commit().await?; + let job_running = get_queued_job(j.id, &w_id, &db).await?; if let Some(job_running) = job_running { let add_job = add_completed_job_error( @@ -1497,6 +1549,19 @@ impl Job { .flatten(), } } + pub fn is_flow_step(&self) -> bool { + match self { + Job::QueuedJob(job) => job.is_flow_step, + Job::CompletedJob(job) => job.is_flow_step, + } + } + + pub fn job_kind(&self) -> &JobKind { + match self { + Job::QueuedJob(job) => &job.job_kind, + Job::CompletedJob(job) => &job.job_kind, + } + } } #[derive(sqlx::FromRow)] diff --git a/backend/windmill-api/src/oidc.rs b/backend/windmill-api/src/oidc.rs index 917c144a14..4a03200b90 100644 --- a/backend/windmill-api/src/oidc.rs +++ b/backend/windmill-api/src/oidc.rs @@ -208,9 +208,7 @@ pub async fn gen_token( job.unwrap() } }; - let mut tx = db.begin().await?; - let job = get_queued_job(job_id, &w_id, &mut tx).await?; - tx.commit().await?; + let job = get_queued_job(job_id, &w_id, &db).await?; let job = job.ok_or_else(|| anyhow::anyhow!("Queued job {} not found", job_id))?; let issue_url = format!("{}/api/oidc/", crate::BASE_URL.read().await.clone()); diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index b6e5a208b3..c6c7c2d90d 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -131,7 +131,7 @@ pub async fn cancel_job<'c: 'async_recursion>( rsmq: Option, force_cancel: bool, ) -> error::Result<(Transaction<'c, Postgres>, Option)> { - let job_running = get_queued_job(id, &w_id, &mut tx).await?; + let job_running = get_queued_job_tx(id, &w_id, &mut tx).await?; if job_running.is_none() { return Ok((tx, None)); @@ -2166,7 +2166,7 @@ pub async fn job_is_complete(db: &DB, id: Uuid, w_id: &str) -> error::Result( +pub async fn get_queued_job_tx<'c>( id: Uuid, w_id: &str, tx: &mut Transaction<'c, Postgres>, @@ -2186,6 +2186,22 @@ pub async fn get_queued_job<'c>( } } +pub async fn get_queued_job(id: Uuid, w_id: &str, db: &DB) -> error::Result> { + let r = sqlx::query( + "SELECT * + FROM queue WHERE id = $1 AND workspace_id = $2", + ) + .bind(id) + .bind(w_id) + .fetch_optional(db) + .await?; + if let Some(row) = r { + Ok(Some(QueuedJob::from_row(&row)?.to_owned())) + } else { + Ok(None) + } +} + pub enum PushIsolationLevel<'c, R: rsmq_async::RsmqConnection + Send + 'c> { IsolatedRoot(DB, Option), Isolated(UserDB, Authed, Option), diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index e38d20fea3..4d81cd8509 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -2265,23 +2265,21 @@ pub async fn handle_job_error; -use windmill_queue::{canceled_job_to_result, get_queued_job, push, QueueTransaction}; +use windmill_queue::{canceled_job_to_result, get_queued_job_tx, push, QueueTransaction}; // #[instrument(level = "trace", skip_all)] pub async fn update_flow_status_after_job_completion< @@ -231,11 +231,9 @@ pub async fn update_flow_status_after_job_completion_internal< let (mut stop_early, skip_if_stop_early) = if let Some(se) = stop_early_override { //do not stop early if module is a flow step - let mut tx = db.begin().await?; - let flow_job = get_queued_job(flow, w_id, &mut tx) + let flow_job = get_queued_job(flow, w_id, db) .await? .ok_or_else(|| Error::InternalErr(format!("requiring flow to be in the queue")))?; - tx.commit().await?; let module = get_module(&flow_job, module_index); if module.is_some_and(|x| matches!(x.value, FlowModuleValue::Flow { .. })) { (false, false) @@ -524,7 +522,7 @@ pub async fn update_flow_status_after_job_completion_internal< .context("remove flow status retry")?; } - let flow_job = get_queued_job(flow, w_id, tx.transaction_mut()) + let flow_job = get_queued_job_tx(flow, w_id, tx.transaction_mut()) .await? .ok_or_else(|| Error::InternalErr(format!("requiring flow to be in the queue")))?; diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index 407fa23281..86ed7c9ef2 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -1,7 +1,7 @@ +{#if (job?.job_kind == 'flow' || job?.job_kind == 'flowpreview') && job?.['running'] && job?.parent_job == undefined} + + + + + +

+					
+			
+
+
+{/if} + (viewTab = 'result')} bind:this={testJobLoader} @@ -256,6 +302,22 @@ {@const stem = `/${job?.job_kind}s`} {@const isScript = job?.job_kind === 'script'} {@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`} + {#if (job?.job_kind == 'flow' || job?.job_kind == 'flowpreview') && job?.['running'] && job?.parent_job == undefined} +
+ + + + + + Show Flow Debug Info + + +
+ {/if} {#if persistentScriptDefinition !== undefined}