Handle batch-apply-object requests in the replica
Add support for the batch-apply-object in the `PullReplicationFilter`
class, which is used by the replica node in a pull-replication setup.
Deserialise the json message into the appopriate class, and pass the
message for further processing to the relevant "action" class.
Bug: Issue 40015567
Change-Id: I069561ed21a9cc07e667590a6afd38b7706858d4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java
index fc97945..253af58 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java
@@ -33,6 +33,8 @@
@UsedAt(PLUGIN_MULTI_SITE)
public static final String APPLY_OBJECTS_API_ENDPOINT = "apply-objects";
+ public static final String BATCH_APPLY_OBJECT_API_ENDPOINT = "batch-apply-object";
+
public static final String FETCH_ENDPOINT = "fetch";
public static final String INIT_PROJECT_ENDPOINT = "init-project";
public static final String DELETE_PROJECT_ENDPOINT = "delete-project";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
index ed6390a..bfe51a2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -27,6 +27,7 @@
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.flogger.FluentLogger;
+import com.google.common.reflect.TypeToken;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.projects.HeadInput;
@@ -63,6 +64,8 @@
import java.io.EOFException;
import java.io.IOException;
import java.io.PrintWriter;
+import java.lang.reflect.Type;
+import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -85,6 +88,7 @@
private FetchAction fetchAction;
private ApplyObjectAction applyObjectAction;
private ApplyObjectsAction applyObjectsAction;
+ private BatchApplyObjectAction batchApplyObjectAction;
private ProjectInitializationAction projectInitializationAction;
private UpdateHeadAction updateHEADAction;
private ProjectDeletionAction projectDeletionAction;
@@ -98,6 +102,7 @@
FetchAction fetchAction,
ApplyObjectAction applyObjectAction,
ApplyObjectsAction applyObjectsAction,
+ BatchApplyObjectAction batchApplyObjectAction,
ProjectInitializationAction projectInitializationAction,
UpdateHeadAction updateHEADAction,
ProjectDeletionAction projectDeletionAction,
@@ -107,6 +112,7 @@
this.fetchAction = fetchAction;
this.applyObjectAction = applyObjectAction;
this.applyObjectsAction = applyObjectsAction;
+ this.batchApplyObjectAction = batchApplyObjectAction;
this.projectInitializationAction = projectInitializationAction;
this.updateHEADAction = updateHEADAction;
this.projectDeletionAction = projectDeletionAction;
@@ -136,6 +142,9 @@
} else if (isApplyObjectsAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
writeResponse(httpResponse, doApplyObjects(httpRequest));
+ } else if (isBatchApplyObjectsAction(httpRequest)) {
+ failIfcurrentUserIsAnonymous();
+ writeResponse(httpResponse, doBatchApplyObject(httpRequest));
} else if (isInitProjectAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
if (!checkAcceptHeader(httpRequest, httpResponse)) {
@@ -213,7 +222,7 @@
@SuppressWarnings("unchecked")
private Response<String> doApplyObject(HttpServletRequest httpRequest)
throws RestApiException, IOException, PermissionBackendException {
- RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class));
+ RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class).getType());
IdString id = getProjectName(httpRequest).get();
return (Response<String>) applyObjectAction.apply(parseProjectResource(id), input);
@@ -222,15 +231,26 @@
@SuppressWarnings("unchecked")
private Response<String> doApplyObjects(HttpServletRequest httpRequest)
throws RestApiException, IOException, PermissionBackendException {
- RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class));
+ RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class).getType());
IdString id = getProjectName(httpRequest).get();
return (Response<String>) applyObjectsAction.apply(parseProjectResource(id), input);
}
@SuppressWarnings("unchecked")
+ private Response<Map<String, Object>> doBatchApplyObject(HttpServletRequest httpRequest)
+ throws RestApiException, IOException, PermissionBackendException {
+ TypeToken<List<RevisionInput>> collectionType = new TypeToken<>() {};
+ List<RevisionInput> inputs = readJson(httpRequest, collectionType.getType());
+ IdString id = getProjectName(httpRequest).get();
+
+ return (Response<Map<String, Object>>)
+ batchApplyObjectAction.apply(parseProjectResource(id), inputs);
+ }
+
+ @SuppressWarnings("unchecked")
private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception {
- HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class));
+ HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class).getType());
IdString id = getProjectName(httpRequest).get();
return (Response<String>) updateHEADAction.apply(parseProjectResource(id), input);
@@ -247,7 +267,7 @@
@SuppressWarnings("unchecked")
private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest)
throws IOException, RestApiException, PermissionBackendException {
- Input input = readJson(httpRequest, TypeLiteral.get(Input.class));
+ Input input = readJson(httpRequest, TypeLiteral.get(Input.class).getType());
IdString id = getProjectName(httpRequest).get();
return (Response<Map<String, Object>>) fetchAction.apply(parseProjectResource(id), input);
@@ -276,7 +296,7 @@
}
}
- private <T> T readJson(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
+ private <T> T readJson(HttpServletRequest httpRequest, Type typeToken)
throws IOException, BadRequestException {
try (BufferedReader br = httpRequest.getReader();
@@ -290,7 +310,7 @@
throw new BadRequestException("Expected JSON object", e);
}
- return gson.fromJson(json, typeLiteral.getType());
+ return gson.fromJson(json, typeToken);
} finally {
try {
// Reader.close won't consume the rest of the input. Explicitly consume the request
@@ -343,6 +363,12 @@
.endsWith(String.format("/%s~" + APPLY_OBJECTS_API_ENDPOINT, pluginName));
}
+ private boolean isBatchApplyObjectsAction(HttpServletRequest httpRequest) {
+ return httpRequest
+ .getRequestURI()
+ .endsWith(String.format("/%s~" + BATCH_APPLY_OBJECT_API_ENDPOINT, pluginName));
+ }
+
private boolean isFetchAction(HttpServletRequest httpRequest) {
return httpRequest.getRequestURI().endsWith(String.format("/%s~" + FETCH_ENDPOINT, pluginName));
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
index f767af4..850d04a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -9,7 +9,6 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.atLeastOnce;
-import static org.mockito.internal.verification.VerificationModeFactory.times;
import com.google.common.net.MediaType;
import com.google.gerrit.entities.Project;
@@ -42,6 +41,7 @@
@Mock private FetchAction fetchAction;
@Mock private ApplyObjectAction applyObjectAction;
@Mock private ApplyObjectsAction applyObjectsAction;
+ @Mock private BatchApplyObjectAction batchApplyObjectAction;
@Mock private ProjectInitializationAction projectInitializationAction;
@Mock private UpdateHeadAction updateHEADAction;
@Mock private ProjectDeletionAction projectDeletionAction;
@@ -62,6 +62,9 @@
String.format("any-prefix/projects/%s/%s~apply-objects", PROJECT_NAME, PLUGIN_NAME);
private final String HEAD_URI =
String.format("any-prefix/projects/%s/%s~HEAD", PROJECT_NAME, PLUGIN_NAME);
+
+ private final String BATCH_APPLY_OBJECT_URI =
+ String.format("any-prefix/projects/%s/%s~batch-apply-object", PROJECT_NAME, PLUGIN_NAME);
private final String DELETE_PROJECT_URI =
String.format("any-prefix/projects/%s/%s~delete-project", PROJECT_NAME, PLUGIN_NAME);
private final String INIT_PROJECT_URI =
@@ -78,6 +81,7 @@
fetchAction,
applyObjectAction,
applyObjectsAction,
+ batchApplyObjectAction,
projectInitializationAction,
updateHEADAction,
projectDeletionAction,
@@ -180,7 +184,6 @@
final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
pullReplicationFilter.doFilter(request, response, filterChain);
- verify(request, times(5)).getRequestURI();
verify(projectInitializationAction).initProject(eq(PROJECT_NAME_GIT));
verify(response).getWriter();
}
@@ -211,7 +214,6 @@
final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
pullReplicationFilter.doFilter(request, response, filterChain);
- verify(request, times(7)).getRequestURI();
verify(projectCache).get(Project.nameKey(PROJECT_NAME));
verify(projectDeletionAction).apply(any(ProjectResource.class), any());
verify(response).getWriter();
@@ -364,4 +366,33 @@
verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
+
+ @Test
+ public void shouldFilterBatchApplyObjectAction() throws Exception {
+
+ byte[] payloadApplyObject =
+ ("[{\"label\":\"Replication\",\"ref_name\":\"refs/heads/foo\","
+ + "\"revision_data\":{"
+ + "\"commit_object\":{\"type\":1,\"content\":\"some-content\"},"
+ + "\"tree_object\":{\"type\":2,\"content\":\"some-content\"},"
+ + "\"blobs\":[]}"
+ + "},"
+ + "{\"label\":\"Replication\",\"ref_name\":\"refs/heads/bar\","
+ + "\"revision_data\":{"
+ + "\"commit_object\":{\"type\":1,\"content\":\"some-content\"},"
+ + "\"tree_object\":{\"type\":2,\"content\":\"some-content\"},"
+ + "\"blobs\":[]}"
+ + "}]")
+ .getBytes(StandardCharsets.UTF_8);
+
+ defineBehaviours(payloadApplyObject, BATCH_APPLY_OBJECT_URI);
+
+ when(batchApplyObjectAction.apply(any(), any())).thenReturn(OK_RESPONSE);
+
+ PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
+ pullReplicationFilter.doFilter(request, response, filterChain);
+
+ verifyBehaviours();
+ verify(batchApplyObjectAction).apply(any(ProjectResource.class), any());
+ }
}