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);
   }