Introduce projectVersion cache Introduces a caching mechanism to reduce the number of requests to DynamoDB by storing the current life version of a project. Cherry-picked from bae28b0cca4900dacbedce70c745df8834633222. Adapted DynamoDBRefDatabaseIT test to avoid compilation error: cannot infer type arguments for TypeLiteral<T> '<>' with anonymous inner classes is not supported in -source 8 Change-Id: Ia260b0c7a684b3314bd32562eb0602fd4d54a20e
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabase.java index cabf2db..712f5d1 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabase.java +++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabase.java
@@ -14,6 +14,8 @@ package com.googlesource.gerrit.plugins.validation.dfsrefdb.dynamodb; +import static com.googlesource.gerrit.plugins.validation.dfsrefdb.dynamodb.ProjectVersionCacheModule.PROJECT_VERSION_CACHE; + import com.amazonaws.services.dynamodbv2.AcquireLockOptions; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient; @@ -28,13 +30,18 @@ import com.gerritforge.gerrit.globalrefdb.ExtendedGlobalRefDatabase; import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException; import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Project; import com.google.gerrit.entities.Project.NameKey; import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.name.Named; import java.util.Optional; +import java.util.concurrent.ExecutionException; import javax.inject.Singleton; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -52,15 +59,18 @@ private final AmazonDynamoDBLockClient lockClient; private final AmazonDynamoDB dynamoDBClient; private final Configuration configuration; + private final LoadingCache<String, Optional<Integer>> projectVersionCache; @Inject DynamoDBRefDatabase( AmazonDynamoDBLockClient lockClient, AmazonDynamoDB dynamoDBClient, - Configuration configuration) { + Configuration configuration, + @Named(PROJECT_VERSION_CACHE) LoadingCache<String, Optional<Integer>> projectVersionCache) { this.lockClient = lockClient; this.dynamoDBClient = dynamoDBClient; this.configuration = configuration; + this.projectVersionCache = projectVersionCache; } String pathFor(Project.NameKey projectName, String refName) { @@ -224,11 +234,12 @@ } @Nullable - public Integer getCurrentVersion(Project.NameKey project) { - // TODO: this should be served by a cache - String pathForVersion = currentVersionKey(project); - GetItemResult item = getItemFromDynamoDB(pathForVersion, false); - return exists(item) ? Integer.parseInt(item.getItem().get(REF_DB_VALUE_KEY).getS()) : null; + public Integer getCurrentVersion(Project.NameKey project) throws GlobalRefDbSystemError { + try { + return projectVersionCache.get(project.get()).orElse(null); + } catch (ExecutionException e) { + throw new GlobalRefDbSystemError("Could not check project version", e); + } } @Override @@ -267,14 +278,37 @@ return getItemFromDynamoDB(refPath, true); } - private GetItemResult getItemFromDynamoDB(String refPath, Boolean consistentRead) { + public GetItemResult getItemFromDynamoDB(String refPath, Boolean consistentRead) { return dynamoDBClient.getItem( configuration.getRefsDbTableName(), ImmutableMap.of(REF_DB_PRIMARY_KEY, new AttributeValue(refPath)), consistentRead); } - private boolean exists(GetItemResult result) { + public boolean exists(GetItemResult result) { return result.getItem() != null && !result.getItem().isEmpty(); } + + static class ProjectVersionCacheLoader extends CacheLoader<String, Optional<Integer>> { + + private final Provider<DynamoDBRefDatabase> dynamoDBRefDatabaseProvider; + + @Inject + public ProjectVersionCacheLoader(Provider<DynamoDBRefDatabase> dynamoDBRefDatabaseProvider) { + this.dynamoDBRefDatabaseProvider = dynamoDBRefDatabaseProvider; + } + + @Override + public Optional<Integer> load(String project) throws Exception { + GetItemResult item = + dynamoDBRefDatabaseProvider + .get() + .getItemFromDynamoDB(currentVersionKey(Project.nameKey(project)), false); + Integer currentVersion = + dynamoDBRefDatabaseProvider.get().exists(item) + ? Integer.parseInt(item.getItem().get(REF_DB_VALUE_KEY).getS()) + : null; + return Optional.ofNullable(currentVersion); + } + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/Module.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/Module.java index 946d401..4def9f6 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/Module.java +++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/Module.java
@@ -33,6 +33,7 @@ DynamicItem.bind(binder(), GlobalRefDatabase.class) .to(DynamoDBRefDatabase.class) .in(Scopes.SINGLETON); + install(new ProjectVersionCacheModule()); bind(AmazonDynamoDB.class).toProvider(AmazonDynamoDBProvider.class).in(SINGLETON); bind(AmazonDynamoDBLockClient.class).toProvider(DynamoDBLockClientProvider.class).in(SINGLETON); listener().to(DynamoDBLifeCycleManager.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/ProjectVersionCacheModule.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/ProjectVersionCacheModule.java new file mode 100644 index 0000000..55c581c --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/ProjectVersionCacheModule.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2024 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.googlesource.gerrit.plugins.validation.dfsrefdb.dynamodb; + +import com.google.gerrit.server.cache.CacheModule; +import com.google.inject.TypeLiteral; +import java.time.Duration; +import java.util.Optional; + +public class ProjectVersionCacheModule extends CacheModule { + + public static final String PROJECT_VERSION_CACHE = "projectVersion"; + + @Override + protected void configure() { + cache(PROJECT_VERSION_CACHE, String.class, new TypeLiteral<Optional<Integer>>() {}) + .expireAfterWrite(Duration.ofSeconds(60)) + .maximumWeight(Long.MAX_VALUE) + .loader(DynamoDBRefDatabase.ProjectVersionCacheLoader.class); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabaseIT.java b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabaseIT.java index 69d671a..ad82802 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabaseIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/dynamodb/DynamoDBRefDatabaseIT.java
@@ -21,11 +21,15 @@ import static org.testcontainers.containers.localstack.LocalStackContainer.Service.DYNAMODB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.google.common.cache.LoadingCache; import com.google.gerrit.acceptance.LightweightPluginDaemonTest; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.WaitUtil; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Project; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; import java.time.Duration; import java.util.Optional; import org.eclipse.jgit.lib.ObjectId; @@ -251,12 +255,22 @@ } @Test - public void removeProjectShouldIncreaseProjectVersion() { + public void removeProjectShouldIncreaseProjectVersionWhenNotCached() { + assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isNull(); + + dynamoDBRefDatabase().remove(project); + projectVersionCache().invalidate(project.get()); + + assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isEqualTo(1); + } + + @Test + public void removeProjectShouldKeepCurrentVersionWhenCached() { assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isNull(); dynamoDBRefDatabase().remove(project); - assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isEqualTo(1); + assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isNull(); } private AmazonDynamoDB dynamoDBClient() { @@ -267,6 +281,15 @@ return plugin.getSysInjector().getInstance(DynamoDBRefDatabase.class); } + private LoadingCache<String, Optional<Integer>> projectVersionCache() { + return plugin + .getSysInjector() + .getInstance( + Key.get( + new TypeLiteral<LoadingCache<String, Optional<Integer>>>() {}, + Names.named(ProjectVersionCacheModule.PROJECT_VERSION_CACHE))); + } + private void createRefInDynamoDB(Project.NameKey project, String refPath, String refValue) { dynamoDBRefDatabase().put(project, refPath, refValue); }