Handle project deletion Handle the removal of a project with a soft-delete approach. Rather than executing a table scan that might be heavy from a latency and from a RCUs consumption prospective, we flag the existing items as obsolete by bumping up a project version refSpec in the form of `|project-name`, to which we associate the current version `version`. This way, deleting a project is just as simple as bumping up its version value. We then prepend the current version to the refSpec when looking up items, so that we can easily ignore obsolete entries. For example, if a project `foo` is deleted a new entry is created as such: | refPath | refValue | | |foo | 1 | If then a new project is created, new refSpecs will be prefixed with the new project version: | refPath | refValue | | |1/foo/refs/heads/master | 3355b5b8b73 | We selected the pipe symbol (|) as a prefix because it is prohibited in repository names, ensuring no ambiguity arises with actual repository names. Change-Id: I2cfdf2e812c9e68735b66eef3b756ec6084bf697
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 933fd3f..d78ef2d 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
@@ -30,6 +30,7 @@ import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError; 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; @@ -62,14 +63,22 @@ this.configuration = configuration; } - static String pathFor(Project.NameKey projectName, String refName) { - return "/" + projectName + "/" + refName; + String pathFor(Project.NameKey projectName, String refName) { + return versionPrefix(getCurrentVersion(projectName)) + "/" + projectName + "/" + refName; + } + + static String currentVersionKey(Project.NameKey projectName) { + return "|" + projectName; + } + + static String versionPrefix(Integer version) { + return version != null ? "|" + version : ""; } @Override public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException { try { - GetItemResult result = getPathFromDynamoDB(project, ref.getName()); + GetItemResult result = getPathFromDynamoDB(pathFor(project, ref.getName())); if (!exists(result)) { return true; } @@ -150,7 +159,10 @@ @Override public <T> void put(NameKey project, String refName, T value) throws GlobalRefDbSystemError { - String refPath = pathFor(project, refName); + doPut(project, pathFor(project, refName), value); + } + + public <T> void doPut(NameKey project, String refPath, T value) throws GlobalRefDbSystemError { String refValue = Optional.ofNullable(value).map(Object::toString).orElse(ObjectId.zeroId().getName()); try { @@ -197,7 +209,7 @@ @Override public boolean exists(Project.NameKey project, String refName) { try { - if (!exists(getPathFromDynamoDB(project, refName))) { + if (!exists(getPathFromDynamoDB(pathFor(project, refName)))) { logger.atFine().log("ref '%s' does not exist in dynamodb", pathFor(project, refName)); return false; } @@ -211,11 +223,23 @@ return false; } + @Nullable + public Integer getCurrentVersion(Project.NameKey project) { + // TODO: this should be served by a cache + String pathForVersion = currentVersionKey(project); + GetItemResult item = getPathFromDynamoDB(pathForVersion, false); + return exists(item) ? Integer.parseInt(item.getItem().get(REF_DB_VALUE_KEY).getS()) : null; + } + @Override public void remove(Project.NameKey project) throws GlobalRefDbSystemError { - // TODO: to remove all refs related to project we'd need to be able to query - // dynamodb by 'project': perhaps we should have a composite key of: - // PK: project, SK: ref + Integer currentVersion = getCurrentVersion(project); + int nextVersion = (currentVersion != null ? currentVersion : 0) + 1; + + doPut(project, currentVersionKey(project), Integer.toString(nextVersion)); + logger.atWarning().log( + "Project %s removed, current version %s, next version %s", + project, currentVersion, nextVersion); } @SuppressWarnings("unchecked") @@ -223,7 +247,7 @@ public <T> Optional<T> get(Project.NameKey project, String refName, Class<T> clazz) throws GlobalRefDbSystemError { try { - GetItemResult item = getPathFromDynamoDB(project, refName); + GetItemResult item = getPathFromDynamoDB(pathFor(project, refName)); if (!exists(item)) { return Optional.empty(); } @@ -239,11 +263,15 @@ } } - private GetItemResult getPathFromDynamoDB(Project.NameKey project, String refName) { + private GetItemResult getPathFromDynamoDB(String refPath) { + return getPathFromDynamoDB(refPath, true); + } + + private GetItemResult getPathFromDynamoDB(String refPath, Boolean consistentRead) { return dynamoDBClient.getItem( configuration.getRefsDbTableName(), - ImmutableMap.of(REF_DB_PRIMARY_KEY, new AttributeValue(pathFor(project, refName))), - true); + ImmutableMap.of(REF_DB_PRIMARY_KEY, new AttributeValue(refPath)), + consistentRead); } private boolean exists(GetItemResult result) {
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 38b6dc8..69d671a 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
@@ -241,6 +241,24 @@ assertThat(dynamoDBRefDatabase().compareAndPut(project, refName, null, newRefValue)).isTrue(); } + @Test + public void projectVersionShouldBeUsedAsPrefix() { + String versionKey = DynamoDBRefDatabase.currentVersionKey(project); + dynamoDBRefDatabase().doPut(project, versionKey, "1"); + + assertThat(dynamoDBRefDatabase().pathFor(project, "refs/heads/master")) + .isEqualTo("|1/" + project + "/refs/heads/master"); + } + + @Test + public void removeProjectShouldIncreaseProjectVersion() { + assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isNull(); + + dynamoDBRefDatabase().remove(project); + + assertThat(dynamoDBRefDatabase().getCurrentVersion(project)).isEqualTo(1); + } + private AmazonDynamoDB dynamoDBClient() { return plugin.getSysInjector().getInstance(AmazonDynamoDB.class); }