Files
windmill/backend/windmill-common/src/min_version.rs
T
5b3913052e refactor: convert read-hot globals to AtomicBool/I64 and ArcSwap (#8815)
* refactor: extract load helpers from reload_setting family

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: convert atomic primitive globals to AtomicBool/AtomicI64

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: convert CRITICAL_*/HUB_API_SECRET/INSTANCE_EVENTS_WEBHOOK/JWT_SECRET to ArcSwap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: pin ee-repo-ref to arcswap-refactor EE branch commit

* refactor: convert BASE_URL/HUB_BASE_URL/MIN_VERSION/LICENSE_KEY*/LICENSE_KEY_ID to ArcSwap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: convert worker hot-path globals to ArcSwap (WORKER_CONFIG et al)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: pin ee-repo-ref to combined arcswap-urls+worker EE commit

* chore: update ee-repo-ref to d8be8f88cb8898c8f6b27421989d53528223815d

This commit updates the EE repository reference after PR #532 was merged in windmill-ee-private.

Previous ee-repo-ref: c375aaaac9ec0fc0480993627d0defc8054c31a4

New ee-repo-ref: d8be8f88cb8898c8f6b27421989d53528223815d

Automated by sync-ee-ref workflow.

* fix: cleanup unused imports + fix 2 missed WORKER_CONFIG readers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to ce0f8fbbbde09c4a858312d2d8716d224e99042c

This commit updates the EE repository reference after PR #534 was merged in windmill-ee-private.

Previous ee-repo-ref: 450b601b5aba0ca0b2045f4b5071aa8701b4bfb7

New ee-repo-ref: ce0f8fbbbde09c4a858312d2d8716d224e99042c

Automated by sync-ee-ref workflow.

* fix: secret_backend_integration test — BASE_URL.write().await → .store()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: convert APP_WORKSPACED_ROUTE to AtomicBool for symmetry with HTTP_ROUTE_WORKSPACED_ROUTE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to e587df8 (post-#535 merge)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-04-14 00:04:10 +00:00

247 lines
9.8 KiB
Rust

use crate::error::{self, Error};
use semver::Version;
// ============ Feature Definitions ============
pub const MIN_VERSION_SUPPORTS_NODE_DEBOUNCING: VC = vc(1, 658, 0, "Flow node debouncing");
pub const MIN_VERSION_SUPPORTS_TOKEN_HASH: VC = vc(1, 659, 0, "Token hash storage");
pub const MIN_VERSION_SUPPORTS_SYNC_JOBS_DEBOUNCING: VC = vc(1, 602, 0, "Sync jobs debouncing");
pub const MIN_VERSION_SUPPORTS_DEBOUNCING_V2: VC = vc(1, 597, 0, "Debouncing V2");
pub const MIN_VERSION_IS_AT_LEAST_1_595: VC = vc(1, 595, 0, "Flow status separate table");
pub const MIN_VERSION_SUPPORTS_RUNNABLE_SETTINGS_V0: VC = vc(1, 592, 0, "Runnable settings V0");
pub const MIN_VERSION_SUPPORTS_V0_WORKSPACE_DEPENDENCIES: VC =
vc(1, 587, 0, "Workspace dependencies");
pub const MIN_VERSION_SUPPORTS_DEBOUNCING: VC = vc(1, 566, 0, "Debouncing");
pub const MIN_VERSION_IS_AT_LEAST_1_440: VC = vc(1, 440, 0, "Flow node value on pull");
pub const MIN_VERSION_IS_AT_LEAST_1_432: VC = vc(1, 432, 0, "Flow script job kind");
pub const MIN_VERSION_IS_AT_LEAST_1_427: VC = vc(1, 427, 0, "Flow version lite table");
// TODO: Currently only shows warning in frontend. In the future,
// workers below this version should be terminated automatically.
/// Minimum version workers must have to stay connected.
/// Served via: GET /api/min_keep_alive_version (returns { worker, agent })
/// Also used by vc() for compile-time checks.
pub const MIN_KEEP_ALIVE_VERSION: (u64, u64, u64) = (1, 420, 0);
/// Minimum version agent workers must have to stay connected.
/// Served via: GET /api/min_keep_alive_version (returns { worker, agent })
pub const AGENT_MIN_KEEP_ALIVE_VERSION: (u64, u64, u64) = (1, 0, 0);
// Compile-time check: MIN_KEEP_ALIVE_VERSION must lag at least 50 minor versions behind current,
// AGENT_MIN_KEEP_ALIVE_VERSION must lag at least 100 minor versions behind current.
// NOTE: These version lags are constants and should NEVER be changed. If this check
// fails, wait until enough versions have passed rather than reducing the lag requirement.
// Skip check if GIT_VERSION is "unknown-version" (no git tags available during build)
const _: () = assert!(
!const_str::contains!(crate::utils::GIT_VERSION, ".")
|| (const_str::parse!(const_str::split!(crate::utils::GIT_VERSION, ".")[1], u64)
- MIN_KEEP_ALIVE_VERSION.1
>= 50
&& const_str::parse!(const_str::split!(crate::utils::GIT_VERSION, ".")[1], u64)
- AGENT_MIN_KEEP_ALIVE_VERSION.1
>= 100)
);
// ============ Implementation ============
lazy_static::lazy_static! {
// Global minimum version across all workers (for feature flags)
pub static ref MIN_VERSION: arc_swap::ArcSwap<Version> = arc_swap::ArcSwap::from_pointee(Version::new(0, 0, 0));
}
/// Creates a VersionConstraint with compile-time assertion that version > MIN_KEEP_ALIVE_VERSION.
/// When MIN_KEEP_ALIVE_VERSION is raised, obsolete constraints will fail to compile.
pub const fn vc(major: u64, minor: u64, patch: u64, name: &'static str) -> VersionConstraint {
let is_greater = major > MIN_KEEP_ALIVE_VERSION.0
|| (major == MIN_KEEP_ALIVE_VERSION.0 && minor > MIN_KEEP_ALIVE_VERSION.1)
|| (major == MIN_KEEP_ALIVE_VERSION.0
&& minor == MIN_KEEP_ALIVE_VERSION.1
&& patch > MIN_KEEP_ALIVE_VERSION.2);
assert!(
is_greater,
"Feature version must be > MIN_KEEP_ALIVE_VERSION. Remove this obsolete constraint."
);
VersionConstraint { available_since: Version::new(major, minor, patch), name }
}
pub type VC = VersionConstraint;
#[derive(Clone)]
pub struct VersionConstraint {
available_since: Version,
name: &'static str,
}
impl VersionConstraint {
pub fn version(&self) -> &Version {
&self.available_since
}
pub async fn met(&self) -> bool {
let min = MIN_VERSION.load();
// If MIN_VERSION is 0.0.0, it hasn't been set yet - assume met
if **min == Version::new(0, 0, 0) {
tracing::warn!(
"MIN_VERSION not set yet, assuming feature '{}' is met",
self.name
);
return true;
}
&self.available_since <= &**min
}
pub async fn assert(&self) -> error::Result<()> {
if self.met().await {
Ok(())
} else {
Err(Error::WorkersAreBehind {
feature: self.name.to_string(),
min_version: self.available_since.to_string(),
})
}
}
}
// ============ Version Management ============
use crate::utils::{GIT_SEM_VERSION, GIT_VERSION};
use crate::worker::Connection;
/// Fetches the minimum version across all workers.
// TODO: consider using HTTP for everything instead of Connection enum
pub async fn get_min_version(conn: &Connection) -> error::Result<Version> {
let fetched = match conn {
Connection::Sql(pool) => {
let pings = sqlx::query_scalar!(
"SELECT wm_version FROM worker_ping WHERE wm_version != $1 AND ping_at > now() - interval '5 minutes'",
GIT_VERSION
).fetch_all(pool).await?;
pings
.iter()
.filter(|x| !x.is_empty())
.filter_map(|x| Version::parse(if x.starts_with('v') { &x[1..] } else { x }).ok())
.min()
}
Connection::Http(client) => Some(
client
.get::<String>("/api/agent_workers/get_min_version")
.await
.map(|v| Version::parse(&v))??,
),
};
Ok(fetched.unwrap_or_else(|| GIT_SEM_VERSION.clone()))
}
/// Updates MIN_VERSION and optionally checks min keep-alive version for workers.
/// If `_worker_mode` is true, checks min keep-alive version and sends critical alerts.
/// If `initial_load` is true, skips the min keep-alive check (server may not be ready).
pub async fn update_min_version(
conn: &Connection,
_worker_mode: bool,
_worker_names: Vec<String>,
_initial_load: bool,
) {
// Update MIN_VERSION
match get_min_version(conn).await {
Ok(ref mut v) => {
let cur = GIT_SEM_VERSION.clone();
if v != &cur {
tracing::info!("Minimal worker version: {v}");
}
v.pre = semver::Prerelease::EMPTY;
v.build = semver::BuildMetadata::EMPTY;
MIN_VERSION.store(std::sync::Arc::new(v.clone()));
}
Err(e) => tracing::error!("Failed to fetch min version: {:#?}", e),
}
// Workers check min keep-alive version and send critical alerts
// Skip on initial_load since the server may not be ready yet
#[cfg(all(feature = "enterprise", feature = "private"))]
if _worker_mode && !_initial_load {
let min_keep_alive_version: Option<Version> = match conn {
Connection::Sql(db) => {
match crate::global_settings::load_value_from_global_settings(
db,
crate::global_settings::MIN_KEEP_ALIVE_VERSION_SETTING,
)
.await
{
Ok(Some(v)) => v.as_str().and_then(|s| Version::parse(s).ok()),
Ok(None) => None,
Err(e) => {
tracing::error!(
"Failed to load min keep-alive version from global_settings: {:#?}",
e
);
None
}
}
}
Connection::Http(client) => {
match client
.get::<serde_json::Value>("/api/min_keep_alive_version")
.await
{
Ok(resp) => resp
.get("agent")
.and_then(|v| v.as_str())
.and_then(|v| Version::parse(v).ok()),
Err(e) => {
tracing::error!("Failed to fetch min keep-alive version: {:#?}", e);
None
}
}
}
};
if let Some(min_keep_alive) = min_keep_alive_version {
let current = GIT_SEM_VERSION.clone();
match conn {
Connection::Sql(db) => {
for worker_name in &_worker_names {
crate::ee::simple_alert_helper(
format!("Worker {worker_name} version {current} is below minimum keep-alive version {min_keep_alive}. Upgrade immediately."),
format!("Worker {worker_name} version {current} is now at or above minimum keep-alive version {min_keep_alive}."),
&format!("worker-below-min-keep-alive-{worker_name}"),
|| current < min_keep_alive,
Some("admins"),
db,
).await;
}
}
Connection::Http(_) => {
if current < min_keep_alive {
tracing::warn!(
"Agent worker version {current} is below minimum keep-alive version {min_keep_alive}. Upgrade immediately."
);
}
}
}
}
}
}
/// Stores the min keep-alive version in global_settings.
/// Called by server on startup, NOT by workers.
pub async fn store_min_keep_alive_version(db: &sqlx::Pool<sqlx::Postgres>) {
let version = format!(
"{}.{}.{}",
MIN_KEEP_ALIVE_VERSION.0, MIN_KEEP_ALIVE_VERSION.1, MIN_KEEP_ALIVE_VERSION.2
);
if let Err(e) = crate::global_settings::set_value_in_global_settings(
db,
crate::global_settings::MIN_KEEP_ALIVE_VERSION_SETTING,
serde_json::json!(version),
)
.await
{
tracing::error!(
"Failed to store min keep-alive version in global_settings: {:#?}",
e
);
}
}