blob: c34855079aa9fb5a0cd541b62c161a2f15253b20 [file] [log] [blame]
// Copyright (C) 2019 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.
package com.google.gerrit.plugins.checks;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.AtomicLongMap;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.plugins.checks.api.CombinedCheckState;
import com.google.gerrit.plugins.checks.cache.proto.Cache.CombinedCheckStateCacheKeyProto;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
/**
* Cache of {@link CombinedCheckState} per change.
*
* <p>In the absence of plugin-defined index fields, this cache is used to performantly populate the
* {@code combinedState} field in {@code ChangeCheckInfo} in the query path.
*/
@Singleton
public class CombinedCheckStateCache {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String NAME = "combined_check_state";
public static Module module() {
return new CacheModule() {
@Override
public void configure() {
persist(NAME, CombinedCheckStateCacheKeyProto.class, CombinedCheckState.class)
.version(1)
.maximumWeight(10000)
.diskLimit(-1)
.keySerializer(new ProtobufSerializer<>(CombinedCheckStateCacheKeyProto.parser()))
.valueSerializer(new EnumCacheSerializer<>(CombinedCheckState.class))
.loader(Loader.class);
}
};
}
@Singleton
static class Metrics {
// Pair of metric and manual counters, to work around the fact that metric classes have no
// getters.
private final Timer1<Boolean> reloadLatency;
private final AtomicLongMap<Boolean> reloadCount;
@Inject
Metrics(MetricMaker metricMaker) {
reloadLatency =
metricMaker.newTimer(
"checks/reload_combined_check_state",
new Description("Latency for reloading combined check state")
.setCumulative()
.setUnit(Units.MILLISECONDS),
Field.ofBoolean(
"updated", "whether reloading resulted in updating the cached value"));
reloadCount = AtomicLongMap.create();
}
void recordReload(boolean updated, Duration elapsed) {
reloadLatency.record(updated, elapsed.toNanos(), NANOSECONDS);
reloadCount.incrementAndGet(updated);
}
long getReloadCount(boolean updated) {
return reloadCount.get(updated);
}
}
private final LoadingCache<CombinedCheckStateCacheKeyProto, CombinedCheckState> cache;
private final Loader loader;
private final Metrics metrics;
@Inject
CombinedCheckStateCache(
@Named(NAME) LoadingCache<CombinedCheckStateCacheKeyProto, CombinedCheckState> cache,
Loader loader,
Metrics metrics) {
this.cache = cache;
this.loader = loader;
this.metrics = metrics;
}
/**
* Get the state from the cache, computing it from checks ref if necessary.
*
* @param project project containing the change.
* @param psId patch set to which the state corresponds.
* @return combined check state.
*/
public CombinedCheckState get(Project.NameKey project, PatchSet.Id psId) {
try {
return cache.get(key(project, psId));
} catch (ExecutionException e) {
throw new StorageException(e);
}
}
/**
* Load the state from primary storage, and update the state in the cache only if it changed.
*
* <p>This method does a cache lookup followed by a write, which is inherently racy. It is
* intended to be used whenever we need to recompute the combined check state, for example when
* sending a {@code ChangeCheckInfo} to the client. As a result, inconsistencies between the cache
* and the actual state should tend to get fixed up immediately after a user views the change.
*
* @param project project containing the change.
* @param psId patch set to which the state corresponds.
* @return combined check state.
*/
public CombinedCheckState reload(Project.NameKey project, PatchSet.Id psId) {
// Possible future optimization: short-circuit before calling this method, if an individual
// check transitioned between two CheckStates which would result in the same CombinedCheckState.
Stopwatch sw = Stopwatch.createStarted();
// Arbitrarily assume that the cache was updated unless we can conclusively prove it wasn't.
boolean updated = true;
try {
CombinedCheckStateCacheKeyProto key = key(project, psId);
CombinedCheckState newState = loader.load(key);
CombinedCheckState oldState = cache.getIfPresent(key);
if (newState != oldState) {
cache.put(key, newState);
} else {
updated = false;
}
return newState;
} finally {
metrics.recordReload(updated, sw.elapsed());
}
}
/**
* Update the state in the cache only if it changed.
*
* <p>This method returns no value and should be called when the caller may have recently updated
* the value in primary storage, but does not need the actual value. There is no guarantee that an
* update initiated by this method completes synchronously.
*
* <p>This method does a cache lookup followed by a write, which is inherently racy.
* Inconsistencies between the cache and the actual state should tend to get fixed up immediately
* after a user views the change, since the read path calls {@link #reload(Project.NameKey,
* PatchSet.Id)}.
*
* @param project project containing the change.
* @param psId patch set to which the state corresponds.
*/
public void updateIfNecessary(Project.NameKey project, PatchSet.Id psId) {
// Possible future optimization: make this whole method async, in the FanOutExecutor.
try {
reload(project, psId);
} catch (RuntimeException e) {
logger.atWarning().withCause(e).log(
"failed to reload CombinedCheckState for %s in %s", psId, project);
}
}
/**
* Directly put a state into the cache.
*
* @param project project containing the change.
* @param psId patch set to which the state corresponds.
* @param state combined check state.
*/
@VisibleForTesting
public void putForTest(Project.NameKey project, PatchSet.Id psId, CombinedCheckState state) {
cache.put(key(project, psId), state);
}
@VisibleForTesting
public long getReloadCount(boolean updated) {
return metrics.getReloadCount(updated);
}
@VisibleForTesting
public CacheStats getStats() {
return cache.stats();
}
private static CombinedCheckStateCacheKeyProto key(Project.NameKey project, PatchSet.Id psId) {
return CombinedCheckStateCacheKeyProto.newBuilder()
.setProject(project.get())
.setChangeId(psId.changeId().get())
.setPatchSetId(psId.get())
.build();
}
@Singleton
private static class Loader
extends CacheLoader<CombinedCheckStateCacheKeyProto, CombinedCheckState> {
private final Checks checks;
@Inject
Loader(Checks checks) {
this.checks = checks;
}
@Override
public CombinedCheckState load(CombinedCheckStateCacheKeyProto key) {
try {
return checks.getCombinedCheckState(
Project.nameKey(key.getProject()),
PatchSet.id(Change.id(key.getChangeId()), key.getPatchSetId()));
} catch (IOException e) {
throw new StorageException(e);
}
}
}
}