mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
fix(forks): clone only the current raw-app bundle, via server-side copy (#9899)
* fix(forks): clone only the current raw-app bundle, via server-side copy Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(forks): fall back to get+put when object-store copy is unsupported Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bcb4dd8d27
commit
5c521d808a
@@ -4734,9 +4734,18 @@ async fn clone_apps(
|
||||
.await?;
|
||||
|
||||
let mut app_id_mapping: HashMap<i64, i64> = HashMap::new();
|
||||
// Only a raw app's current (last) version has a bundle worth carrying into the fork: bundles exist
|
||||
// only for raw apps, and older versions aren't viewable/runnable (the bundle secret is only ever
|
||||
// minted for `versions.last()`). Copying a bundle for every version of every app is what makes
|
||||
// forking a workspace with many app versions hang — a serial S3 round-trip per version, held inside
|
||||
// the fork transaction. Collect each app's current version here; intersect with raw versions below.
|
||||
let mut latest_version_ids: HashSet<i64> = HashSet::new();
|
||||
|
||||
// Clone apps with new IDs
|
||||
for app in apps {
|
||||
if let Some(¤t_version) = app.versions.last() {
|
||||
latest_version_ids.insert(current_version);
|
||||
}
|
||||
let new_app_id = sqlx::query_scalar!(
|
||||
"INSERT INTO app (workspace_id, path, summary, policy, versions, extra_perms, custom_path)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
@@ -4756,6 +4765,7 @@ async fn clone_apps(
|
||||
}
|
||||
|
||||
let mut version_id_mapping: HashMap<i64, i64> = HashMap::new();
|
||||
let mut raw_version_ids: HashSet<i64> = HashSet::new();
|
||||
|
||||
{
|
||||
// Clone app versions
|
||||
@@ -4771,6 +4781,9 @@ async fn clone_apps(
|
||||
|
||||
for version in app_versions {
|
||||
if let Some(&new_app_id) = app_id_mapping.get(&version.app_id) {
|
||||
if version.raw_app {
|
||||
raw_version_ids.insert(version.id);
|
||||
}
|
||||
let new_version_id = sqlx::query_scalar!(
|
||||
"INSERT INTO app_version (app_id, value, created_by, created_at, raw_app)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||
@@ -4788,9 +4801,16 @@ async fn clone_apps(
|
||||
}
|
||||
}
|
||||
|
||||
// Clone app bundles for raw apps
|
||||
if !version_id_mapping.is_empty() {
|
||||
let old_ids: Vec<i64> = version_id_mapping.keys().copied().collect();
|
||||
// The bundles worth cloning: each raw app's current version (latest ∩ raw). Everything else either
|
||||
// has no bundle (low-code apps) or an unreachable one (older versions), so we don't touch S3 for it.
|
||||
let bundle_version_ids: HashSet<i64> = latest_version_ids
|
||||
.intersection(&raw_version_ids)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
// Clone app bundles — only the current version of each raw app (see bundle_version_ids).
|
||||
if !bundle_version_ids.is_empty() {
|
||||
let old_ids: Vec<i64> = bundle_version_ids.iter().copied().collect();
|
||||
let bundles = sqlx::query!(
|
||||
"SELECT app_version_id, file_type, data FROM app_bundles
|
||||
WHERE app_version_id = ANY($1) AND w_id = $2",
|
||||
@@ -4826,40 +4846,31 @@ async fn clone_apps(
|
||||
let object_store = windmill_object_store::get_object_store().await;
|
||||
if let Some(os) = object_store {
|
||||
for (&old_version_id, &new_version_id) in &version_id_mapping {
|
||||
if !bundle_version_ids.contains(&old_version_id) {
|
||||
continue;
|
||||
}
|
||||
for file_type in &["js", "css"] {
|
||||
if cloned_from_db.contains(&(old_version_id, file_type.to_string())) {
|
||||
continue;
|
||||
}
|
||||
let src_path = format!(
|
||||
"/app_bundles/{}/{}.{}",
|
||||
source_workspace_id, old_version_id, file_type
|
||||
);
|
||||
let get_result = os
|
||||
.get(&windmill_object_store::object_store_reexports::Path::from(
|
||||
src_path,
|
||||
))
|
||||
.await;
|
||||
match get_result {
|
||||
Ok(result) => {
|
||||
let data = result.bytes().await.map_err(
|
||||
windmill_object_store::object_store_error_to_error,
|
||||
)?;
|
||||
let dst_path = format!(
|
||||
"/app_bundles/{}/{}.{}",
|
||||
target_workspace_id, new_version_id, file_type
|
||||
);
|
||||
os.put(
|
||||
&windmill_object_store::object_store_reexports::Path::from(
|
||||
dst_path.clone(),
|
||||
),
|
||||
data.into(),
|
||||
)
|
||||
.await
|
||||
.map_err(
|
||||
windmill_object_store::object_store_error_to_error,
|
||||
)?;
|
||||
// Prefer a server-side copy (no bytes through the backend). A missing source —
|
||||
// e.g. a raw app with a js bundle but no css — surfaces as NotFound and is
|
||||
// skipped. Not every object-store provider supports server-side copy, so fall
|
||||
// back to download+upload on any other error.
|
||||
let src_path =
|
||||
windmill_object_store::object_store_reexports::Path::from(format!(
|
||||
"/app_bundles/{}/{}.{}",
|
||||
source_workspace_id, old_version_id, file_type
|
||||
));
|
||||
let dst_path =
|
||||
windmill_object_store::object_store_reexports::Path::from(format!(
|
||||
"/app_bundles/{}/{}.{}",
|
||||
target_workspace_id, new_version_id, file_type
|
||||
));
|
||||
match os.copy(&src_path, &dst_path).await {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
"Cloned app bundle from S3: {}.{} -> {}.{}",
|
||||
"Cloned app bundle in object store: {}.{} -> {}.{}",
|
||||
old_version_id,
|
||||
file_type,
|
||||
new_version_id,
|
||||
@@ -4867,12 +4878,39 @@ async fn clone_apps(
|
||||
);
|
||||
}
|
||||
Err(windmill_object_store::object_store_reexports::ObjectStoreError::NotFound { .. }) => {
|
||||
// No bundle in S3 for this version/type, skip
|
||||
// No bundle in the object store for this version/type, skip
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(
|
||||
windmill_object_store::object_store_error_to_error(e),
|
||||
Err(copy_err) => {
|
||||
// Provider may not support server-side copy — fall back to get+put.
|
||||
tracing::warn!(
|
||||
"object store copy failed ({copy_err:#}), falling back to get+put for app bundle {}.{}",
|
||||
old_version_id, file_type
|
||||
);
|
||||
match os.get(&src_path).await {
|
||||
Ok(result) => {
|
||||
let data = result.bytes().await.map_err(
|
||||
windmill_object_store::object_store_error_to_error,
|
||||
)?;
|
||||
os.put(&dst_path, data.into()).await.map_err(
|
||||
windmill_object_store::object_store_error_to_error,
|
||||
)?;
|
||||
tracing::info!(
|
||||
"Cloned app bundle via get+put fallback: {}.{} -> {}.{}",
|
||||
old_version_id,
|
||||
file_type,
|
||||
new_version_id,
|
||||
file_type
|
||||
);
|
||||
}
|
||||
Err(windmill_object_store::object_store_reexports::ObjectStoreError::NotFound { .. }) => {
|
||||
// No bundle for this version/type, skip
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(
|
||||
windmill_object_store::object_store_error_to_error(e),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user