Merge remote-tracking branch 'origin/main' into http-trigger-cors-config

# Conflicts:
#	backend/windmill-trigger-http/src/handler.rs
#	frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts
This commit is contained in:
hugocasa
2026-08-28 17:43:00 +02:00
196 changed files with 9129 additions and 1738 deletions
+13 -4
View File
@@ -270,6 +270,15 @@ pub async fn create_many_http_triggers(
format!("http_triggers:write:{}", &new_http_trigger.base.path)
})?;
// This route inserts directly, bypassing the shared create handler.
// `error_wrapper` would turn the rejection into a 500.
new_http_trigger.error_handling.validate().map_err(|err| {
Error::BadRequest(format!(
"Error occurred for HTTP route at route path: {}, error: {}",
new_http_trigger.config.route_path, err
))
})?;
handler
.validate_new(&db, &w_id, &new_http_trigger.config)
.await
@@ -578,8 +587,8 @@ impl TriggerCrud for HttpTrigger {
route_path,
&route_path_key,
Some(effective_workspaced),
trigger.config.wrap_body,
trigger.config.raw_string,
trigger.config.wrap_body.unwrap_or(false),
trigger.config.raw_string.unwrap_or(false),
trigger.config.allowed_origins.as_deref(),
trigger.config.authentication_resource_path,
trigger.base.script_path,
@@ -636,8 +645,8 @@ impl TriggerCrud for HttpTrigger {
workspace_id = $21 AND
path = $22
"#,
trigger.config.wrap_body,
trigger.config.raw_string,
trigger.config.wrap_body.unwrap_or(false),
trigger.config.raw_string.unwrap_or(false),
trigger.config.allowed_origins.as_deref(),
trigger.config.authentication_resource_path,
trigger.base.script_path,
+33 -5
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use quick_cache::sync::Cache;
use serde::{Deserialize, Serialize};
@@ -29,9 +30,12 @@ lazy_static::lazy_static! {
pub static ref HTTP_ROUTERS_CACHE: RwLock<RoutersCache> = RwLock::new(RoutersCache {
routers: HashMap::new(),
version: 0,
invalidations: 0,
});
}
static HTTP_ROUTERS_INVALIDATIONS: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Deserialize, Clone)]
pub struct TriggerRoute {
pub path: String,
@@ -59,6 +63,10 @@ pub struct TriggerRoute {
pub struct RoutersCache {
pub routers: HashMap<HttpMethod, matchit::Router<TriggerRoute>>,
pub version: i64,
/// `HTTP_ROUTERS_INVALIDATIONS` as of the moment these rows were read. A rebuild that
/// started before an invalidation publishes a count behind the current one, which is what
/// stops it from passing its own stale rows off as covering that invalidation.
invalidations: u64,
}
#[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, Hash, Eq, PartialEq)]
@@ -275,12 +283,24 @@ pub fn validate_authentication_method(
}
}
pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, RoutersCache>)> {
/// `force` rebuilds unconditionally. `nextval` on `http_trigger_version_seq` runs inside the
/// writing transaction and sequences are non-transactional, so another session can cache the
/// bumped version against still-uncommitted rows, after which every version-gated refresh is a
/// no-op. Force when reacting to a bump that could have been observed before its own rows were.
pub async fn refresh_routers(
db: &DB,
force: bool,
) -> Result<(bool, RwLockReadGuard<'_, RoutersCache>)> {
let invalidations = HTTP_ROUTERS_INVALIDATIONS.load(Ordering::Relaxed);
let version = sqlx::query_scalar!("SELECT last_value FROM http_trigger_version_seq",)
.fetch_one(db)
.await?;
let routers_cache = HTTP_ROUTERS_CACHE.read().await;
if routers_cache.version == 0 || version > routers_cache.version {
if force
|| routers_cache.version == 0
|| version > routers_cache.version
|| invalidations != routers_cache.invalidations
{
drop(routers_cache);
let mut routers = HashMap::new();
@@ -360,7 +380,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
}
let mut routers_cache = HTTP_ROUTERS_CACHE.write().await;
*routers_cache = RoutersCache { routers, version };
*routers_cache = RoutersCache { routers, version, invalidations };
Ok((true, routers_cache.downgrade()))
} else {
@@ -369,11 +389,19 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
}
}
/// Record that the cache no longer covers everything committed, so the next refresh rebuilds
/// whatever the version says. The routes already loaded keep being served in the meantime. Use
/// after a forced refresh fails: its change is inside the cached version, so nothing else would
/// retry it.
pub fn invalidate_routers() {
HTTP_ROUTERS_INVALIDATIONS.fetch_add(1, Ordering::Relaxed);
}
pub async fn refresh_routers_loop(
db: &DB,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
match refresh_routers(db).await {
match refresh_routers(db, false).await {
Ok(_) => {
tracing::info!("Loaded HTTP routers");
}
@@ -389,7 +417,7 @@ pub async fn refresh_routers_loop(
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
match refresh_routers(&db).await {
match refresh_routers(&db, false).await {
Ok((true, _)) => {
tracing::info!("Refreshed HTTP routers");
}