Ensure save/restore only on first action step

Instead of relying on the separate cache implementations to check for the
existence of cached products, we now explicitly track whether or not the execution
is the first time the action has been invoked for a job.
This commit is contained in:
Daz DeBoer 2021-12-07 12:56:36 -07:00
parent 253d6427fd
commit 1041604f29
No known key found for this signature in database
GPG key ID: DD6B9F0B06683D5D
4 changed files with 45 additions and 38 deletions

View file

@ -83,21 +83,16 @@ export abstract class AbstractCache {
/** /**
* Restores the cache entry, finding the closest match to the currently running job. * Restores the cache entry, finding the closest match to the currently running job.
* If the target output already exists, caching will be skipped.
*/ */
async restore(listener: CacheListener): Promise<void> { async restore(listener: CacheListener): Promise<void> {
if (this.cacheOutputExists()) {
core.info(`${this.cacheDescription} already exists. Not restoring from cache.`)
return
}
const entryListener = listener.entry(this.cacheDescription) const entryListener = listener.entry(this.cacheDescription)
const cacheKey = this.prepareCacheKey() const cacheKey = this.prepareCacheKey()
this.debug( this.debug(
`Requesting ${this.cacheDescription} with `Requesting ${this.cacheDescription} with
key:${cacheKey.key} key:${cacheKey.key}
restoreKeys:[${cacheKey.restoreKeys}]` restoreKeys:[${cacheKey.restoreKeys}]`
) )
const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys) const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
@ -142,28 +137,17 @@ export abstract class AbstractCache {
protected async afterRestore(_listener: CacheListener): Promise<void> {} protected async afterRestore(_listener: CacheListener): Promise<void> {}
/** /**
* Saves the cache entry based on the current cache key, unless: * Saves the cache entry based on the current cache key unless the cache was restored with the exact key,
* - If the cache output existed before restore, then it is not saved. * in which case we cannot overwrite it.
* - If the cache was restored with the exact key, we cannot overwrite it.
* *
* If the cache entry was restored with a partial match on a restore key, then * If the cache entry was restored with a partial match on a restore key, then
* it is saved with the exact key. * it is saved with the exact key.
*/ */
async save(listener: CacheListener): Promise<void> { async save(listener: CacheListener): Promise<void> {
if (!this.cacheOutputExists()) {
core.info(`No ${this.cacheDescription} to cache.`)
return
}
// Retrieve the state set in the previous 'restore' step. // Retrieve the state set in the previous 'restore' step.
const cacheKeyFromRestore = core.getState(this.cacheKeyStateKey) const cacheKeyFromRestore = core.getState(this.cacheKeyStateKey)
const cacheResultFromRestore = core.getState(this.cacheResultStateKey) const cacheResultFromRestore = core.getState(this.cacheResultStateKey)
if (!cacheKeyFromRestore) {
core.info(`${this.cacheDescription} existed prior to cache restore. Not saving.`)
return
}
if (cacheResultFromRestore && cacheKeyFromRestore === cacheResultFromRestore) { if (cacheResultFromRestore && cacheKeyFromRestore === cacheResultFromRestore) {
core.info(`Cache hit occurred on the cache key ${cacheKeyFromRestore}, not saving cache.`) core.info(`Cache hit occurred on the cache key ${cacheKeyFromRestore}, not saving cache.`)
return return
@ -206,6 +190,5 @@ export abstract class AbstractCache {
} }
} }
protected abstract cacheOutputExists(): boolean
protected abstract getCachePath(): string[] protected abstract getCachePath(): string[]
} }

View file

@ -294,12 +294,6 @@ export class GradleUserHomeCache extends AbstractCache {
return path.resolve(os.homedir(), '.gradle') return path.resolve(os.homedir(), '.gradle')
} }
protected cacheOutputExists(): boolean {
// Need to check for 'caches' directory to avoid incorrect detection on MacOS agents
const dir = path.resolve(this.gradleUserHome, 'caches')
return fs.existsSync(dir)
}
/** /**
* Determines the paths within Gradle User Home to cache. * Determines the paths within Gradle User Home to cache.
* By default, this is the 'caches' and 'notifications' directories, * By default, this is the 'caches' and 'notifications' directories,

View file

@ -1,5 +1,4 @@
import path from 'path' import path from 'path'
import fs from 'fs'
import {AbstractCache} from './cache-base' import {AbstractCache} from './cache-base'
// TODO: Maybe allow the user to override / tweak this set // TODO: Maybe allow the user to override / tweak this set
@ -17,11 +16,6 @@ export class ProjectDotGradleCache extends AbstractCache {
this.rootDir = rootDir this.rootDir = rootDir
} }
protected cacheOutputExists(): boolean {
const dir = this.getProjectDotGradleDir()
return fs.existsSync(dir)
}
protected getCachePath(): string[] { protected getCachePath(): string[] {
const dir = this.getProjectDotGradleDir() const dir = this.getProjectDotGradleDir()
return PATHS_TO_CACHE.map(x => path.resolve(dir, x)) return PATHS_TO_CACHE.map(x => path.resolve(dir, x))

View file

@ -4,20 +4,20 @@ import {ProjectDotGradleCache} from './cache-project-dot-gradle'
import {isCacheDisabled, isCacheReadOnly} from './cache-utils' import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
import {logCachingReport, CacheListener} from './cache-reporting' import {logCachingReport, CacheListener} from './cache-reporting'
const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED'
const BUILD_ROOT_DIR = 'BUILD_ROOT_DIR' const BUILD_ROOT_DIR = 'BUILD_ROOT_DIR'
const CACHE_LISTENER = 'CACHE_LISTENER' const CACHE_LISTENER = 'CACHE_LISTENER'
export async function restore(buildRootDirectory: string): Promise<void> { export async function restore(buildRootDirectory: string): Promise<void> {
if (!shouldRestoreCaches()) {
return
}
const gradleUserHomeCache = new GradleUserHomeCache(buildRootDirectory) const gradleUserHomeCache = new GradleUserHomeCache(buildRootDirectory)
const projectDotGradleCache = new ProjectDotGradleCache(buildRootDirectory) const projectDotGradleCache = new ProjectDotGradleCache(buildRootDirectory)
gradleUserHomeCache.init() gradleUserHomeCache.init()
if (isCacheDisabled()) {
core.info('Cache is disabled: will not restore state from previous builds.')
return
}
await core.group('Restore Gradle state from cache', async () => { await core.group('Restore Gradle state from cache', async () => {
core.saveState(BUILD_ROOT_DIR, buildRootDirectory) core.saveState(BUILD_ROOT_DIR, buildRootDirectory)
@ -38,6 +38,10 @@ export async function restore(buildRootDirectory: string): Promise<void> {
} }
export async function save(): Promise<void> { export async function save(): Promise<void> {
if (!shouldSaveCaches()) {
return
}
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER)) const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
if (isCacheReadOnly()) { if (isCacheReadOnly()) {
@ -56,3 +60,35 @@ export async function save(): Promise<void> {
logCachingReport(cacheListener) logCachingReport(cacheListener)
} }
function shouldRestoreCaches(): boolean {
if (isCacheDisabled()) {
core.info('Cache is disabled: will not restore state from previous builds.')
return false
}
if (process.env[CACHE_RESTORED_VAR]) {
core.info('Cache only restored on first action step.')
return false
}
// Export var that is detected in all later restore steps
core.exportVariable(CACHE_RESTORED_VAR, true)
// Export state that is detected in corresponding post-action step
core.saveState(CACHE_RESTORED_VAR, true)
return true
}
function shouldSaveCaches(): boolean {
if (isCacheDisabled()) {
core.info('Cache is disabled: will not save state for later builds.')
return false
}
if (!core.getState(CACHE_RESTORED_VAR)) {
core.info('Cache will only be saved in final post-action step.')
return false
}
return true
}