Forward project-wide change index deletions
When a project is deleted, all of its changes are removed from the
index.
Listen to the core onAllChangesDeletedForProject hook (introduced by
I4c8a536290) and forward a single project-scoped index/change request so
peers can delete all change documents for that project in one operation.
To propagate this event to the peer nodes, this change adds:
- a REST endpoint: DELETE
/plugins/high-availability/index/change/<project>~0
- A JGroups command
Bug: Issue 443765380
Change-Id: Idcd255f0e9778b6c4b8495dafd2f5ecdce4e1a91
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
index a787d3a..fd654ba 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
@@ -14,6 +14,8 @@
package com.ericsson.gerrit.plugins.highavailability.forwarder;
+import static com.ericsson.gerrit.plugins.highavailability.forwarder.rest.RestForwarder.ALL_CHANGES_FOR_PROJECT;
+
import com.ericsson.gerrit.plugins.highavailability.index.ChangeChecker;
import com.ericsson.gerrit.plugins.highavailability.index.ChangeCheckerImpl;
import com.ericsson.gerrit.plugins.highavailability.index.ForwardedIndexExecutor;
@@ -119,19 +121,29 @@
@Override
protected CompletableFuture<Boolean> doDelete(String id, Optional<IndexEvent> indexEvent)
throws IOException {
- indexer.delete(parseChangeId(id));
- log.atFine().log("Change %s successfully deleted from index", id);
+ if (ALL_CHANGES_FOR_PROJECT.equals(extractChangeId(id))) {
+ Project.NameKey projectName = parseProject(id);
+ indexer.deleteAllForProject(projectName);
+ log.atFine().log("All %s changes successfully deleted from index", projectName.get());
+ } else {
+ indexer.delete(parseChangeId(id));
+ log.atFine().log("Change %s successfully deleted from index", id);
+ }
return CompletableFuture.completedFuture(true);
}
private static Change.Id parseChangeId(String id) {
- return Change.id(Integer.parseInt(getChangeIdParts(id).get(1)));
+ return Change.id(Integer.parseInt(extractChangeId(id)));
}
private static Project.NameKey parseProject(String id) {
return Project.nameKey(getChangeIdParts(id).get(0));
}
+ private static String extractChangeId(String id) {
+ return getChangeIdParts(id).get(1);
+ }
+
private static List<String> getChangeIdParts(String id) {
return Splitter.on("~").splitToList(id);
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
index b73b676..59ba29b 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
@@ -14,6 +14,7 @@
package com.ericsson.gerrit.plugins.highavailability.forwarder;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.events.Event;
import java.util.concurrent.CompletableFuture;
@@ -119,4 +120,13 @@
* false.
*/
CompletableFuture<Boolean> removeFromProjectList(String projectName);
+
+ /**
+ * Forward the removal of all project changes from index to the other master.
+ *
+ * @param projectName the name of the project whose changes should be removed from the index
+ * @return {@link CompletableFuture} of true if successful, otherwise {@link CompletableFuture} of
+ * false.
+ */
+ CompletableFuture<Boolean> deleteAllChangesForProject(Project.NameKey projectName);
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/DeleteAllProjectChangesFromIndex.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/DeleteAllProjectChangesFromIndex.java
new file mode 100644
index 0000000..79933fb
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/DeleteAllProjectChangesFromIndex.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2025 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.ericsson.gerrit.plugins.highavailability.forwarder.jgroups;
+
+import com.google.gerrit.entities.Project;
+
+public class DeleteAllProjectChangesFromIndex extends Command {
+ static final String TYPE = "delete-all-project-changes-from-index";
+
+ private final Project.NameKey projectName;
+
+ protected DeleteAllProjectChangesFromIndex(Project.NameKey projectName) {
+ super(TYPE);
+ this.projectName = projectName;
+ }
+
+ public String getProjectName() {
+ return projectName.get();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/JGroupsForwarder.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/JGroupsForwarder.java
index 40a358e..9f1f179 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/JGroupsForwarder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/JGroupsForwarder.java
@@ -19,6 +19,7 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.events.Event;
import com.google.gson.Gson;
import com.google.inject.Inject;
@@ -108,6 +109,11 @@
return execute(new RemoveFromProjectList(projectName));
}
+ @Override
+ public CompletableFuture<Boolean> deleteAllChangesForProject(Project.NameKey projectName) {
+ return execute(new DeleteAllProjectChangesFromIndex(projectName));
+ }
+
private CompletableFuture<Boolean> execute(Command cmd) {
return executor.getAsync(() -> executeOnce(cmd));
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java
index 922fbd6..d53cd82 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java
@@ -20,8 +20,10 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.events.Event;
@@ -37,12 +39,14 @@
import org.apache.http.HttpException;
import org.apache.http.client.ClientProtocolException;
-class RestForwarder implements Forwarder {
+public class RestForwarder implements Forwarder {
enum RequestMethod {
POST,
DELETE
}
+ public static final String ALL_CHANGES_FOR_PROJECT = "0";
+
private static final FluentLogger log = FluentLogger.forEnclosingClass();
private final HttpSession httpSession;
@@ -115,6 +119,12 @@
return escapedProjectName + '~' + changeId;
}
+ @VisibleForTesting
+ public static String buildAllChangesForProjectEndpoint(String projectName) {
+ String escapedProjectName = Url.encode(projectName);
+ return escapedProjectName + '~' + ALL_CHANGES_FOR_PROJECT;
+ }
+
@Override
public CompletableFuture<Boolean> indexProject(String projectName, IndexEvent event) {
return execute(
@@ -150,6 +160,15 @@
Url.encode(projectName));
}
+ @Override
+ public CompletableFuture<Boolean> deleteAllChangesForProject(Project.NameKey projectName) {
+ return execute(
+ RequestMethod.DELETE,
+ "Delete all project changes from index",
+ "index/change",
+ buildAllChangesForProjectEndpoint(projectName.get()));
+ }
+
private static String buildProjectListEndpoint() {
return Joiner.on("/").join("cache", Constants.PROJECT_LIST);
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
index ec2a091..6475d99 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
@@ -18,6 +18,7 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.GroupIndexedListener;
@@ -58,6 +59,17 @@
currCtx.onlyWithContext((ctx) -> executeIndexChangeTask(projectName, id));
}
+ @Override
+ public void onAllChangesDeletedForProject(String projectName) {
+ currCtx.onlyWithContext((ctx) -> executeAllChangesDeletedForProject(projectName));
+ }
+
+ private void executeAllChangesDeletedForProject(String projectName) {
+ if (!Context.isForwardedEvent()) {
+ forwarder.deleteAllChangesForProject(Project.nameKey(projectName));
+ }
+ }
+
private void executeIndexChangeTask(String projectName, int id) {
if (!Context.isForwardedEvent()) {
String changeId = projectName + "~" + id;
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
index cc6939c..6a3a348 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -14,6 +14,7 @@
package com.ericsson.gerrit.plugins.highavailability.forwarder;
+import static com.ericsson.gerrit.plugins.highavailability.forwarder.rest.RestForwarder.buildAllChangesForProjectEndpoint;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -54,6 +55,7 @@
private static final int TEST_CHANGE_NUMBER = 123;
private static String TEST_PROJECT = "test/project";
+ private static final String TEST_PROJECT_ENCODED = "test%2Fproject";
private static String TEST_CHANGE_ID = TEST_PROJECT + "~" + TEST_CHANGE_NUMBER;
private static final boolean CHANGE_EXISTS = true;
private static final boolean CHANGE_DOES_NOT_EXIST = false;
@@ -112,6 +114,14 @@
}
@Test
+ public void AllChangesAreDeletedFromIndex() throws Exception {
+ handler
+ .index(buildAllChangesForProjectEndpoint(TEST_PROJECT), Operation.DELETE, Optional.empty())
+ .get(10, SECONDS);
+ verify(indexerMock, times(1)).deleteAllForProject(Project.nameKey(TEST_PROJECT_ENCODED));
+ }
+
+ @Test
public void changeToIndexDoesNotExist() throws Exception {
setupChangeAccessRelatedMocks(CHANGE_DOES_NOT_EXIST, CHANGE_OUTDATED);
handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty()).get(10, SECONDS);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
index 8a2745e..4b98fe1 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
@@ -14,6 +14,7 @@
package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+import static com.ericsson.gerrit.plugins.highavailability.forwarder.rest.RestForwarder.buildAllChangesForProjectEndpoint;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -32,6 +33,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gson.Gson;
@@ -80,6 +82,14 @@
PROJECT_NAME_URL_END + "~" + CHANGE_NUMBER);
private static final String DELETE_CHANGE_ENDPOINT =
Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/change", "~" + CHANGE_NUMBER);
+ private static final String DELETE_ALL_CHANGES_ENDPOINT =
+ Joiner.on("/")
+ .join(
+ URL,
+ PLUGINS,
+ PLUGIN_NAME,
+ "index/change",
+ buildAllChangesForProjectEndpoint(PROJECT_NAME));
private static final int ACCOUNT_NUMBER = 2;
private static final String INDEX_ACCOUNT_ENDPOINT =
Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/account", ACCOUNT_NUMBER);
@@ -246,6 +256,17 @@
}
@Test
+ public void testAllChangesDeletedFromIndexOK() throws Exception {
+ when(httpSessionMock.delete(eq(DELETE_ALL_CHANGES_ENDPOINT)))
+ .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
+ assertThat(
+ forwarder
+ .deleteAllChangesForProject(Project.nameKey(PROJECT_NAME))
+ .get(TEST_TIMEOUT, TEST_TIMEOUT_UNITS))
+ .isTrue();
+ }
+
+ @Test
public void testChangeDeletedFromIndexFailed() throws Exception {
when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT)))
.thenReturn(new HttpResult(FAILED, EMPTY_MSG));