blob: 4743a01ec6bf018830aa46e904c68384f91e2953 [file] [log] [blame]
// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import com.google.common.flogger.FluentLogger
import com.google.gerrit.entities.Project
import com.google.gerrit.extensions.annotations.*
import com.google.gerrit.extensions.events.LifecycleListener
import com.google.gerrit.lifecycle.LifecycleModule
import com.google.gerrit.metrics.*
import com.google.gerrit.server.config.*
import com.google.gerrit.server.git.LocalDiskRepositoryManager
import com.google.inject.Singleton
import org.eclipse.jgit.lib.Constants
import javax.inject.Inject
import java.util.concurrent.TimeUnit
import static groovy.io.FileType.FILES
@Singleton
@Listen
class RepoRepackTracker implements LifecycleListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass()
private static final DESCRIPTION_GC = "Check gc running for the project"
private static final DESCRIPTION_REPACK = "Check repack running for the project"
private static final NAME_GC = "is_gc_running_per_project"
private static final NAME_REPACK = "is_repack_running_per_project"
private static final GC_PID_FILE = "gc.pid"
private static final GIT_PACK_FOLDER = "objects/pack"
private static final TMP_PREFIX = "tmp_"
private static final TMP_SUFFIX = ".tmp"
@Inject
@PluginName
String pluginName
@Inject
PluginConfigFactory configFactory
@Inject
MetricMaker metrics
@Inject
LocalDiskRepositoryManager repoMgr
private def tmpFilter = ~/^(${TMP_PREFIX}.*|.*${TMP_SUFFIX})$/
private long repackStalenessAfterMs
private long gcStalenessAfterMs
CallbackMetric1<String, Long> callbackMetricGc
CallbackMetric1<String, Long> callbackMetricRepack
List<String> projects
@Override
void start() {
PluginConfig pluginConfig = configFactory.getFromGerritConfig(pluginName)
long repackStalenessAfter = ConfigUtil.getTimeUnit(
pluginConfig.getString("considerRepackStaleAfter", "60 minutes"),
60L,
TimeUnit.MINUTES)
long gcStalenessAfter = ConfigUtil.getTimeUnit(
pluginConfig.getString("considerGcStaleAfter", "720 minutes"),
720L,
TimeUnit.MINUTES)
repackStalenessAfterMs = TimeUnit.MILLISECONDS.convert(repackStalenessAfter, TimeUnit.MINUTES)
gcStalenessAfterMs = TimeUnit.MILLISECONDS.convert(gcStalenessAfter, TimeUnit.MINUTES)
projects = pluginConfig.getStringList("project")
callbackMetricGc = createCallbackMetric(NAME_GC, DESCRIPTION_GC)
callbackMetricRepack = createCallbackMetric(NAME_REPACK, DESCRIPTION_REPACK)
addMetricsTriggers()
logger.atInfo().log("Plugin %s started for %d projects (considering gc stale after %d minutes, repack after %d minutes)",
pluginName,
projects.size(),
gcStalenessAfter,
repackStalenessAfter)
}
CallbackMetric1<String, Long> createCallbackMetric(String name, String description) {
metrics.newCallbackMetric(
name,
Long.class,
new Description(description).setGauge(),
Field.ofProjectName("repository_name")
.description(description)
.build()
)
}
void addMetricsTriggers() {
metrics.newTrigger(
callbackMetricRepack, {
if (projects.isEmpty()) {
callbackMetricRepack.forceCreate("")
} else {
projects.each { e ->
callbackMetricRepack.set(e, checkRepackingRunningForProject(e))
}
callbackMetricRepack.prune()
}
})
metrics.newTrigger(
callbackMetricGc, {
if (projects.isEmpty()) {
callbackMetricGc.forceCreate("")
} else {
projects.each { e ->
callbackMetricGc.set(e, checkGCRunningForProject(e))
}
callbackMetricGc.prune()
}
})
}
long checkGCRunningForProject(String projectName) {
def isGcRunning = 0L
try {
def repoDir = getRepoDir(projectName)
def gcPid = new File(repoDir, GC_PID_FILE)
isGcRunning = (gcPid.exists() && !isFileStale(gcPid))? 1L : 0L
} catch (Exception e) {
logger.atSevere().withCause(e).log("Could not check project %s", projectName)
}
isGcRunning
}
long checkRepackingRunningForProject(String projectName) {
def isRepackRunning = 0L
try {
def repoDir = getRepoDir(projectName)
def packDir = new File(repoDir, GIT_PACK_FOLDER)
if (packDir.exists() && hasRepackTmpFiles(packDir)) {
isRepackRunning = 1L
}
} catch (Exception e) {
logger.atSevere().withCause(e).log("Could not check project %s", projectName)
}
isRepackRunning
}
File getRepoDir(String projectName) {
def name = Project.nameKey(projectName)
def path = repoMgr.getBasePath(name)
return path.resolve("${name.get()}${Constants.DOT_GIT_EXT}").toFile()
}
boolean hasRepackTmpFiles(folder) {
def modifiedCutoffTime = System.currentTimeMillis() - repackStalenessAfterMs
def tmpFileFound = false
folder.traverse(type: FILES, nameFilter: tmpFilter) { file ->
if (file.lastModified() >= modifiedCutoffTime) {
tmpFileFound = true
}
}
return tmpFileFound
}
boolean isFileStale(File f) {
def fileExpiry = f.lastModified() + gcStalenessAfterMs
return System.currentTimeMillis() > fileExpiry
}
@Override
void stop() {}
}
class RepoRepackTrackerModule extends LifecycleModule {
protected void configure() {
listener().to(RepoRepackTracker)
}
}
modules = [RepoRepackTrackerModule]