From e550374f06c0694f8dfdd03effededf5b0aba8fe Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Fri, 18 Sep 2026 00:58:19 +0200 Subject: [PATCH 1/2] fix(datatables): migrate fork reservations on workspace rename, and lock the parent's data tables for the whole fork Co-Authored-By: Claude Opus 5 (1M context) --- backend/windmill-api-workspaces/src/workspaces.rs | 4 +++- .../src/workspaces_extra.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 2d840d9e51..411bc8538c 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -8039,7 +8039,6 @@ async fn point_kept_datatables_at_parent( forked_w_id: &str, cloned: &[ForkedDatatableInfo], ) -> Result<()> { - windmill_common::workspaces::lock_fork_datatables(tx, parent_w_id).await?; let settings: Option = sqlx::query_scalar!( "SELECT datatable FROM workspace_settings WHERE workspace_id = $1", forked_w_id @@ -8607,6 +8606,9 @@ async fn create_workspace_fork( } let mut tx: Transaction<'_, Postgres> = db.begin().await?; + // Before the settings clone reads the parent's data tables: a pointer this fork ends up with + // must not be written after cleanup of the parent decided that nothing points at its copies. + windmill_common::workspaces::lock_fork_datatables(&mut tx, &parent_workspace_id).await?; if nw.is_dev_workspace { // The checks above ran outside a transaction, so the parent's eligibility and the chain's diff --git a/backend/windmill-api-workspaces/src/workspaces_extra.rs b/backend/windmill-api-workspaces/src/workspaces_extra.rs index 480f815ad8..0d64eb4ac1 100644 --- a/backend/windmill-api-workspaces/src/workspaces_extra.rs +++ b/backend/windmill-api-workspaces/src/workspaces_extra.rs @@ -110,6 +110,21 @@ pub(crate) async fn change_workspace_id( .execute(&mut *tx) .await?; + // A fork copy reserved for the old id would otherwise be unreachable: its creator cannot + // import into it or finish its fork under the new id, and nothing else would ever drop it. + sqlx::query( + r#"UPDATE global_settings SET value = jsonb_set(value, '{databases}', ( + SELECT COALESCE(jsonb_object_agg(k, CASE WHEN v->>'workspace_id' = $1 + THEN jsonb_set(v, '{workspace_id}', to_jsonb($2::text)) ELSE v END), '{}'::jsonb) + FROM jsonb_each(COALESCE(value->'databases', '{}'::jsonb)) AS e(k, v) + )) + WHERE name = 'custom_instance_pg_databases'"#, + ) + .bind(&old_id) + .bind(&rw.new_id) + .execute(&mut *tx) + .await?; + // Duplicate workspace settings (keep copy in old workspace for reference) info!("Duplicating workspace_settings table"); sqlx::query!( From 8c554ef1337aa6cd99ab869443dfd30becc65a36 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Fri, 18 Sep 2026 01:00:06 +0200 Subject: [PATCH 2/2] fix(datatables): take the fork data table lock once, before the external cluster's Co-Authored-By: Claude Opus 5 (1M context) --- backend/windmill-api-workspaces/src/workspaces.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index fcc0d685ca..4c3d695c8f 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -8755,6 +8755,8 @@ async fn create_workspace_fork( let mut tx: Transaction<'_, Postgres> = db.begin().await?; // Before the settings clone reads the parent's data tables: a pointer this fork ends up with // must not be written after cleanup of the parent decided that nothing points at its copies. + // Also before the external cluster's lifecycle lock, which finalizing an external copy takes: + // fork cleanup takes the two in this order. windmill_common::workspaces::lock_fork_datatables(&mut tx, &parent_workspace_id).await?; if nw.is_dev_workspace { @@ -8868,9 +8870,6 @@ async fn create_workspace_fork( // re-enables in the fork, with parent-conflict warnings on enable. clone_triggers_and_schedules(&mut tx, &parent_workspace_id, &forked_id).await?; - // Before the external cluster's lifecycle lock, which finalizing an external copy takes: fork - // cleanup takes the two in this order. - windmill_common::workspaces::lock_fork_datatables(&mut tx, &parent_workspace_id).await?; // Update forked datatable settings to point to new databases for fdt in &nw.forked_datatables { apply_forked_datatable(&db, &mut tx, &authed, &parent_workspace_id, &forked_id, fdt)