Clean up projects from the SharedRef Db

When a project is deleted using delete-project plugin the project
data needs to be cleaned up from the shared-ref database.

Bug: Issue 10766
Change-Id: Ie954c758921b58e39c67cf56ca72fec7b2aedb9a
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectDeletedSharedDbCleanup.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectDeletedSharedDbCleanup.java
new file mode 100644
index 0000000..7f58d39
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectDeletedSharedDbCleanup.java
@@ -0,0 +1,53 @@
+// 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.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import java.io.IOException;
+
+public class ProjectDeletedSharedDbCleanup implements ProjectDeletedListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SharedRefDatabase sharedDb;
+
+  private final ValidationMetrics validationMetrics;
+
+  @Inject
+  public ProjectDeletedSharedDbCleanup(
+      SharedRefDatabase sharedDb, ValidationMetrics validationMetrics) {
+    this.sharedDb = sharedDb;
+    this.validationMetrics = validationMetrics;
+  }
+
+  @Override
+  public void onProjectDeleted(Event event) {
+    String projectName = event.getProjectName();
+    logger.atInfo().log(
+        "Deleting project '%s'. Will perform a cleanup in Shared-Ref database.", projectName);
+
+    try {
+      sharedDb.removeProject(projectName);
+    } catch (IOException e) {
+      validationMetrics.incrementSplitBrain();
+      logger.atSevere().withCause(e).log(
+          "Project '%s' deleted from GIT but it was not able to cleanup"
+              + " from Shared-Ref database",
+          projectName);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
index 790935b..cfbd783 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
@@ -156,4 +156,12 @@
    * @return true if the ref exists on the project
    */
   boolean exists(String project, String refName);
+
+  /**
+   * Clean project path from SharedRefDatabase
+   *
+   * @param project project name
+   * @throws IOException
+   */
+  void removeProject(String project) throws IOException;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
index 321ad8a..afd3766 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
@@ -73,6 +73,15 @@
   }
 
   @Override
+  public void removeProject(String project) throws IOException {
+    try {
+      client.delete().deletingChildrenIfNeeded().forPath("/" + project);
+    } catch (Exception e) {
+      throw new IOException(String.format("Not able to delete project '%s'", project), e);
+    }
+  }
+
+  @Override
   public boolean exists(String project, String refName) throws ZookeeperRuntimeException {
     try {
       return client.checkExists().forPath(pathFor(project, refName)) != null;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java
index 927591e..3e8f75a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java
@@ -14,8 +14,11 @@
 
 package com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
 
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.AbstractModule;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
+import com.googlesource.gerrit.plugins.multisite.validation.ProjectDeletedSharedDbCleanup;
 import com.googlesource.gerrit.plugins.multisite.validation.ZkConnectionConfig;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
 import org.apache.curator.framework.CuratorFramework;
@@ -38,5 +41,7 @@
             new ZkConnectionConfig(
                 cfg.getZookeeperConfig().buildCasRetryPolicy(),
                 cfg.getZookeeperConfig().getZkInterProcessLockTimeOut()));
+
+    DynamicSet.bind(binder(), ProjectDeletedListener.class).to(ProjectDeletedSharedDbCleanup.class);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
index 9e85563..8819b8b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
@@ -15,7 +15,12 @@
 package com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
 
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.googlesource.gerrit.plugins.multisite.validation.ProjectDeletedSharedDbCleanup;
+import com.googlesource.gerrit.plugins.multisite.validation.ValidationMetrics;
 import com.googlesource.gerrit.plugins.multisite.validation.ZkConnectionConfig;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
@@ -36,6 +41,8 @@
   ZkSharedRefDatabase zkSharedRefDatabase;
   SharedRefEnforcement refEnforcement;
 
+  ValidationMetrics mockValidationMetrics;
+
   @Before
   public void setup() {
     refEnforcement = new DefaultSharedRefEnforcement();
@@ -50,6 +57,8 @@
             new ZkConnectionConfig(
                 new RetryNTimes(NUMBER_OF_RETRIES, SLEEP_BETWEEN_RETRIES_MS),
                 TRANSACTION_LOCK_TIMEOUT));
+
+    mockValidationMetrics = mock(ValidationMetrics.class);
   }
 
   @After
@@ -156,8 +165,62 @@
     return SharedRefDatabase.newRef(aBranchRef(), objectId);
   }
 
+  @Test
+  public void removeProjectShouldRemoveTheWholePathInZk() throws Exception {
+    String projectName = A_TEST_PROJECT_NAME;
+    Ref someRef = refOf(AN_OBJECT_ID_1);
+
+    zookeeperContainer.createRefInZk(projectName, someRef);
+
+    assertThat(zookeeperContainer.readRefValueFromZk(projectName, someRef))
+        .isEqualTo(AN_OBJECT_ID_1);
+
+    assertThat(getNumChildrenForPath("/")).isEqualTo(1);
+
+    zkSharedRefDatabase.removeProject(projectName);
+
+    assertThat(getNumChildrenForPath("/")).isEqualTo(0);
+  }
+
+  @Test
+  public void aDeleteProjectEventShouldCleanupProjectFromZk() throws Exception {
+    String projectName = A_TEST_PROJECT_NAME;
+    Ref someRef = refOf(AN_OBJECT_ID_1);
+    ProjectDeletedSharedDbCleanup projectDeletedSharedDbCleanup =
+        new ProjectDeletedSharedDbCleanup(zkSharedRefDatabase, mockValidationMetrics);
+
+    ProjectDeletedListener.Event event =
+        new ProjectDeletedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return projectName;
+          }
+
+          @Override
+          public NotifyHandling getNotify() {
+            return NotifyHandling.NONE;
+          }
+        };
+
+    zookeeperContainer.createRefInZk(projectName, someRef);
+
+    assertThat(getNumChildrenForPath("/")).isEqualTo(1);
+
+    projectDeletedSharedDbCleanup.onProjectDeleted(event);
+
+    assertThat(getNumChildrenForPath("/")).isEqualTo(0);
+  }
+
   @Override
   public String testBranch() {
     return "branch_" + nameRule.getMethodName();
   }
+
+  private int getNumChildrenForPath(String path) throws Exception {
+    return zookeeperContainer
+        .getCurator()
+        .checkExists()
+        .forPath(String.format(path))
+        .getNumChildren();
+  }
 }