From ee6231590ed91063f104e6d054b52e88b569986f Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 30 Mar 2026 15:16:38 +0000 Subject: [PATCH] fix: enable S3 bundle cache for PHP previews without lock file (#8608) * perf: enable S3 bundle cache for PHP previews without lock file Co-Authored-By: Claude Opus 4.5 * fix: resolve borrow-after-move of lock in php cache save Co-Authored-By: Claude Opus 4.5 * refactor: use DB-based lockfile cache for PHP previews instead of requirements-only key Co-Authored-By: Claude Opus 4.5 * fix: prevent stale lockfile TTL refresh in PHP preview cache Co-Authored-By: Claude Opus 4.5 * chore: add sqlx offline cache entry for PHP lockfile resolution query Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- ...9b20cd61f9967a2012fc1c8fe352ee7596358.json | 15 ++++++ backend/windmill-worker/src/php_executor.rs | 47 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 backend/.sqlx/query-a860dd9722f608184c4b1ef5e609b20cd61f9967a2012fc1c8fe352ee7596358.json diff --git a/backend/.sqlx/query-a860dd9722f608184c4b1ef5e609b20cd61f9967a2012fc1c8fe352ee7596358.json b/backend/.sqlx/query-a860dd9722f608184c4b1ef5e609b20cd61f9967a2012fc1c8fe352ee7596358.json new file mode 100644 index 0000000000..539c029265 --- /dev/null +++ b/backend/.sqlx/query-a860dd9722f608184c4b1ef5e609b20cd61f9967a2012fc1c8fe352ee7596358.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('7 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = EXCLUDED.lockfile, expiration = EXCLUDED.expiration", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "a860dd9722f608184c4b1ef5e609b20cd61f9967a2012fc1c8fe352ee7596358" +} diff --git a/backend/windmill-worker/src/php_executor.rs b/backend/windmill-worker/src/php_executor.rs index 89d3bd5b62..d3d0fb36fb 100644 --- a/backend/windmill-worker/src/php_executor.rs +++ b/backend/windmill-worker/src/php_executor.rs @@ -84,8 +84,27 @@ pub async fn composer_install( ) -> Result { check_executor_binary_exists("php", PHP_PATH.as_str(), "php")?; - // When a lock file is available the dependency set is fully pinned, so we - // can cache the installed vendor/ directory and reuse it across executions. + // When no lock is provided (previews), try to reuse a previously resolved + // lockfile from the DB so we can hit the same vendor cache as deployed scripts. + let lock = if lock.is_none() && !*COMPOSER_VENDOR_CACHE_DISABLED { + let req_hash = format!("composer-{}", calculate_hash(&requirements)); + if let Some(db) = conn.as_sql() { + sqlx::query_scalar!( + "SELECT lockfile FROM pip_resolution_cache WHERE hash = $1", + req_hash + ) + .fetch_optional(db) + .await + .ok() + .flatten() + } else { + None + } + } else { + lock + }; + + // Cache the installed vendor/ directory keyed by requirements + lock content. // Set COMPOSER_VENDOR_CACHE_DISABLED=1 to opt out. let vendor_cache_hit = if !*COMPOSER_VENDOR_CACHE_DISABLED { if let Some(ref lock_content) = lock { @@ -171,6 +190,9 @@ pub async fn composer_install( ) .await?; + // lock was `None` means composer resolved deps from scratch (no lock from + // caller or DB). This is the only case where we should update the DB cache. + let freshly_resolved = lock.is_none(); let resolved_lock = match lock { Some(l) => l, None => { @@ -195,6 +217,27 @@ pub async fn composer_install( { tracing::warn!("Could not save composer vendor dir to cache: {e:?}"); } + + // Cache the resolved lockfile in the DB so future previews (which lack a + // lock file) can look it up by requirements hash and hit the same vendor + // cache. TTL of 7 days keeps previews reasonably fresh. + // Only write when composer resolved from scratch (no lock from caller or + // DB) to avoid endlessly refreshing the TTL on stale resolutions. + if freshly_resolved { + let req_hash = format!("composer-{}", calculate_hash(&requirements)); + if let Some(db) = conn.as_sql() { + if let Err(e) = sqlx::query!( + "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('7 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = EXCLUDED.lockfile, expiration = EXCLUDED.expiration", + req_hash, + &resolved_lock + ) + .execute(db) + .await + { + tracing::warn!("Could not cache composer lockfile resolution: {e:?}"); + } + } + } } Ok(format!(