feat: record each instance's python version and start new ones on 3.13 (#11656)

* feat: pin the default python version of an instance at initialization

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: detect a new instance from the schema it created, not from its age

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: record the python baseline on the migration connection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: record the instance python version in the setting itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: start new instances on python 3.15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the forced python version ahead of the stored one when a worker reads it late

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: start new instances on python 3.14 rather than 3.15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: start new instances on python 3.13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 06610ecc950ed64a2709d7f1aa8c7b49a95fdbae

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

Previous ee-repo-ref: 599eae0034b34c5e0d3d42df37cf18bbe01f9b49

New ee-repo-ref: 06610ecc950ed64a2709d7f1aa8c7b49a95fdbae

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2026-10-13 15:22:07 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 windmill-internal-app[bot]
parent 41b9779ae3
commit 7558ce27a9
8 changed files with 99 additions and 18 deletions
+6 -4
View File
@@ -152,11 +152,12 @@ ARG WITH_HELM=true
ARG WITH_GIT=true
ARG features=""
# To change latest stable version:
# 1. Change placeholder in instanceSettings.ts
# 2. Change LATEST_STABLE_PY in dockerfile
# 3. Change #[default] annotation for PyVersion in backend
# Instances keep the Python version they were initialized with, so both are preinstalled:
# LATEST_STABLE_PY is what instances initialized before the version was raised are on
# (PyVAlias::default() in the backend), NEW_INSTANCE_PY the one new instances start on
# (PyVAlias::NEW_INSTANCE). When raising the latter, keep installing every earlier value.
ARG LATEST_STABLE_PY=3.12
ARG NEW_INSTANCE_PY=3.13
ENV UV_PYTHON_INSTALL_DIR=/tmp/windmill/cache/py_runtime
ENV UV_PYTHON_PREFERENCE=only-managed
@@ -249,6 +250,7 @@ RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releas
# timestamps or Python's mtime-based .pyc invalidation discards these compiled files.
RUN UV_CACHE_DIR=/tmp/build_cache/uv UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install 3.11 --compile-bytecode
RUN UV_CACHE_DIR=/tmp/build_cache/uv UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $LATEST_STABLE_PY --compile-bytecode
RUN UV_CACHE_DIR=/tmp/build_cache/uv UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $NEW_INSTANCE_PY --compile-bytecode
RUN curl -sL https://deb.nodesource.com/setup_24.x | bash -
+51
View File
@@ -17,7 +17,9 @@ use tokio::task::JoinHandle;
pub use windmill_common::db::DB;
use windmill_common::{
error::Error,
global_settings::INSTANCE_PYTHON_VERSION_SETTING,
utils::{generate_lock_id, GIT_VERSION},
worker::PyVAlias,
};
#[allow(unused_imports)]
@@ -298,6 +300,12 @@ pub async fn migrate(
let migrator = db.acquire().await?;
let mut custom_migrator = CustomMigrator { inner: migrator };
let new_instance =
sqlx::query_scalar::<_, bool>("SELECT to_regclass('_sqlx_migrations') IS NULL")
.fetch_one(custom_migrator.connection())
.await
.unwrap_or(false);
if let Err(err) = sqlx::query!(
"DELETE FROM _sqlx_migrations WHERE
version=20250131115248 OR version=20250902085503 OR version=20250201145630 OR
@@ -372,12 +380,55 @@ pub async fn migrate(
}
crate::live_migrations::custom_migrations(&mut custom_migrator).await?;
if let Err(e) = seed_instance_python_version(custom_migrator.connection(), new_instance).await {
tracing::error!("Could not record the instance python version: {e:#}");
}
Ok(Some(crate::live_migrations::spawn_background_migrations(
db.clone(),
killpill_rx,
)))
}
/// Gives `instance_python_version` a concrete value when the instance has none, so the version
/// scripts resolve around is recorded per instance instead of following whatever a release
/// compiles in. Only the process that created the schema knows the instance is new, so its value
/// replaces what a server that started alongside it wrote; every other start fills in the frozen
/// default, and only where no version was chosen (`default` is what the settings page stored
/// for that).
///
/// Runs on the migration connection: a `DATABASE_CONNECTIONS=1` pool has no second one to give.
async fn seed_instance_python_version(
conn: &mut PgConnection,
new_instance: bool,
) -> Result<(), Error> {
let (version, on_conflict) = if new_instance {
(
PyVAlias::NEW_INSTANCE,
"DO UPDATE SET value = EXCLUDED.value",
)
} else {
(
PyVAlias::default(),
"DO UPDATE SET value = EXCLUDED.value WHERE global_settings.value = '\"default\"'::jsonb",
)
};
// The stored setting outranks the `INSTANCE_PYTHON_VERSION` environment variable, so an
// instance configured through it would otherwise be moved off its version by this write.
let version = std::env::var("INSTANCE_PYTHON_VERSION")
.ok()
.filter(|v| !v.is_empty() && v.split('.').all(|part| part.parse::<u32>().is_ok()))
.unwrap_or_else(|| version.version_string());
sqlx::query(&format!(
"INSERT INTO global_settings (name, value) VALUES ($1, to_jsonb($2::text))
ON CONFLICT (name) {on_conflict}"
))
.bind(INSTANCE_PYTHON_VERSION_SETTING)
.bind(version)
.execute(conn)
.await?;
Ok(())
}
pub async fn wait_for_migrations(
db: &DB,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
@@ -1063,6 +1063,7 @@ pub const PROTECTED_SETTINGS: &[&str] = &[
"rsa_keys",
"jwt_secret",
"min_keep_alive_version",
"instance_python_version",
// Deleting it would let a worker sign OIDC tokens with a local key again.
"oidc_external_signer",
];
+13
View File
@@ -2891,6 +2891,9 @@ pub fn lockfile_line_has_continuation(line: &str) -> bool {
pub enum PyVAlias {
Py310 = 10,
Py311,
// Frozen: what a lock with no `# py:` line runs on, and what the server writes into
// `instance_python_version` for an instance that never chose one. Raise `NEW_INSTANCE`
// instead, which existing instances never pick up.
#[default]
Py312,
Py313,
@@ -2911,6 +2914,11 @@ impl Into<u32> for PyVAlias {
}
impl PyVAlias {
/// What the server writes into `instance_python_version` when it initializes an instance.
/// Raising it also means the images must keep preinstalling every earlier value, or
/// instances that started on one download their runtime on first use.
pub const NEW_INSTANCE: PyVAlias = PyVAlias::Py313;
pub fn all<T: From<PyVAlias>>() -> Vec<T> {
use PyVAlias::*;
vec![
@@ -2922,6 +2930,11 @@ impl PyVAlias {
Py315.into(),
]
}
/// `MAJOR.MINOR`, the form the instance settings store.
pub fn version_string(self) -> String {
format!("{}.{}", self.major(), self as u32)
}
// Get MAJOR part of alias. (semver: MAJOR.MINOR.PATCH)
fn major(&self) -> u32 {
use PyVAlias::*;
+24 -5
View File
@@ -14,6 +14,7 @@ use tokio::{fs::DirBuilder, process::Command, sync::RwLock};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
global_settings::{load_value_from_global_settings, INSTANCE_PYTHON_VERSION_SETTING},
worker::{try_parse_locked_python_version_from_requirements, Connection, PyVAlias},
};
@@ -49,10 +50,6 @@ impl From<PyV> for PyVAlias {
}
}
// To change latest stable version:
// 1. Change placeholder in instanceSettings.ts
// 2. Change LATEST_STABLE_PY in dockerfile
// 3. Change #[default] annotation for PyVersion in backend
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PyV(pub pep440_rs::Version);
@@ -260,7 +257,29 @@ impl PyV {
conn: Option<Connection>,
) -> Self {
let mut err = None;
let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() {
let mut setting = INSTANCE_PYTHON_VERSION.read().await.clone();
// A worker that loaded its settings while the server was still initializing the instance
// read no version, and may have started listening for setting changes only after it was
// written. Jobs only exist once it is, so reading it here settles that worker for good.
if setting.is_none() {
if let Some(Connection::Sql(db)) = conn.as_ref() {
// Same precedence as the settings load: forced value, stored value, environment.
let env = |name: &str| std::env::var(name).ok().filter(|v| !v.is_empty());
setting = match env("FORCE_INSTANCE_PYTHON_VERSION") {
Some(forced) => Some(forced),
None => load_value_from_global_settings(db, INSTANCE_PYTHON_VERSION_SETTING)
.await
.ok()
.flatten()
.and_then(|v| serde_json::from_value::<String>(v).ok())
.or_else(|| env("INSTANCE_PYTHON_VERSION")),
};
if setting.is_some() {
*INSTANCE_PYTHON_VERSION.write().await = setting.clone();
}
}
}
let pyv = match setting {
Some(v) if &v == "default" => PyVAlias::default().into(),
Some(v) => pep440_rs::Version::from_str(&v).unwrap_or_else(|_| {
let v = PyVAlias::default().into();
+2
View File
@@ -30,6 +30,7 @@ FROM ${DEBIAN_IMAGE}
ARG APP=/usr/src/app
ARG LATEST_STABLE_PY=3.12.13
ARG NEW_INSTANCE_PY=3.13.16
# UV configuration
ENV UV_CACHE_DIR=/tmp/windmill/cache/uv
@@ -65,6 +66,7 @@ RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releas
# under the read-only nsjail runtime mount (uv >= 0.9.25). The copy below MUST preserve
# timestamps or Python's mtime-based .pyc invalidation discards these compiled files.
RUN UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $LATEST_STABLE_PY --compile-bytecode
RUN UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $NEW_INSTANCE_PY --compile-bytecode
# Copy to final location with world-writable permissions for arbitrary UID support
RUN mkdir -p /tmp/windmill/cache && \
+2
View File
@@ -30,6 +30,7 @@ FROM ${DEBIAN_IMAGE}
ARG APP=/usr/src/app
ARG LATEST_STABLE_PY=3.12.13
ARG NEW_INSTANCE_PY=3.13.16
# UV configuration
ENV UV_CACHE_DIR=/tmp/windmill/cache/uv
@@ -65,6 +66,7 @@ RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releas
# under the read-only nsjail runtime mount (uv >= 0.9.25). The copy below MUST preserve
# timestamps or Python's mtime-based .pyc invalidation discards these compiled files.
RUN UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $LATEST_STABLE_PY --compile-bytecode
RUN UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $NEW_INSTANCE_PY --compile-bytecode
# Copy to final location with world-writable permissions for arbitrary UID support
RUN mkdir -p /tmp/windmill/cache && \
@@ -838,17 +838,8 @@ export const settings: Record<string, Setting[]> = {
description: 'Default python version for newly deployed scripts',
key: 'instance_python_version',
fieldType: 'select_python',
// To change latest stable version:
// 1. Change placeholder in instanceSettings.ts
// 2. Change LATEST_STABLE_PY in dockerfile
// 3. Change #[default] annotation for PyVersion in backend
placeholder: '3.10,3.11,3.12,3.13,3.14,3.15',
select_items: [
{
label: 'Latest Stable',
value: 'default',
tooltip: 'python-3.12'
},
{
label: '3.10'
},