Skip to content

Commit

Permalink
Use Gradle 8.8 features for cleanup
Browse files Browse the repository at this point in the history
Gradle 8.8 introduces new features that allow us to avoid using
timestamp manipulation to force the cleanup of the Gradle User Home directory.

This solution is simpler and more robust, but relies on Gradle 8.8+ always being
used for the cache cleanup operation.

Fixes #24
  • Loading branch information
bigdaz committed Jun 28, 2024
1 parent 169bec5 commit 95ef722
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 41 deletions.
67 changes: 31 additions & 36 deletions sources/src/caching/cache-cleaner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'fs'
import path from 'path'
import {provisionAndMaybeExecute} from '../execution/gradle'
Expand All @@ -13,25 +12,20 @@ export class CacheCleaner {
this.tmpDir = tmpDir
}

async prepare(): Promise<void> {
// Reset the file-access journal so that files appear not to have been used recently
fs.rmSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true, force: true})
fs.mkdirSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true})
fs.writeFileSync(
path.resolve(this.gradleUserHome, 'caches/journal-1/file-access.properties'),
'inceptionTimestamp=0'
)

// Set the modification time of all files to the past: this timestamp is used when there is no matching entry in the journal
await this.ageAllFiles()

// Touch all 'gc' files so that cache cleanup won't run immediately.
await this.touchAllFiles('gc.properties')
async prepare(): Promise<string> {
// Save the current timestamp
const timestamp = Date.now().toString()
core.saveState('clean-timestamp', timestamp)
return timestamp
}

async forceCleanup(): Promise<void> {
// Age all 'gc' files so that cache cleanup will run immediately.
await this.ageAllFiles('gc.properties')
const cleanTimestamp = core.getState('clean-timestamp')
await this.forceCleanupFilesOlderThan(cleanTimestamp)
}

async forceCleanupFilesOlderThan(cleanTimestamp: string): Promise<void> {
core.info(`Cleaning up caches before ${cleanTimestamp}`)

// Run a dummy Gradle build to trigger cache cleanup
const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project')
Expand All @@ -40,11 +34,31 @@ export class CacheCleaner {
path.resolve(cleanupProjectDir, 'settings.gradle'),
'rootProject.name = "dummy-cleanup-project"'
)
fs.writeFileSync(
path.resolve(cleanupProjectDir, 'init.gradle'),
`
beforeSettings { settings ->
def cleanupTime = ${cleanTimestamp}
settings.caches {
cleanup = Cleanup.ALWAYS
releasedWrappers.removeUnusedEntriesOlderThan.set(cleanupTime)
snapshotWrappers.removeUnusedEntriesOlderThan.set(cleanupTime)
downloadedResources.removeUnusedEntriesOlderThan.set(cleanupTime)
createdResources.removeUnusedEntriesOlderThan.set(cleanupTime)
buildCache.removeUnusedEntriesOlderThan.set(cleanupTime)
}
}
`
)
fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}')

await provisionAndMaybeExecute('current', cleanupProjectDir, [
'-g',
this.gradleUserHome,
'-I',
'init.gradle',
'--quiet',
'--no-daemon',
'--no-scan',
Expand All @@ -53,23 +67,4 @@ export class CacheCleaner {
'noop'
])
}

private async ageAllFiles(fileName = '*'): Promise<void> {
core.debug(`Aging all files in Gradle User Home with name ${fileName}`)
await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date(0))
}

private async touchAllFiles(fileName = '*'): Promise<void> {
core.debug(`Touching all files in Gradle User Home with name ${fileName}`)
await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date())
}

private async setUtimes(pattern: string, timestamp: Date): Promise<void> {
const globber = await glob.create(pattern, {
implicitDescendants: false
})
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
}
}
31 changes: 26 additions & 5 deletions sources/test/jest/cache-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as exec from '@actions/exec'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'fs'
import path from 'path'
import {CacheCleaner} from '../../src/caching/cache-cleaner'
Expand All @@ -14,7 +15,7 @@ test('will cleanup unused dependency jars and build-cache entries', async () =>

await runGradleBuild(projectRoot, 'build', '3.1')

await cacheCleaner.prepare()
const timestamp = await cacheCleaner.prepare()

await runGradleBuild(projectRoot, 'build', '3.1.1')

Expand All @@ -26,7 +27,7 @@ test('will cleanup unused dependency jars and build-cache entries', async () =>
expect(fs.existsSync(commonsMath311)).toBe(true)
expect(fs.readdirSync(buildCacheDir).length).toBe(4) // gc.properties, build-cache-1.lock, and 2 task entries

await cacheCleaner.forceCleanup()
await cacheCleaner.forceCleanupFilesOlderThan(timestamp)

expect(fs.existsSync(commonsMath31)).toBe(false)
expect(fs.existsSync(commonsMath311)).toBe(true)
Expand All @@ -42,25 +43,39 @@ test('will cleanup unused gradle versions', async () => {
// Initialize HOME with 2 different Gradle versions
await runGradleWrapperBuild(projectRoot, 'build')
await runGradleBuild(projectRoot, 'build')
await cacheCleaner.prepare()

const timestamp = await cacheCleaner.prepare()

// Run with only one of these versions
await runGradleBuild(projectRoot, 'build')

const gradle802 = path.resolve(gradleUserHome, "caches/8.0.2")
const transforms3 = path.resolve(gradleUserHome, "caches/transforms-3")
const metadata100 = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.100")
const wrapper802 = path.resolve(gradleUserHome, "wrapper/dists/gradle-8.0.2-bin")
const gradleCurrent = path.resolve(gradleUserHome, "caches/8.8")
const metadataCurrent = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.106")

expect(fs.existsSync(gradle802)).toBe(true)
expect(fs.existsSync(transforms3)).toBe(true)
expect(fs.existsSync(metadata100)).toBe(true)
expect(fs.existsSync(wrapper802)).toBe(true)

expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)

await cacheCleaner.forceCleanup()
// The wrapper won't be removed if it was recently downloaded. Age it.
setUtimes(wrapper802, new Date(Date.now() - 48 * 60 * 60 * 1000))

await cacheCleaner.forceCleanupFilesOlderThan(timestamp)

expect(fs.existsSync(gradle802)).toBe(false)
expect(fs.existsSync(transforms3)).toBe(false)
expect(fs.existsSync(metadata100)).toBe(false)
expect(fs.existsSync(wrapper802)).toBe(false)

expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)
})

async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> {
Expand All @@ -86,3 +101,9 @@ function prepareTestProject(): string {
return projectRoot
}

async function setUtimes(pattern: string, timestamp: Date): Promise<void> {
const globber = await glob.create(pattern)
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
}

0 comments on commit 95ef722

Please sign in to comment.