Restore dependency and wrapper caches concurrently (#1174)

* Restore dependency and wrapper caches concurrently

Run primary dependency and wrapper cache restores in parallel while preserving existing outputs and save semantics.

Add unit and E2E coverage for concurrent restore behavior, wrapper cache validation, and additional-cache error handling.

Include a manual benchmark workflow for baseline-vs-candidate restore timing comparisons across OSes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d43ec3dd-96c7-4eb3-909e-b8123cd12d3c

* Address PR review comments

Use env variables for cache-hit values in benchmark record steps to avoid expression expansion in run commands, and rename E2E restore step labels for clarity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d43ec3dd-96c7-4eb3-909e-b8123cd12d3c

* Stabilize wrapper cache restore checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d43ec3dd-96c7-4eb3-909e-b8123cd12d3c
This commit is contained in:
Bruno Borges
2026-07-29 13:41:24 -04:00
committed by GitHub
parent ec4dbbe20d
commit 9f43141311
9 changed files with 683 additions and 59 deletions
+43 -18
View File
@@ -97899,10 +97899,23 @@ async function computeAdditionalCacheKey(additionalCache) {
*/
async function restore(id, cacheDependencyPath) {
const packageManager = findPackageManager(id);
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
const [primaryKey, preparedAdditionalCaches] = await Promise.all([
computeCacheKey(packageManager, cacheDependencyPath),
prepareAdditionalCaches(packageManager.additionalCaches ?? [])
]);
core.debug(`primary key is ${primaryKey}`);
core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
core.setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
for (const preparedCache of preparedAdditionalCaches) {
core.debug(`${preparedCache.cache.name} primary key is ${preparedCache.primaryKey}`);
core.saveState(additionalCachePrimaryKeyState(preparedCache.cache.name), preparedCache.primaryKey);
}
await Promise.all([
restorePrimaryCache(packageManager, primaryKey),
...preparedAdditionalCaches.map(preparedCache => restoreAdditionalCache(preparedCache))
]);
}
async function restorePrimaryCache(packageManager, primaryKey) {
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269)
const matchedKey = await cache.restoreCache(packageManager.path, primaryKey);
if (matchedKey) {
@@ -97914,29 +97927,35 @@ async function restore(id, cacheDependencyPath) {
core.setOutput('cache-hit', false);
core.info(`${packageManager.id} cache is not found`);
}
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
* Compute keys for additional caches (e.g. build-tool wrapper distributions).
* Additional caches without a matching configuration file are omitted.
*/
async function restoreAdditionalCache(additionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return;
}
core.debug(`${additionalCache.name} primary key is ${primaryKey}`);
core.saveState(additionalCachePrimaryKeyState(additionalCache.name), primaryKey);
async function prepareAdditionalCaches(additionalCaches) {
const preparedCaches = await Promise.all(additionalCaches.map(async (additionalCache) => {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return undefined;
}
return { cache: additionalCache, primaryKey };
}));
return preparedCaches.filter((preparedCache) => preparedCache !== undefined);
}
/**
* Restore an additional cache keyed independently of the main dependency cache.
*/
async function restoreAdditionalCache(preparedCache) {
const { cache: additionalCache, primaryKey } = preparedCache;
const matchedKey = await cache.restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
core.saveState(additionalCacheMatchedKeyState(additionalCache.name), matchedKey);
core.info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
else {
core.info(`${additionalCache.name} cache is not found`);
}
}
/**
* Save the dependency cache
@@ -97948,7 +97967,13 @@ async function save(id) {
// Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
try {
await saveAdditionalCache(packageManager, additionalCache);
}
catch (error) {
const err = error;
warning(`Failed to save ${additionalCache.name} cache: ${err.message}. Continuing with primary cache save.`);
}
}
if (!primaryKey) {
warning('Error retrieving key from state.');
@@ -98030,7 +98055,7 @@ async function saveAdditionalCache(packageManager, additionalCache) {
}
else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
warning('Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.');
warning(`Failed to save ${additionalCache.name} cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with \`--no-daemon\` option. Refer to https://github.com/actions/cache/issues/454 for details.`);
}
throw error;
}
+43 -18
View File
@@ -129154,10 +129154,23 @@ async function computeAdditionalCacheKey(additionalCache) {
*/
async function restore(id, cacheDependencyPath) {
const packageManager = findPackageManager(id);
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
const [primaryKey, preparedAdditionalCaches] = await Promise.all([
computeCacheKey(packageManager, cacheDependencyPath),
prepareAdditionalCaches(packageManager.additionalCaches ?? [])
]);
core_debug(`primary key is ${primaryKey}`);
saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
for (const preparedCache of preparedAdditionalCaches) {
core_debug(`${preparedCache.cache.name} primary key is ${preparedCache.primaryKey}`);
saveState(additionalCachePrimaryKeyState(preparedCache.cache.name), preparedCache.primaryKey);
}
await Promise.all([
restorePrimaryCache(packageManager, primaryKey),
...preparedAdditionalCaches.map(preparedCache => restoreAdditionalCache(preparedCache))
]);
}
async function restorePrimaryCache(packageManager, primaryKey) {
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269)
const matchedKey = await restoreCache(packageManager.path, primaryKey);
if (matchedKey) {
@@ -129169,29 +129182,35 @@ async function restore(id, cacheDependencyPath) {
setOutput('cache-hit', false);
info(`${packageManager.id} cache is not found`);
}
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
* Compute keys for additional caches (e.g. build-tool wrapper distributions).
* Additional caches without a matching configuration file are omitted.
*/
async function restoreAdditionalCache(additionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core_debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return;
}
core_debug(`${additionalCache.name} primary key is ${primaryKey}`);
saveState(additionalCachePrimaryKeyState(additionalCache.name), primaryKey);
async function prepareAdditionalCaches(additionalCaches) {
const preparedCaches = await Promise.all(additionalCaches.map(async (additionalCache) => {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core_debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return undefined;
}
return { cache: additionalCache, primaryKey };
}));
return preparedCaches.filter((preparedCache) => preparedCache !== undefined);
}
/**
* Restore an additional cache keyed independently of the main dependency cache.
*/
async function restoreAdditionalCache(preparedCache) {
const { cache: additionalCache, primaryKey } = preparedCache;
const matchedKey = await restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
saveState(additionalCacheMatchedKeyState(additionalCache.name), matchedKey);
info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
else {
info(`${additionalCache.name} cache is not found`);
}
}
/**
* Save the dependency cache
@@ -129203,7 +129222,13 @@ async function save(id) {
// Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
try {
await saveAdditionalCache(packageManager, additionalCache);
}
catch (error) {
const err = error;
core.warning(`Failed to save ${additionalCache.name} cache: ${err.message}. Continuing with primary cache save.`);
}
}
if (!primaryKey) {
core.warning('Error retrieving key from state.');
@@ -129285,7 +129310,7 @@ async function saveAdditionalCache(packageManager, additionalCache) {
}
else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
core.warning('Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.');
core.warning(`Failed to save ${additionalCache.name} cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with \`--no-daemon\` option. Refer to https://github.com/actions/cache/issues/454 for details.`);
}
throw error;
}