UI and http triggers

This commit is contained in:
HugoCasa
2025-11-28 16:49:46 +01:00
parent b95a8586b9
commit b7d4c63456
27 changed files with 974 additions and 551 deletions
+108 -55
View File
@@ -3900,6 +3900,7 @@ async fn batch_rerun_handle_job(
let result = push_flow_job_by_path_into_queue(
authed.clone(),
db.clone(),
None,
user_db.clone(),
w_id.clone(),
StripPath(job.script_path.clone()),
@@ -3908,7 +3909,7 @@ async fn batch_rerun_handle_job(
None,
)
.await;
if let Ok((uuid, _)) = result {
if let Ok((uuid, _, _)) = result {
return Ok(uuid.to_string());
}
}
@@ -3917,6 +3918,7 @@ async fn batch_rerun_handle_job(
push_script_job_by_path_into_queue(
authed.clone(),
db.clone(),
None,
user_db.clone(),
w_id.clone(),
StripPath(job.script_path.clone()),
@@ -3925,6 +3927,7 @@ async fn batch_rerun_handle_job(
None,
)
.await
.map(|r| r.0)
} else {
run_job_by_hash_inner(
authed.clone(),
@@ -3937,8 +3940,9 @@ async fn batch_rerun_handle_job(
None,
)
.await
.map(|r| r.0)
};
if let Ok((uuid, _)) = result {
if let Ok(uuid) = result {
return Ok(uuid.to_string());
}
}
@@ -4056,17 +4060,18 @@ pub async fn run_flow_by_path(
)
.await?;
let (uuid, _) = push_flow_job_by_path_into_queue(
authed, db, user_db, w_id, flow_path, run_query, args, None,
let (uuid, _, _) = push_flow_job_by_path_into_queue(
authed, db, None, user_db, w_id, flow_path, run_query, args, None,
)
.await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
pub async fn run_flow(
pub async fn run_flow<'c>(
authed: &ApiAuthed,
db: &DB,
tx_o: Option<sqlx::Transaction<'c, sqlx::Postgres>>,
user_db: UserDB,
w_id: &str,
flow_path: &str,
@@ -4074,7 +4079,11 @@ pub async fn run_flow(
run_query: RunJobQuery,
args: PushArgsOwned,
trigger: Option<TriggerMetadata>,
) -> error::Result<(Uuid, Option<String>)> {
) -> error::Result<(
Uuid,
Option<String>,
Option<sqlx::Transaction<'c, sqlx::Postgres>>,
)> {
let FlowVersionInfo {
version,
tag,
@@ -4092,22 +4101,30 @@ pub async fn run_flow(
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of_email) = on_behalf_of_email.as_ref() {
(
on_behalf_of_email,
username_to_permissioned_as(&edited_by),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let return_tx = tx_o.is_some();
let (email, permissioned_as, push_authed, tx) = if let Some(tx) = tx_o {
(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Transaction(tx),
)
} else if let Some(on_behalf_of_email) = on_behalf_of_email.as_ref() {
(
on_behalf_of_email,
username_to_permissioned_as(&edited_by),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let (uuid, mut tx) = push(
&db,
@@ -4166,9 +4183,13 @@ pub async fn run_flow(
.await?;
}
tx.commit().await?;
Ok((uuid, early_return))
// If we were given a transaction, return it; otherwise commit it
if return_tx {
Ok((uuid, early_return, Some(tx)))
} else {
tx.commit().await?;
Ok((uuid, early_return, None))
}
}
pub async fn run_flow_and_wait_result(
@@ -4182,9 +4203,10 @@ pub async fn run_flow_and_wait_result(
args: PushArgsOwned,
trigger: Option<TriggerMetadata>,
) -> error::Result<Response> {
let (uuid, early_return) = run_flow(
let (uuid, early_return, _) = run_flow(
authed,
db,
None,
user_db,
w_id,
flow_path,
@@ -4198,16 +4220,21 @@ pub async fn run_flow_and_wait_result(
run_wait_result(&db, uuid, w_id, early_return, &authed.username).await
}
pub async fn push_flow_job_by_path_into_queue(
pub async fn push_flow_job_by_path_into_queue<'c>(
authed: ApiAuthed,
db: DB,
tx_o: Option<sqlx::Transaction<'c, sqlx::Postgres>>,
user_db: UserDB,
w_id: String,
flow_path: StripPath,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger: Option<TriggerMetadata>,
) -> error::Result<(Uuid, Option<String>)> {
) -> error::Result<(
Uuid,
Option<String>,
Option<sqlx::Transaction<'c, sqlx::Postgres>>,
)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -4223,6 +4250,7 @@ pub async fn push_flow_job_by_path_into_queue(
run_flow(
&authed,
&db,
tx_o,
user_db,
&w_id,
flow_path,
@@ -4293,9 +4321,10 @@ pub async fn run_flow_by_version_inner(
let flow_version_info =
get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?;
run_flow(
let (uuid, early_return, _) = run_flow(
&authed,
&db,
None,
user_db,
&w_id,
&flow_path,
@@ -4304,7 +4333,9 @@ pub async fn run_flow_by_version_inner(
args,
trigger,
)
.await
.await?;
Ok((uuid, early_return))
}
#[cfg(not(feature = "enterprise"))]
@@ -4425,9 +4456,10 @@ pub async fn run_script_by_path(
)
.await?;
let (uuid, _) = push_script_job_by_path_into_queue(
let (uuid, _, _) = push_script_job_by_path_into_queue(
authed,
db,
None,
user_db,
w_id,
script_path,
@@ -4440,16 +4472,21 @@ pub async fn run_script_by_path(
Ok((StatusCode::CREATED, uuid.to_string()))
}
pub async fn push_script_job_by_path_into_queue(
pub async fn push_script_job_by_path_into_queue<'c>(
authed: ApiAuthed,
db: DB,
tx_o: Option<sqlx::Transaction<'c, sqlx::Postgres>>,
user_db: UserDB,
w_id: String,
script_path: StripPath,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger: Option<TriggerMetadata>,
) -> error::Result<(Uuid, Option<bool>)> {
) -> error::Result<(
Uuid,
Option<bool>,
Option<sqlx::Transaction<'c, sqlx::Postgres>>,
)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -4470,22 +4507,30 @@ pub async fn push_script_job_by_path_into_queue(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of) = on_behalf_of.as_ref() {
(
on_behalf_of.email.as_str(),
on_behalf_of.permissioned_as.clone(),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
authed.email.as_str(),
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
)
};
let return_tx = tx_o.is_some();
let (email, permissioned_as, push_authed, tx) = if let Some(tx) = tx_o {
(
authed.email.as_str(),
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Transaction(tx),
)
} else if let Some(on_behalf_of) = on_behalf_of.as_ref() {
(
on_behalf_of.email.as_str(),
on_behalf_of.permissioned_as.clone(),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
authed.email.as_str(),
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
)
};
let (uuid, tx) = push(
&db,
@@ -4524,9 +4569,14 @@ pub async fn push_script_job_by_path_into_queue(
run_query.suspended_mode,
)
.await?;
tx.commit().await?;
Ok((uuid, delete_after_use))
// If we were given a transaction, return it; otherwise commit it
if return_tx {
Ok((uuid, delete_after_use, Some(tx)))
} else {
tx.commit().await?;
Ok((uuid, delete_after_use, None))
}
}
#[derive(Deserialize)]
@@ -5652,9 +5702,10 @@ pub async fn stream_job(
let uuid = match runnable_id {
RunnableId::ScriptId(ScriptId::ScriptPath(script_path))
| RunnableId::HubScript(script_path) => {
push_script_job_by_path_into_queue(
let (uuid, _, _) = push_script_job_by_path_into_queue(
authed.clone(),
db.clone(),
None,
user_db,
w_id.clone(),
StripPath(script_path),
@@ -5662,8 +5713,8 @@ pub async fn stream_job(
args,
None,
)
.await?
.0
.await?;
uuid
}
RunnableId::ScriptId(ScriptId::ScriptHash(script_hash)) => {
run_job_by_hash_inner(
@@ -5683,6 +5734,7 @@ pub async fn stream_job(
push_flow_job_by_path_into_queue(
authed.clone(),
db.clone(),
None,
user_db,
w_id.clone(),
StripPath(flow_path),
@@ -6738,9 +6790,10 @@ async fn run_dynamic_select(
let push_args = PushArgsOwned { extra: None, args: script_args.clone() };
let (uuid, _) = push_script_job_by_path_into_queue(
let (uuid, _, _) = push_script_job_by_path_into_queue(
authed.clone(),
db.clone(),
None,
user_db.clone(),
w_id.clone(),
StripPath(path),
@@ -1,10 +1,10 @@
use crate::{
db::{ApiAuthed, DB},
triggers::{trigger_helpers::trigger_runnable_inner, TriggerForReassignment, Trigger, TriggerCrud},
triggers::{trigger_helpers::trigger_runnable_inner, TriggerCrud, TriggerForReassignment},
};
#[cfg(feature = "http_trigger")]
use crate::triggers::http::{handler::HttpTrigger, HttpConfig};
use crate::triggers::http::handler::HttpTrigger;
#[cfg(feature = "mqtt_trigger")]
use crate::triggers::mqtt::{MqttConfig, MqttTrigger};
@@ -13,7 +13,7 @@ use crate::triggers::mqtt::{MqttConfig, MqttTrigger};
use crate::triggers::postgres::{PostgresConfig, PostgresTrigger};
#[cfg(feature = "websocket")]
use crate::triggers::websocket::{WebsocketConfig, WebsocketTrigger};
use crate::triggers::websocket::WebsocketTrigger;
#[cfg(all(feature = "smtp", feature = "enterprise", feature = "private"))]
use crate::triggers::email::{EmailConfig, EmailTrigger};
@@ -33,6 +33,7 @@ use axum::{
extract::{Extension, Path},
response::Json,
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::collections::HashMap;
use uuid::Uuid;
@@ -41,13 +42,21 @@ use windmill_common::{db::UserDB, error, jobs::JobTriggerKind, triggers::Trigger
struct JobWithArgs {
id: Uuid,
args: Option<sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
created_at: chrono::DateTime<chrono::Utc>,
}
pub async fn reassign_suspended_jobs(
#[derive(Deserialize, Serialize, Default)]
pub struct ReassignJobsBody {
#[serde(default)]
pub job_ids: Option<Vec<Uuid>>,
}
pub async fn resume_suspended_trigger_jobs(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, trigger_kind, trigger_path)): Path<(String, JobTriggerKind, String)>,
Json(body): Json<ReassignJobsBody>,
) -> error::Result<Json<String>> {
let mut tx = user_db.clone().begin(&authed).await?;
@@ -185,70 +194,206 @@ pub async fn reassign_suspended_jobs(
}
};
let jobs = sqlx::query_as!(JobWithArgs,
"SELECT id, args as \"args: _\" FROM v2_job WHERE workspace_id = $1 AND kind = 'unassigned'::JOB_KIND AND trigger_kind = $2 AND trigger = $3",
w_id,
trigger_kind as _,
trigger_path,
).fetch_all(&mut *tx).await?;
let jobs = if let Some(job_ids) = body.job_ids.as_ref() {
if job_ids.is_empty() {
vec![]
} else {
sqlx::query_as!(
JobWithArgs,
"SELECT id, args as \"args: _\", created_at FROM v2_job
WHERE workspace_id = $1
AND kind = 'unassigned'::JOB_KIND
AND trigger_kind = $2
AND trigger = $3
AND id = ANY($4)",
w_id,
trigger_kind as _,
trigger_path,
job_ids as _,
)
.fetch_all(&mut *tx)
.await?
}
} else {
sqlx::query_as!(
JobWithArgs,
"SELECT id, args as \"args: _\", created_at FROM v2_job
WHERE workspace_id = $1
AND kind = 'unassigned'::JOB_KIND
AND trigger_kind = $2
AND trigger = $3",
w_id,
trigger_kind as _,
trigger_path,
)
.fetch_all(&mut *tx)
.await?
};
let trigger_metadata = TriggerMetadata::new(Some(trigger_path.clone()), trigger_kind);
let l = jobs.len();
for job in jobs {
trigger_runnable_inner(
&db,
Some(user_db.clone()),
authed.clone(),
&w_id,
&trigger.script_path,
trigger.is_flow,
windmill_queue::PushArgsOwned {
extra: None,
args: job.args.map(|a| a.0).unwrap_or_default(),
},
trigger.retry.as_ref(),
trigger.error_handler_path.as_deref(),
trigger.error_handler_args.as_ref(),
trigger_path.clone(),
None,
trigger_metadata.clone(),
None,
)
.await?;
// If job was created before trigger was edited, simply update it to unsuspend
// instead of deleting and repushing
if job.created_at > trigger.edited_at {
let job_kind = if trigger.is_flow {
windmill_common::jobs::JobKind::Flow
} else {
windmill_common::jobs::JobKind::Script
};
// Delete the unassigned job from all related tables
sqlx::query!("DELETE FROM v2_job_queue WHERE id = $1", job.id)
sqlx::query!(
"UPDATE v2_job SET kind = $1 WHERE id = $2",
job_kind as _,
job.id
)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM v2_job_runtime WHERE id = $1", job.id)
// Update the job to unsuspend it and set the correct kind
sqlx::query!(
"UPDATE v2_job_queue SET scheduled_for = now() WHERE id = $1",
job.id
)
.execute(&mut *tx)
.await?;
} else {
// Job was created after trigger edit - delete and repush with new configuration
// Pass the transaction to trigger_runnable_inner so everything is in the same transaction
let (_uuid, _delete_after_use, _early_return, tx_o) = trigger_runnable_inner(
&db,
Some(tx),
Some(user_db.clone()),
authed.clone(),
&w_id,
&trigger.script_path,
trigger.is_flow,
windmill_queue::PushArgsOwned {
extra: None,
args: job.args.map(|a| a.0).unwrap_or_default(),
},
trigger.retry.as_ref(),
trigger.error_handler_path.as_deref(),
trigger.error_handler_args.as_ref(),
trigger_path.clone(),
None,
trigger_metadata.clone(),
None,
)
.await?;
sqlx::query!("DELETE FROM job_perms WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
tx = match tx_o {
Some(tx) => tx,
None => {
return Err(error::Error::internal_err(
"Transaction should be returned when passed in".to_string(),
));
}
};
sqlx::query!("DELETE FROM concurrency_key WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
// Delete the unassigned job from all related tables
sqlx::query!("DELETE FROM v2_job_queue WHERE id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM debounce_key WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM v2_job_runtime WHERE id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM debounce_stale_data WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM job_perms WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM v2_job WHERE id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM concurrency_key WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM debounce_key WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM debounce_stale_data WHERE job_id = $1", job.id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM v2_job WHERE id = $1", job.id)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
Ok(Json(format!("Reassigned {} jobs", l)))
}
pub async fn cancel_suspended_trigger_jobs(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, trigger_kind, trigger_path)): Path<(String, JobTriggerKind, String)>,
Json(body): Json<ReassignJobsBody>,
) -> error::Result<Json<String>> {
let mut tx = user_db.clone().begin(&authed).await?;
// Get the list of job IDs to cancel
let jobs_to_cancel = if let Some(job_ids) = body.job_ids.as_ref() {
if job_ids.is_empty() {
vec![]
} else {
sqlx::query_scalar!(
"SELECT id FROM v2_job
WHERE workspace_id = $1
AND kind = 'unassigned'::JOB_KIND
AND trigger_kind = $2
AND trigger = $3
AND id = ANY($4)",
w_id,
trigger_kind as _,
trigger_path,
job_ids as _,
)
.fetch_all(&mut *tx)
.await?
}
} else {
sqlx::query_scalar!(
"SELECT id FROM v2_job
WHERE workspace_id = $1
AND kind = 'unassigned'::JOB_KIND
AND trigger_kind = $2
AND trigger = $3",
w_id,
trigger_kind as _,
trigger_path,
)
.fetch_all(&mut *tx)
.await?
};
let count = jobs_to_cancel.len();
if count > 0 {
// Use the cancel_jobs helper from windmill_queue
for job_id in &jobs_to_cancel {
let (returned_tx, _) = windmill_queue::cancel_job(
&authed.username,
Some("canceled by trigger management".to_string()),
*job_id,
&w_id,
tx,
&db,
false,
false,
)
.await?;
tx = returned_tx;
}
}
tx.commit().await?;
Ok(Json(format!("Canceled {} jobs", count)))
}
+134 -17
View File
@@ -37,8 +37,7 @@ pub struct TriggerForReassignment {
pub email: String,
pub edited_at: DateTime<Utc>,
pub error_handler_path: Option<String>,
pub error_handler_args:
Option<sqlx::types::Json<HashMap<String, serde_json::Value>>>,
pub error_handler_args: Option<sqlx::types::Json<HashMap<String, serde_json::Value>>>,
pub retry: Option<sqlx::types::Json<windmill_common::flows::Retry>>,
}
@@ -428,7 +427,8 @@ pub fn trigger_routes<T: TriggerCrud + 'static>() -> Router {
.route("/update/*path", post(update_trigger::<T>))
.route("/delete/*path", delete(delete_trigger::<T>))
.route("/exists/*path", get(exists_trigger::<T>))
.route("/setenabled/*path", post(set_enabled_trigger::<T>));
.route("/setenabled/*path", post(set_enabled_trigger::<T>))
.route("/update_status/*path", post(update_trigger_status::<T>));
if T::SUPPORTS_TEST_CONNECTION {
router = router.route("/test", post(test_connection::<T>));
@@ -651,6 +651,123 @@ struct SetEnabledPayload {
enabled: bool,
}
#[derive(Deserialize)]
struct UpdateStatusPayload {
suspended_mode: bool,
enabled: bool,
}
async fn update_trigger_status<T: TriggerCrud>(
Extension(handler): Extension<Arc<T>>,
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((workspace_id, path)): Path<(String, StripPath)>,
Json(payload): Json<UpdateStatusPayload>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("{}:write", T::scope_domain_name()))?;
let mut tx = user_db.begin(&authed).await?;
let updated = if T::SUPPORTS_SERVER_STATE {
sqlx::query(&format!(
r#"
UPDATE
{}
SET
suspended_mode = $1,
enabled = $2,
email = $3,
edited_by = $4,
edited_at = now(),
server_id = NULL,
error = NULL
WHERE
workspace_id = $5 AND
path = $6
"#,
T::TABLE_NAME
))
.bind(payload.suspended_mode)
.bind(payload.suspended_mode || payload.enabled)
.bind(&authed.email)
.bind(&authed.username)
.bind(&workspace_id)
.bind(path)
.execute(&mut *tx)
.await?
.rows_affected()
} else {
sqlx::query(&format!(
r#"
UPDATE
{}
SET
suspended_mode = $1,
enabled = $2,
email = $3,
edited_by = $4,
edited_at = now()
WHERE
workspace_id = $5 AND
path = $6
"#,
T::TABLE_NAME
))
.bind(payload.suspended_mode)
.bind(payload.suspended_mode || payload.enabled)
.bind(&authed.email)
.bind(&authed.username)
.bind(&workspace_id)
.bind(path)
.execute(&mut *tx)
.await?
.rows_affected()
};
if updated == 0 {
return Err(Error::NotFound(format!(
"Trigger not found at path: {}",
path
)));
}
handler.set_enabled_extra_action(&mut *tx).await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&workspace_id,
T::get_deployed_object(path.to_owned()),
Some(format!(
"{} trigger '{}' status updated",
T::DEPLOYMENT_NAME,
path
)),
true,
)
.await?;
Ok(format!(
"Trigger '{}' updated: {} mode, {}",
path,
if payload.suspended_mode {
"suspended"
} else {
"active"
},
if payload.enabled {
"enabled"
} else {
"disabled"
}
))
}
async fn set_enabled_trigger<T: TriggerCrud>(
Extension(handler): Extension<Arc<T>>,
authed: ApiAuthed,
@@ -825,21 +942,21 @@ pub fn generate_trigger_routers() -> Router {
);
}
// {
// use crate::triggers::global_handler::{
// cancel_suspended_trigger_jobs, resume_suspended_trigger_jobs,
// };
{
use crate::triggers::global_handler::{
cancel_suspended_trigger_jobs, resume_suspended_trigger_jobs,
};
// router = router
// .route(
// "/trigger/:trigger_kind/resume_suspended_trigger_job/*trigger_path",
// post(resume_suspended_trigger_jobs),
// )
// .route(
// "/trigger/:trigger_kind/cancel_suspended_trigger_job/*trigger_path",
// post(cancel_suspended_trigger_jobs),
// );
// }
router = router
.route(
"/trigger/:trigger_kind/resume_suspended_trigger_job/*trigger_path",
post(resume_suspended_trigger_jobs),
)
.route(
"/trigger/:trigger_kind/cancel_suspended_trigger_job/*trigger_path",
post(cancel_suspended_trigger_jobs),
);
}
router
}
@@ -41,7 +41,7 @@ use windmill_common::{
db::UserDB,
error::{Error, Result},
jobs::JobTriggerKind,
triggers::{TriggerMetadata, TriggerKind},
triggers::{TriggerKind, TriggerMetadata},
utils::{not_found_if_none, require_admin, StripPath},
worker::CLOUD_HOSTED,
};
@@ -1050,7 +1050,7 @@ async fn route_job(
.map_err(|e| e.into_response())?;
let trigger_info = TriggerMetadata::new(Some(trigger.path.clone()), JobTriggerKind::Http);
if !trigger.suspended_mode {
if trigger.suspended_mode {
let _ = trigger_runnable(
&db,
Some(user_db),
@@ -1064,7 +1064,7 @@ async fn route_job(
trigger.error_handler_args.as_ref(),
format!("http_trigger/{}", trigger.path),
None,
trigger.suspended_mode,
true,
trigger_info,
)
.await
@@ -1073,7 +1073,7 @@ async fn route_job(
return Ok((
StatusCode::OK,
format!(
"Trigger: {} in inactive mode, incoming request has been queued",
"Trigger {} is in suspended mode, jobs are added to the queue but suspended",
&trigger.path
),
)
@@ -1084,8 +1084,9 @@ async fn route_job(
match trigger.request_type {
RequestType::SyncSse => {
// Trigger the job (always async when streaming)
let (uuid, _, _) = trigger_runnable_inner(
let (uuid, _, _, _) = trigger_runnable_inner(
&db,
None,
Some(user_db.clone()),
authed.clone(),
&trigger.workspace_id,
@@ -1157,7 +1158,7 @@ async fn route_job(
trigger.error_handler_args.as_ref(),
format!("http_trigger/{}", trigger.path),
None,
trigger.suspended_mode,
false,
trigger_info,
)
.await
@@ -1,6 +1,5 @@
use anyhow::Context;
use axum::response::IntoResponse;
use chrono::{DateTime, Utc};
use http::StatusCode;
use serde::Deserialize;
use serde_json::value::RawValue;
@@ -505,8 +504,9 @@ pub trait TriggerJobArgs {
}
#[allow(dead_code)]
pub async fn trigger_runnable_inner(
pub async fn trigger_runnable_inner<'c>(
db: &DB,
tx_o: Option<sqlx::Transaction<'c, sqlx::Postgres>>,
user_db: Option<UserDB>,
authed: ApiAuthed,
workspace_id: &str,
@@ -520,7 +520,12 @@ pub async fn trigger_runnable_inner(
job_id: Option<Uuid>,
trigger: TriggerMetadata,
suspended_mode: Option<bool>,
) -> Result<(Uuid, Option<bool>, Option<String>)> {
) -> Result<(
Uuid,
Option<bool>,
Option<String>,
Option<sqlx::Transaction<'c, sqlx::Postgres>>,
)> {
let error_handler_args = error_handler_args.map(|args| {
let args = args
.0
@@ -531,12 +536,13 @@ pub async fn trigger_runnable_inner(
});
let user_db = user_db.unwrap_or_else(|| UserDB::new(db.clone()));
let (uuid, delete_after_use, early_return) = if is_flow {
let (uuid, delete_after_use, early_return, tx_out) = if is_flow {
let run_query = RunJobQuery { job_id, suspended_mode, ..Default::default() };
let path = StripPath(runnable_path.to_string());
let (uuid, early_return) = push_flow_job_by_path_into_queue(
let (uuid, early_return, tx_out) = push_flow_job_by_path_into_queue(
authed,
db.clone(),
tx_o,
user_db,
workspace_id.to_string(),
path,
@@ -545,10 +551,11 @@ pub async fn trigger_runnable_inner(
Some(trigger),
)
.await?;
(uuid, None, early_return)
(uuid, None, early_return, tx_out)
} else {
let (uuid, delete_after_use) = trigger_script_internal(
let (uuid, delete_after_use, tx_out) = trigger_script_internal(
db,
tx_o,
user_db,
authed,
workspace_id,
@@ -563,10 +570,10 @@ pub async fn trigger_runnable_inner(
suspended_mode,
)
.await?;
(uuid, delete_after_use, None)
(uuid, delete_after_use, None, tx_out)
};
Ok((uuid, delete_after_use, early_return))
Ok((uuid, delete_after_use, early_return, tx_out))
}
#[allow(dead_code)]
@@ -588,6 +595,7 @@ pub async fn trigger_runnable(
) -> Result<axum::response::Response> {
let uuid = trigger_runnable_inner(
db,
None,
user_db,
authed,
workspace_id,
@@ -623,8 +631,9 @@ pub async fn trigger_runnable_and_wait_for_result(
trigger: TriggerMetadata,
) -> Result<axum::response::Response> {
let username = authed.username.clone();
let (uuid, delete_after_use, early_return) = trigger_runnable_inner(
let (uuid, delete_after_use, early_return, _) = trigger_runnable_inner(
db,
None,
user_db,
authed,
workspace_id,
@@ -666,8 +675,9 @@ pub async fn trigger_runnable_and_wait_for_raw_result(
trigger: TriggerMetadata,
) -> Result<(Box<RawValue>, bool)> {
let username = authed.username.clone();
let (uuid, delete_after_use, early_return) = trigger_runnable_inner(
let (uuid, delete_after_use, early_return, _) = trigger_runnable_inner(
db,
None,
user_db,
authed,
workspace_id,
@@ -743,8 +753,9 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx(
}
}
async fn trigger_script_internal(
async fn trigger_script_internal<'c>(
db: &DB,
tx_o: Option<sqlx::Transaction<'c, sqlx::Postgres>>,
user_db: UserDB,
authed: ApiAuthed,
workspace_id: &str,
@@ -757,13 +768,18 @@ async fn trigger_script_internal(
job_id: Option<Uuid>,
trigger: TriggerMetadata,
suspended_mode: Option<bool>,
) -> Result<(Uuid, Option<bool>)> {
) -> Result<(
Uuid,
Option<bool>,
Option<sqlx::Transaction<'c, sqlx::Postgres>>,
)> {
if retry.is_none() && error_handler_path.is_none() {
let run_query = RunJobQuery { job_id, suspended_mode, ..Default::default() };
let path = StripPath(script_path.to_string());
push_script_job_by_path_into_queue(
let (uuid, delete_after_use, tx_out) = push_script_job_by_path_into_queue(
authed,
db.clone(),
tx_o,
user_db,
workspace_id.to_string(),
path,
@@ -771,10 +787,12 @@ async fn trigger_script_internal(
args,
Some(trigger),
)
.await
.await?;
Ok((uuid, delete_after_use, tx_out))
} else {
trigger_script_with_retry_and_error_handler(
let (uuid, delete_after_use, tx_out) = trigger_script_with_retry_and_error_handler(
db,
tx_o,
user_db,
authed,
workspace_id,
@@ -788,12 +806,14 @@ async fn trigger_script_internal(
trigger,
suspended_mode,
)
.await
.await?;
Ok((uuid, delete_after_use, tx_out))
}
}
async fn trigger_script_with_retry_and_error_handler(
async fn trigger_script_with_retry_and_error_handler<'c>(
db: &DB,
tx_o: Option<sqlx::Transaction<'c, sqlx::Postgres>>,
user_db: UserDB,
authed: ApiAuthed,
workspace_id: &str,
@@ -806,7 +826,11 @@ async fn trigger_script_with_retry_and_error_handler(
job_id: Option<Uuid>,
trigger: TriggerMetadata,
suspended_mode: Option<bool>,
) -> Result<(Uuid, Option<bool>)> {
) -> Result<(
Uuid,
Option<bool>,
Option<sqlx::Transaction<'c, sqlx::Postgres>>,
)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -830,22 +854,30 @@ async fn trigger_script_with_retry_and_error_handler(
check_tag_available_for_workspace(&db, &workspace_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of) = on_behalf_of.as_ref() {
(
on_behalf_of.email.as_str(),
on_behalf_of.permissioned_as.clone(),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
authed.email.as_str(),
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
)
};
let return_tx = tx_o.is_some();
let (email, permissioned_as, push_authed, tx) = if let Some(tx) = tx_o {
(
authed.email.as_str(),
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Transaction(tx),
)
} else if let Some(on_behalf_of) = on_behalf_of.as_ref() {
(
on_behalf_of.email.as_str(),
on_behalf_of.permissioned_as.clone(),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
authed.email.as_str(),
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
)
};
let push_args = PushArgs { args: &args.args, extra: args.extra };
@@ -921,7 +953,12 @@ async fn trigger_script_with_retry_and_error_handler(
suspended_mode,
)
.await?;
tx.commit().await?;
Ok((uuid, delete_after_use))
// If we were given a transaction, return it; otherwise commit it
if return_tx {
Ok((uuid, delete_after_use, Some(tx)))
} else {
tx.commit().await?;
Ok((uuid, delete_after_use, None))
}
}