Skip to content

Commit

Permalink
Improve userdev cache cleanup strategy
Browse files Browse the repository at this point in the history
For now, new config properties are experimental, v1 cleanup is still implemented, and the old property is also used as the fallback value for the new expiry time property.
  • Loading branch information
jpenilla committed Dec 30, 2024
1 parent 2c814fb commit 83dcee1
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,12 @@ fun ioDispatcher(name: String): ExecutorCoroutineDispatcher =
Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
object : ThreadFactory {
val logger = Logging.getLogger("$name-ioDispatcher-${ioDispatcherCount.getAndIncrement()}")
val id = ioDispatcherCount.getAndIncrement()
val logger = Logging.getLogger("$name-ioDispatcher-$id")
val count = AtomicInteger(0)

override fun newThread(r: Runnable): Thread {
val thr = Thread(r, "$name-ioDispatcher-${ioDispatcherCount.getAndIncrement()}-Thread-${count.getAndIncrement()}")
val thr = Thread(r, "$name-ioDispatcher-$id-Thread-${count.getAndIncrement()}")
thr.setUncaughtExceptionHandler { thread, ex ->
logger.error("Uncaught exception in thread $thread", ex)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import io.papermc.paperweight.userdev.internal.setup.SetupHandler
import io.papermc.paperweight.userdev.internal.setup.UserdevSetup
import io.papermc.paperweight.userdev.internal.setup.UserdevSetupTask
import io.papermc.paperweight.userdev.internal.util.cleanSharedCaches
import io.papermc.paperweight.userdev.internal.util.deleteUnusedAfter
import io.papermc.paperweight.userdev.internal.util.delayCleanupBy
import io.papermc.paperweight.userdev.internal.util.expireUnusedAfter
import io.papermc.paperweight.userdev.internal.util.genSources
import io.papermc.paperweight.userdev.internal.util.performCleanupAfter
import io.papermc.paperweight.userdev.internal.util.sharedCaches
import io.papermc.paperweight.util.*
import io.papermc.paperweight.util.constants.*
Expand Down Expand Up @@ -350,17 +352,20 @@ abstract class PaperweightUser : Plugin<Project> {
}

val serviceName = "paperweight-userdev:setupService:$bundleHash"
val ret = target.gradle.sharedServices
.registerIfAbsent(serviceName, UserdevSetup::class) {
parameters {
cache.set(cacheDir)
bundleZip.set(devBundleZip)
bundleZipHash.set(bundleHash)
downloadService.set(target.download)
genSources.set(target.genSources)
deleteUnusedAfter.set(deleteUnusedAfter(target))
}
val ret = target.gradle.sharedServices.registerIfAbsent(serviceName, UserdevSetup::class) {
parameters {
cache.set(cacheDir)
downloadService.set(target.download)
genSources.set(target.genSources)

bundleZip.set(devBundleZip)
bundleZipHash.set(bundleHash)

expireUnusedAfter.set(expireUnusedAfter(target))
performCleanupAfter.set(performCleanupAfter(target))
delayCleanupBy.set(delayCleanupBy(target))
}
}
buildEventsListenerRegistry.onTaskCompletion(ret)
return ret
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* paperweight is a Gradle plugin for the PaperMC project.
*
* Copyright (c) 2023 Kyle Wood (DenWav)
* Contributors
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 only, no later versions.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/

package io.papermc.paperweight.userdev.internal.action

import io.papermc.paperweight.userdev.internal.util.formatNs
import io.papermc.paperweight.util.*
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import kotlin.io.path.*
import org.gradle.api.logging.LogLevel
import org.gradle.api.logging.Logging

class CacheManager(private val root: Path) {
companion object {
private val logger = Logging.getLogger(CacheManager::class.java)
}

data class MaintenanceInfo(
val lastCleanup: Long = System.currentTimeMillis(),
val scheduledCleanup: Long? = null,
) {
fun writeTo(file: Path) {
file.createParentDirectories().writeText(gson.toJson(this))
}

companion object {
fun readFrom(file: Path): MaintenanceInfo = gson.fromJson(file)
}
}

fun performMaintenance(
expireUnusedAfter: Long,
performCleanupAfter: Long,
delayCleanupBy: Long,
bundleZipHash: String,
) {
if (!root.isDirectory()) {
return
}

val maintenanceLock = root.resolve("maintenance.lock")
val maintenanceFile = root.resolve("maintenance.json")

val start = System.nanoTime()

withLock(maintenanceLock) {
logger.info("paperweight-userdev: Acquired cache maintenance lock in ${formatNs(System.nanoTime() - start)}")

// update last used for final outputs in case the task was up-to-date
root.listDirectoryEntries().forEach { entry ->
val metadataFile = entry.resolve(WorkGraph.METADATA_FILE)
if (!entry.isDirectory() || !metadataFile.isRegularFile() || entry.resolve("lock").exists()) {
return@forEach
}
if (entry.name.endsWith("_$bundleZipHash")) {
gson.fromJson<WorkGraph.Metadata>(metadataFile).updateLastUsed().writeTo(metadataFile)
}
}

if (maintenanceFile.isRegularFile()) {
val info = MaintenanceInfo.readFrom(maintenanceFile)
if (System.currentTimeMillis() - info.lastCleanup < performCleanupAfter) {
return@withLock
}
if (info.scheduledCleanup == null) {
val cleanup = System.currentTimeMillis() + delayCleanupBy
logger.info("paperweight-userdev: Scheduled cache cleanup for after ${Instant.ofEpochMilli(cleanup)}")
info.copy(scheduledCleanup = cleanup).writeTo(maintenanceFile)
} else if (System.currentTimeMillis() >= info.scheduledCleanup) {
if (cleanup(expireUnusedAfter)) {
info.copy(lastCleanup = System.currentTimeMillis(), scheduledCleanup = null).writeTo(maintenanceFile)
}
// else: cleanup was skipped due to locked cache entry, try again later
}
} else {
MaintenanceInfo().writeTo(maintenanceFile)
}
}

logger.info("paperweight-userdev: Finished cache maintenance in ${formatNs(System.nanoTime() - start)}")
}

private fun cleanup(deleteUnusedAfter: Long): Boolean {
val tryDelete = mutableListOf<Path>()
val keep = mutableListOf<Path>()
root.listDirectoryEntries().forEach {
val metadataFile = it.resolve(WorkGraph.METADATA_FILE)
if (!metadataFile.isRegularFile()) {
return@forEach
}
if (it.resolve("lock").exists()) {
logger.info("paperweight-userdev: Aborted cache cleanup due to locked cache entry (${it.name})")
return false
}
val since = System.currentTimeMillis() - metadataFile.getLastModifiedTime().toMillis()
if (since > deleteUnusedAfter) {
tryDelete.add(it)
} else {
keep.add(it)
}
}

var deleted = 0
var deletedSize = 0L
if (tryDelete.isNotEmpty()) {
keep.forEach { k ->
val metadataFile = k.resolve(WorkGraph.METADATA_FILE)
gson.fromJson<WorkGraph.Metadata>(metadataFile).skippedWhenUpToDate?.let {
tryDelete.removeIf { o -> o.name in it }
}
}

tryDelete.forEach {
deleted++
it.deleteRecursive { toDelete ->
if (toDelete.isRegularFile()) {
deletedSize += Files.size(toDelete)
}
}
}
}

val level = if (deleted > 0) LogLevel.LIFECYCLE else LogLevel.INFO
logger.log(
level,
"paperweight-userdev: Deleted $deleted expired cache entries totaling ${deletedSize / 1024}KB"
)
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class WorkGraph(
) {
companion object {
private val logger = Logging.getLogger(WorkGraph::class.java)
const val METADATA_FILE = "metadata.json"
}

class Node(
Expand Down Expand Up @@ -151,7 +152,7 @@ class WorkGraph(

val lockFile = work.resolve("${node.registration.name}_$inputHash/lock")

val metadataFile = work.resolve("${node.registration.name}_$inputHash/metadata.json")
val metadataFile = work.resolve("${node.registration.name}_$inputHash/$METADATA_FILE")
val upToDate = withLock(lockFile) {
if (metadataFile.exists()) {
val metadata = metadataFile.bufferedReader().use { gson.fromJson(it, Metadata::class.java) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,26 +127,26 @@ interface SetupHandler {

companion object {
@Suppress("unchecked_cast")
fun create(
parameters: UserdevSetup.Parameters,
bundleInfo: BundleInfo<Any>
): SetupHandler = when (bundleInfo.config) {
is GenerateDevBundle.DevBundleConfig -> SetupHandlerImpl(
parameters,
bundleInfo as BundleInfo<GenerateDevBundle.DevBundleConfig>,
)

is DevBundleV5.Config -> SetupHandlerImplV5(
parameters,
bundleInfo as BundleInfo<DevBundleV5.Config>
)

is DevBundleV2.Config -> SetupHandlerImplV2(
parameters,
bundleInfo as BundleInfo<DevBundleV2.Config>
)

else -> throw PaperweightException("Unknown dev bundle config type: ${bundleInfo.config::class.java.typeName}")
fun create(parameters: UserdevSetup.Parameters): SetupHandler {
val bundleInfo = readBundleInfo(parameters.bundleZip.path)
return when (bundleInfo.config) {
is GenerateDevBundle.DevBundleConfig -> SetupHandlerImpl(
parameters,
bundleInfo as BundleInfo<GenerateDevBundle.DevBundleConfig>,
)

is DevBundleV5.Config -> SetupHandlerImplV5(
parameters,
bundleInfo as BundleInfo<DevBundleV5.Config>
)

is DevBundleV2.Config -> SetupHandlerImplV2(
parameters,
bundleInfo as BundleInfo<DevBundleV2.Config>
)

else -> throw PaperweightException("Unknown dev bundle config type: ${bundleInfo.config::class.java.typeName}")
}
}
}
}
Loading

0 comments on commit 83dcee1

Please sign in to comment.