Merge changes from topic "apply-batch-object"
* changes:
Handle batch-apply-object requests in the replica
Introduce `apply-batch-object` REST API endpoint
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
index bcbd73d..54cc6f4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
@@ -37,6 +37,7 @@
import com.google.inject.Provider;
import com.googlesource.gerrit.plugins.replication.ObservableQueue;
import com.googlesource.gerrit.plugins.replication.pull.FetchResultProcessing.GitUpdateProcessing;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
@@ -44,8 +45,8 @@
import com.googlesource.gerrit.plugins.replication.pull.filter.ApplyObjectsRefsFilter;
import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URISyntaxException;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -57,9 +58,9 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -89,7 +90,7 @@
private final Provider<SourcesCollection> sources; // For Guice circular dependency
private volatile boolean running;
private volatile boolean replaying;
- private final Queue<ReferenceUpdatedEvent> beforeStartupEventsQueue;
+ private final Queue<ReferenceBatchUpdatedEvent> beforeStartupEventsQueue;
private FetchApiClient.Factory fetchClientFactory;
private Integer fetchCallsTimeout;
private ExcludedRefsFilter refsFilter;
@@ -175,14 +176,30 @@
if (e.type.equals(BATCH_REF_UPDATED_EVENT_TYPE)) {
BatchRefUpdateEvent event = (BatchRefUpdateEvent) e;
- event.refUpdates.get().stream()
- .sorted(ReplicationQueue::sortByMetaRefAsLast)
- .forEachOrdered(
- refUpdateAttribute -> {
- if (isRefToBeReplicated(refUpdateAttribute.refName)) {
- fireRefUpdate(refUpdateAttribute, e.eventCreatedOn);
- }
- });
+ long eventCreatedOn = e.eventCreatedOn;
+ List<ReferenceUpdatedEvent> refs =
+ event.refUpdates.get().stream()
+ .filter(u -> isRefToBeReplicated(u.refName))
+ .map(
+ u -> {
+ repLog.info(
+ "Ref event received: {} on project {}:{} - {} => {}",
+ refUpdateType(u.oldRev, u.newRev),
+ event.getProjectNameKey().get(),
+ u.refName,
+ u.oldRev,
+ u.newRev);
+ return ReferenceUpdatedEvent.from(u, eventCreatedOn);
+ })
+ .sorted(ReplicationQueue::sortByMetaRefAsLast)
+ .collect(Collectors.toList());
+
+ if (!refs.isEmpty()) {
+ ReferenceBatchUpdatedEvent referenceBatchUpdatedEvent =
+ ReferenceBatchUpdatedEvent.create(
+ event.getProjectNameKey().get(), refs, eventCreatedOn);
+ fire(referenceBatchUpdatedEvent);
+ }
}
return;
}
@@ -191,22 +208,25 @@
RefUpdatedEvent event = (RefUpdatedEvent) e;
if (isRefToBeReplicated(event.getRefName())) {
- fireRefUpdate(event.refUpdate.get(), event.eventCreatedOn);
+ RefUpdateAttribute refUpdateAttribute = event.refUpdate.get();
+ repLog.info(
+ "Ref event received: {} on project {}:{} - {} => {}",
+ refUpdateType(refUpdateAttribute.oldRev, refUpdateAttribute.newRev),
+ event.getProjectNameKey().get(),
+ refUpdateAttribute.refName,
+ refUpdateAttribute.oldRev,
+ refUpdateAttribute.newRev);
+
+ ReferenceBatchUpdatedEvent referenceBatchUpdatedEvent =
+ ReferenceBatchUpdatedEvent.create(
+ event.getProjectNameKey().get(),
+ List.of(ReferenceUpdatedEvent.from(refUpdateAttribute, e.eventCreatedOn)),
+ e.eventCreatedOn);
+ fire(referenceBatchUpdatedEvent);
}
}
}
- private void fireRefUpdate(RefUpdateAttribute refUpdate, long eventCreatedOn) {
- repLog.info(
- "Ref event received: {} on project {}:{} - {} => {}",
- refUpdateType(refUpdate.oldRev, refUpdate.newRev),
- refUpdate.project,
- refUpdate.refName,
- refUpdate.oldRev,
- refUpdate.newRev);
- fire(ReferenceUpdatedEvent.from(refUpdate, eventCreatedOn));
- }
-
@Override
public void onProjectDeleted(ProjectDeletedListener.Event event) {
Project.NameKey project = Project.nameKey(event.getProjectName());
@@ -217,10 +237,10 @@
source.getApis().forEach(apiUrl -> source.scheduleDeleteProject(apiUrl, project)));
}
- private static int sortByMetaRefAsLast(RefUpdateAttribute a, RefUpdateAttribute b) {
- repLog.debug("sortByMetaRefAsLast({} <=> {})", a.refName, b.refName);
+ private static int sortByMetaRefAsLast(ReferenceUpdatedEvent a, ReferenceUpdatedEvent b) {
+ repLog.debug("sortByMetaRefAsLast({} <=> {})", a.refName(), b.refName());
return Boolean.compare(
- RefNames.isNoteDbMetaRef(a.refName), RefNames.isNoteDbMetaRef(b.refName));
+ RefNames.isNoteDbMetaRef(a.refName()), RefNames.isNoteDbMetaRef(b.refName()));
}
private static String refUpdateType(String oldRev, String newRev) {
@@ -237,13 +257,13 @@
return !refsFilter.match(refName);
}
- private void fire(ReferenceUpdatedEvent event) {
+ private void fire(ReferenceBatchUpdatedEvent event) {
ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher.get()));
fire(event, state);
state.markAllFetchTasksScheduled();
}
- private void fire(ReferenceUpdatedEvent event, ReplicationState state) {
+ private void fire(ReferenceBatchUpdatedEvent event, ReplicationState state) {
if (!running) {
stateLog.warn(
"Replication plugin did not finish startup before event, event replication is postponed",
@@ -263,12 +283,7 @@
final Consumer<Source> callFunction =
callFunction(
- Project.nameKey(event.projectName()),
- event.objectId(),
- event.refName(),
- event.eventCreatedOn(),
- event.isDelete(),
- state);
+ Project.nameKey(event.projectName()), event.refs(), event.eventCreatedOn(), state);
fetchCallsPool
.submit(() -> allSources.parallelStream().forEach(callFunction))
.get(fetchCallsTimeout, TimeUnit.MILLISECONDS);
@@ -288,13 +303,10 @@
private Consumer<Source> callFunction(
NameKey project,
- ObjectId objectId,
- String refName,
+ List<ReferenceUpdatedEvent> refs,
long eventCreatedOn,
- boolean isDelete,
ReplicationState state) {
- CallFunction call =
- getCallFunction(project, objectId, refName, eventCreatedOn, isDelete, state);
+ CallFunction call = getCallFunction(project, refs, eventCreatedOn, state);
return (source) -> {
boolean callSuccessful;
@@ -303,60 +315,65 @@
} catch (Exception e) {
repLog.warn(
String.format(
- "Failed to apply object %s on project %s:%s, falling back to git fetch",
- objectId.name(), project, refName),
+ "Failed to batch apply object %s on project %s, falling back to git fetch",
+ refs.stream()
+ .map(event -> String.format("%s:%s", event.refName(), event.objectId()))
+ .collect(Collectors.joining(",")),
+ project),
e);
callSuccessful = false;
}
if (!callSuccessful) {
- callFetch(source, project, refName, state);
+ callFetch(source, project, refs, state);
}
};
}
private CallFunction getCallFunction(
NameKey project,
- ObjectId objectId,
- String refName,
+ List<ReferenceUpdatedEvent> refs,
long eventCreatedOn,
- boolean isDelete,
ReplicationState state) {
- if (isDelete) {
- return ((source) ->
- callSendObject(source, project, refName, eventCreatedOn, isDelete, null, state));
- }
try {
- Optional<RevisionData> revisionData =
- revReaderProvider.get().read(project, objectId, refName, 0);
- repLog.info(
- "RevisionData is {} for {}:{}",
- revisionData.map(RevisionData::toString).orElse("ABSENT"),
- project,
- refName);
+ List<BatchApplyObjectData> refsBatch =
+ refs.stream()
+ .map(ref -> toBatchApplyObject(project, ref, state))
+ .collect(Collectors.toList());
- if (revisionData.isPresent()) {
- return ((source) ->
- callSendObject(
- source,
- project,
- refName,
- eventCreatedOn,
- isDelete,
- Arrays.asList(revisionData.get()),
- state));
+ if (!containsLargeRef(refsBatch)) {
+ return ((source) -> callBatchSendObject(source, project, refsBatch, eventCreatedOn, state));
}
- } catch (InvalidObjectIdException | IOException e) {
+ } catch (UncheckedIOException e) {
+ stateLog.error("Falling back to calling fetch", e, state);
+ }
+ return ((source) -> callFetch(source, project, refs, state));
+ }
+
+ private BatchApplyObjectData toBatchApplyObject(
+ NameKey project, ReferenceUpdatedEvent event, ReplicationState state) {
+ if (event.isDelete()) {
+ Optional<RevisionData> noRevisionData = Optional.empty();
+ return BatchApplyObjectData.create(event.refName(), noRevisionData, event.isDelete());
+ }
+ try {
+ Optional<RevisionData> maybeRevisionData =
+ revReaderProvider.get().read(project, event.objectId(), event.refName(), 0);
+ return BatchApplyObjectData.create(event.refName(), maybeRevisionData, event.isDelete());
+ } catch (IOException e) {
stateLog.error(
String.format(
"Exception during reading ref: %s, project:%s, message: %s",
- refName, project.get(), e.getMessage()),
+ event.refName(), project.get(), e.getMessage()),
e,
state);
+ throw new UncheckedIOException(e);
}
+ }
- return (source) -> callFetch(source, project, refName, state);
+ private boolean containsLargeRef(List<BatchApplyObjectData> batchApplyObjectData) {
+ return batchApplyObjectData.stream().anyMatch(e -> e.revisionData().isEmpty() && !e.isDelete());
}
private boolean callSendObject(
@@ -460,6 +477,126 @@
return resultIsSuccessful;
}
+ private boolean callBatchSendObject(
+ Source source,
+ NameKey project,
+ List<BatchApplyObjectData> refsBatch,
+ long eventCreatedOn,
+ ReplicationState state)
+ throws MissingParentObjectException {
+ boolean batchResultSuccessful = true;
+
+ List<BatchApplyObjectData> filteredRefsBatch =
+ refsBatch.stream()
+ .filter(r -> source.wouldFetchProject(project) && source.wouldFetchRef(r.refName()))
+ .collect(Collectors.toList());
+
+ String batchApplyObjectStr =
+ filteredRefsBatch.stream()
+ .map(BatchApplyObjectData::toString)
+ .collect(Collectors.joining(","));
+ FetchApiClient fetchClient = fetchClientFactory.create(source);
+
+ for (String apiUrl : source.getApis()) {
+ try {
+ URIish uri = new URIish(apiUrl);
+ repLog.info(
+ "Pull replication REST API batch apply object to {} for {}:[{}]",
+ apiUrl,
+ project,
+ batchApplyObjectStr);
+ Context<String> apiTimer = applyObjectMetrics.startEnd2End(source.getRemoteConfigName());
+ HttpResult result =
+ fetchClient.callBatchSendObject(project, filteredRefsBatch, eventCreatedOn, uri);
+ boolean resultSuccessful = result.isSuccessful();
+ repLog.info(
+ "Pull replication REST API batch apply object to {} COMPLETED for {}:[{}], HTTP Result:"
+ + " {} - time:{} ms",
+ apiUrl,
+ project,
+ batchApplyObjectStr,
+ result,
+ apiTimer.stop() / 1000000.0);
+
+ if (!resultSuccessful
+ && result.isProjectMissing(project)
+ && source.isCreateMissingRepositories()) {
+ result = initProject(project, uri, fetchClient, result);
+ repLog.info("Missing project {} created, HTTP Result:{}", project, result);
+ }
+
+ if (!resultSuccessful && result.isParentObjectMissing()) {
+ resultSuccessful = true;
+ for (BatchApplyObjectData batchApplyObject : filteredRefsBatch) {
+ String refName = batchApplyObject.refName();
+ if ((RefNames.isNoteDbMetaRef(refName) || applyObjectsRefsFilter.match(refName))
+ && batchApplyObject.revisionData().isPresent()) {
+
+ Optional<RevisionData> maybeRevisionData = batchApplyObject.revisionData();
+ List<RevisionData> allRevisions =
+ fetchWholeMetaHistory(project, refName, maybeRevisionData.get());
+
+ resultSuccessful &=
+ callSendObject(
+ source,
+ project,
+ refName,
+ eventCreatedOn,
+ batchApplyObject.isDelete(),
+ allRevisions,
+ state);
+ } else {
+ throw new MissingParentObjectException(
+ project, refName, source.getRemoteConfigName());
+ }
+ }
+ }
+
+ if (!resultSuccessful && !result.isSendBatchObjectAvailable()) {
+ resultSuccessful = true;
+ for (BatchApplyObjectData batchApplyObjectData : filteredRefsBatch) {
+ resultSuccessful &=
+ callSendObject(
+ source,
+ project,
+ batchApplyObjectData.refName(),
+ eventCreatedOn,
+ batchApplyObjectData.isDelete(),
+ batchApplyObjectData.revisionData().map(ImmutableList::of).orElse(null),
+ state);
+ }
+ }
+
+ batchResultSuccessful &= resultSuccessful;
+ } catch (URISyntaxException e) {
+ repLog.warn(
+ "Pull replication REST API batch apply object to {} *FAILED* for {}:[{}]",
+ apiUrl,
+ project,
+ batchApplyObjectStr,
+ e);
+ stateLog.error(String.format("Cannot parse pull replication api url:%s", apiUrl), state);
+ batchResultSuccessful = false;
+ } catch (IOException | IllegalArgumentException e) {
+ repLog.warn(
+ "Pull replication REST API batch apply object to {} *FAILED* for {}:[{}]",
+ apiUrl,
+ project,
+ batchApplyObjectStr,
+ e);
+ stateLog.error(
+ String.format(
+ "Exception during the pull replication fetch rest api call. Endpoint url:%s,"
+ + " message:%s",
+ apiUrl, e.getMessage()),
+ e,
+ state);
+ batchResultSuccessful = false;
+ }
+ }
+ return batchResultSuccessful;
+ }
+
private List<RevisionData> fetchWholeMetaHistory(
NameKey project, String refName, RevisionData revision)
throws RepositoryNotFoundException, MissingObjectException, IncorrectObjectTypeException,
@@ -483,52 +620,60 @@
}
private boolean callFetch(
- Source source, Project.NameKey project, String refName, ReplicationState state) {
+ Source source,
+ Project.NameKey project,
+ List<ReferenceUpdatedEvent> refs,
+ ReplicationState state) {
boolean resultIsSuccessful = true;
- if (source.wouldFetchProject(project) && source.wouldFetchRef(refName)) {
- for (String apiUrl : source.getApis()) {
- try {
- URIish uri = new URIish(apiUrl);
- FetchApiClient fetchClient = fetchClientFactory.create(source);
- repLog.info("Pull replication REST API fetch to {} for {}:{}", apiUrl, project, refName);
- Context<String> timer = fetchMetrics.startEnd2End(source.getRemoteConfigName());
- HttpResult result = fetchClient.callFetch(project, refName, uri);
- long elapsedMs = TimeUnit.NANOSECONDS.toMillis(timer.stop());
- boolean resultSuccessful = result.isSuccessful();
- repLog.info(
- "Pull replication REST API fetch to {} COMPLETED for {}:{}, HTTP Result:"
- + " {} - time:{} ms",
- apiUrl,
- project,
- refName,
- result,
- elapsedMs);
- if (!resultSuccessful
- && result.isProjectMissing(project)
- && source.isCreateMissingRepositories()) {
- result = initProject(project, uri, fetchClient, result);
- }
- if (!resultSuccessful) {
- stateLog.warn(
- String.format(
- "Pull replication rest api fetch call failed. Endpoint url: %s, reason:%s",
- apiUrl, result.getMessage().orElse("unknown")),
- state);
- }
+ for (ReferenceUpdatedEvent refEvent : refs) {
+ String refName = refEvent.refName();
+ if (source.wouldFetchProject(project) && source.wouldFetchRef(refName)) {
+ for (String apiUrl : source.getApis()) {
+ try {
+ URIish uri = new URIish(apiUrl);
+ FetchApiClient fetchClient = fetchClientFactory.create(source);
+ repLog.info(
+ "Pull replication REST API fetch to {} for {}:{}", apiUrl, project, refName);
+ Context<String> timer = fetchMetrics.startEnd2End(source.getRemoteConfigName());
+ HttpResult result = fetchClient.callFetch(project, refName, uri);
+ long elapsedMs = TimeUnit.NANOSECONDS.toMillis(timer.stop());
+ boolean resultSuccessful = result.isSuccessful();
+ repLog.info(
+ "Pull replication REST API fetch to {} COMPLETED for {}:{}, HTTP Result:"
+ + " {} - time:{} ms",
+ apiUrl,
+ project,
+ refName,
+ result,
+ elapsedMs);
+ if (!resultSuccessful
+ && result.isProjectMissing(project)
+ && source.isCreateMissingRepositories()) {
+ result = initProject(project, uri, fetchClient, result);
+ }
+ if (!resultSuccessful) {
+ stateLog.warn(
+ String.format(
+ "Pull replication rest api fetch call failed. Endpoint url: %s, reason:%s",
+ apiUrl, result.getMessage().orElse("unknown")),
+ state);
+ }
- resultIsSuccessful &= result.isSuccessful();
- } catch (URISyntaxException e) {
- stateLog.error(String.format("Cannot parse pull replication api url:%s", apiUrl), state);
- resultIsSuccessful = false;
- } catch (Exception e) {
- stateLog.error(
- String.format(
- "Exception during the pull replication fetch rest api call. Endpoint url:%s,"
- + " message:%s",
- apiUrl, e.getMessage()),
- e,
- state);
- resultIsSuccessful = false;
+ resultIsSuccessful &= result.isSuccessful();
+ } catch (URISyntaxException e) {
+ stateLog.error(
+ String.format("Cannot parse pull replication api url:%s", apiUrl), state);
+ resultIsSuccessful = false;
+ } catch (Exception e) {
+ stateLog.error(
+ String.format(
+ "Exception during the pull replication fetch rest api call. Endpoint url:%s,"
+ + " message:%s",
+ apiUrl, e.getMessage()),
+ e,
+ state);
+ resultIsSuccessful = false;
+ }
}
}
}
@@ -555,8 +700,14 @@
private void fireBeforeStartupEvents() {
Set<String> eventsReplayed = new HashSet<>();
- for (ReferenceUpdatedEvent event : beforeStartupEventsQueue) {
- String eventKey = String.format("%s:%s", event.projectName(), event.refName());
+ for (ReferenceBatchUpdatedEvent event : beforeStartupEventsQueue) {
+ String eventKey =
+ String.format(
+ "%s:%s",
+ event.projectName(),
+ event.refs().stream()
+ .map(ReferenceUpdatedEvent::refName)
+ .collect(Collectors.joining()));
if (!eventsReplayed.contains(eventKey)) {
repLog.info("Firing pending task {}", event);
fire(event);
@@ -577,6 +728,22 @@
}
@AutoValue
+ abstract static class ReferenceBatchUpdatedEvent {
+
+ static ReferenceBatchUpdatedEvent create(
+ String projectName, List<ReferenceUpdatedEvent> refs, long eventCreatedOn) {
+ return new AutoValue_ReplicationQueue_ReferenceBatchUpdatedEvent(
+ projectName, refs, eventCreatedOn);
+ }
+
+ public abstract String projectName();
+
+ public abstract List<ReferenceUpdatedEvent> refs();
+
+ public abstract long eventCreatedOn();
+ }
+
+ @AutoValue
abstract static class ReferenceUpdatedEvent {
static ReferenceUpdatedEvent create(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchApplyObjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchApplyObjectAction.java
new file mode 100644
index 0000000..c67ceec
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchApplyObjectAction.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2023 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.replication.pull.api;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+class BatchApplyObjectAction implements RestModifyView<ProjectResource, List<RevisionInput>> {
+
+ private final ApplyObjectAction applyObjectAction;
+
+ @Inject
+ BatchApplyObjectAction(ApplyObjectAction applyObjectAction) {
+ this.applyObjectAction = applyObjectAction;
+ }
+
+ @Override
+ public Response<?> apply(ProjectResource resource, List<RevisionInput> inputs)
+ throws RestApiException {
+
+ List<Response<?>> allResponses = new ArrayList<>();
+ for (RevisionInput input : inputs) {
+ Response<?> individualResponse = applyObjectAction.apply(resource, input);
+ allResponses.add(individualResponse);
+ }
+
+ return Response.ok(allResponses);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
index be71946..b6b8fd0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
@@ -125,6 +125,7 @@
return (requestURI.contains(pluginName)
&& (requestURI.endsWith(String.format("/%s~apply-object", pluginName))
|| requestURI.endsWith(String.format("/%s~apply-objects", pluginName))
+ || requestURI.endsWith(String.format("/%s~batch-apply-object", pluginName))
|| requestURI.endsWith(String.format("/%s~fetch", pluginName))
|| requestURI.endsWith(String.format("/%s~delete-project", pluginName))
|| requestURI.contains(String.format("/%s/init-project/", pluginName))))
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/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java
new file mode 100644
index 0000000..7a613c0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 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.replication.pull.api.data;
+
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+
+@AutoValue
+public abstract class BatchApplyObjectData {
+
+ public static BatchApplyObjectData create(
+ String refName, Optional<RevisionData> revisionData, boolean isDelete)
+ throws IllegalArgumentException {
+ if (isDelete && revisionData.isPresent()) {
+ throw new IllegalArgumentException(
+ "DELETE ref-updates cannot be associated with a RevisionData");
+ }
+ return new AutoValue_BatchApplyObjectData(refName, revisionData, isDelete);
+ }
+
+ public abstract String refName();
+
+ public abstract Optional<RevisionData> revisionData();
+
+ public abstract boolean isDelete();
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s:%s isDelete=%s",
+ refName(), revisionData().map(RevisionData::toString).orElse("ABSENT"), isDelete());
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
index 1b3dc43..5f39811 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
@@ -19,6 +19,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.googlesource.gerrit.plugins.replication.pull.Source;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import java.io.IOException;
import java.util.List;
@@ -54,6 +55,13 @@
URIish targetUri)
throws IOException;
+ HttpResult callBatchSendObject(
+ NameKey project,
+ List<BatchApplyObjectData> batchApplyObjects,
+ long eventCreatedOn,
+ URIish targetUri)
+ throws IOException;
+
HttpResult callSendObjects(
NameKey project,
String refName,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
index 09148fe..2b9ef09 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -36,6 +36,7 @@
import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
import com.googlesource.gerrit.plugins.replication.pull.Source;
import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
@@ -44,6 +45,7 @@
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
@@ -196,6 +198,32 @@
}
@Override
+ public HttpResult callBatchSendObject(
+ NameKey project,
+ List<BatchApplyObjectData> batchedRefs,
+ long eventCreatedOn,
+ URIish targetUri)
+ throws IOException {
+ List<RevisionInput> inputs =
+ batchedRefs.stream()
+ .map(
+ batchApplyObject ->
+ new RevisionInput(
+ instanceId,
+ batchApplyObject.refName(),
+ eventCreatedOn,
+ batchApplyObject.revisionData().orElse(null)))
+ .collect(Collectors.toList());
+
+ String url = formatUrl(targetUri.toString(), project, "batch-apply-object");
+
+ HttpPost post = new HttpPost(url);
+ post.setEntity(new StringEntity(GSON.toJson(inputs)));
+ post.addHeader(new BasicHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()));
+ return executeRequest(post, bearerTokenProvider.get(), targetUri);
+ }
+
+ @Override
public HttpResult callSendObjects(
NameKey project,
String refName,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java
index ec9d65f..6428ece 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.replication.pull.client;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.gerrit.entities.Project;
import java.util.Optional;
@@ -51,4 +52,8 @@
? "OK"
: "FAILED" + ", status=" + responseCode + message.map(s -> " '" + s + "'").orElse("");
}
+
+ public boolean isSendBatchObjectAvailable() {
+ return responseCode != SC_NOT_FOUND;
+ }
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
index 4f853a2..7131513 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
@@ -22,7 +22,6 @@
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -49,6 +48,7 @@
import com.google.inject.Provider;
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
@@ -57,6 +57,7 @@
import com.googlesource.gerrit.plugins.replication.pull.filter.ApplyObjectsRefsFilter;
import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
@@ -66,13 +67,13 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
-import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@@ -103,6 +104,7 @@
@Mock RevisionData revisionDataWithParents;
List<ObjectId> revisionDataParentObjectIds;
@Mock HttpResult httpResult;
+ @Mock HttpResult batchHttpResult;
@Mock ApplyObjectsRefsFilter applyObjectsRefsFilter;
@Mock Config config;
@@ -112,6 +114,7 @@
@Captor ArgumentCaptor<String> stringCaptor;
@Captor ArgumentCaptor<Project.NameKey> projectNameKeyCaptor;
@Captor ArgumentCaptor<List<RevisionData>> revisionsDataCaptor;
+ @Captor ArgumentCaptor<List<BatchApplyObjectData>> batchRefsCaptor;
private ExcludedRefsFilter refsFilter;
private ReplicationQueue objectUnderTest;
@@ -159,12 +162,17 @@
lenient()
.when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any()))
.thenReturn(httpResult);
+ lenient()
+ .when(fetchRestApiClient.callBatchSendObject(any(), any(), anyLong(), any()))
+ .thenReturn(batchHttpResult);
when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(fetchHttpResult);
when(fetchRestApiClient.initProject(any(), any())).thenReturn(successfulHttpResult);
when(successfulHttpResult.isSuccessful()).thenReturn(true);
when(httpResult.isSuccessful()).thenReturn(true);
+ when(batchHttpResult.isSuccessful()).thenReturn(true);
when(fetchHttpResult.isSuccessful()).thenReturn(true);
when(httpResult.isProjectMissing(any())).thenReturn(false);
+ when(batchHttpResult.isProjectMissing(any())).thenReturn(false);
when(applyObjectsRefsFilter.match(any())).thenReturn(false);
applyObjectMetrics = new ApplyObjectMetrics("pull-replication", new DisabledMetricMaker());
@@ -187,12 +195,12 @@
}
@Test
- public void shouldCallSendObjectWhenMetaRef() throws IOException {
+ public void shouldCallBatchSendObjectWhenMetaRef() throws IOException {
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/meta");
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient).callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient).callBatchSendObject(any(), any(), anyLong(), any());
}
@Test
@@ -219,7 +227,7 @@
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient).callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient).callBatchSendObject(any(), any(), anyLong(), any());
}
@Test
@@ -229,15 +237,14 @@
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient, never())
- .callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient, never()).callBatchSendObject(any(), any(), anyLong(), any());
}
@Test
public void shouldCallInitProjectWhenProjectIsMissing() throws IOException {
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/meta");
- when(httpResult.isSuccessful()).thenReturn(false);
- when(httpResult.isProjectMissing(any())).thenReturn(true);
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isProjectMissing(any())).thenReturn(true);
when(source.isCreateMissingRepositories()).thenReturn(true);
objectUnderTest.start();
@@ -265,8 +272,10 @@
@Test
public void shouldNotCallInitProjectWhenReplicateNewRepositoriesNotSet() throws IOException {
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/meta");
- when(httpResult.isSuccessful()).thenReturn(false);
- when(httpResult.isProjectMissing(any())).thenReturn(true);
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isProjectMissing(any())).thenReturn(true);
+ lenient().when(httpResult.isSuccessful()).thenReturn(false);
+ lenient().when(httpResult.isProjectMissing(any())).thenReturn(true);
when(source.isCreateMissingRepositories()).thenReturn(false);
objectUnderTest.start();
@@ -276,7 +285,7 @@
}
@Test
- public void shouldCallSendObjectWhenPatchSetRefAndRefUpdateEvent() throws IOException {
+ public void shouldCallBatchSendObjectWhenPatchSetRefAndRefUpdateEvent() throws IOException {
when(config.getBoolean("event", "stream-events", "enableBatchRefUpdatedEvents", false))
.thenReturn(false);
@@ -299,16 +308,16 @@
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient).callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient).callBatchSendObject(any(), any(), anyLong(), any());
}
@Test
- public void shouldCallSendObjectWhenPatchSetRef() throws IOException {
+ public void shouldCallBatchSendObjectWhenPatchSetRef() throws IOException {
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/1");
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient).callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient).callBatchSendObject(any(), any(), anyLong(), any());
}
@Test
@@ -338,14 +347,49 @@
}
@Test
- public void shouldFallbackToCallFetchWhenParentObjectIsMissing() throws IOException {
+ public void
+ shouldFallbackToCallFetchWhenParentObjectIsMissingAndRefDoesntMatchApplyObjectsRefsFilter()
+ throws IOException {
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/1");
objectUnderTest.start();
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(true);
+
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient).callFetch(any(), anyString(), any());
+ }
+
+ @Test
+ public void
+ shouldFallbackToApplyObjectsForEachRefWhenParentObjectIsMissingAndRefMatchesApplyObjectsRefFilter()
+ throws IOException {
+ Event event = generateBatchRefUpdateEvent("refs/changes/01/1/1", "refs/changes/02/1/1");
+ objectUnderTest.start();
+
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(true);
+ when(applyObjectsRefsFilter.match(any())).thenReturn(true);
+
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient, times(2))
+ .callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient, never()).callFetch(any(), anyString(), any());
+ }
+
+ @Test
+ public void shouldFallbackToCallFetchWhenSendBatchObjectNotAvailableAndApplyObjectFails()
+ throws IOException {
+ Event event = generateBatchRefUpdateEvent("refs/changes/01/1/1");
+ objectUnderTest.start();
+
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(false);
+ when(batchHttpResult.isSendBatchObjectAvailable()).thenReturn(false);
when(httpResult.isSuccessful()).thenReturn(false);
- when(httpResult.isParentObjectMissing()).thenReturn(true);
- when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any()))
- .thenReturn(httpResult);
+ when(httpResult.isParentObjectMissing()).thenReturn(false);
objectUnderTest.onEvent(event);
@@ -358,10 +402,56 @@
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/meta");
objectUnderTest.start();
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(true);
+
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient, times(1))
+ .callSendObjects(any(), anyString(), anyLong(), revisionsDataCaptor.capture(), any());
+ List<List<RevisionData>> revisionsDataValues = revisionsDataCaptor.getAllValues();
+ assertThat(revisionsDataValues).hasSize(1);
+
+ List<RevisionData> firstRevisionsValues = revisionsDataValues.get(0);
+ assertThat(firstRevisionsValues).hasSize(1 + revisionDataParentObjectIds.size());
+ assertThat(firstRevisionsValues).contains(revisionData);
+ }
+
+ @Test
+ public void shouldFallbackToApplyAllParentObjectsWhenParentObjectIsMissingOnAllowedRefs()
+ throws IOException {
+ String refName = "refs/tags/test-tag";
+ Event event = generateBatchRefUpdateEvent(refName);
+ objectUnderTest.start();
+
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(true);
+ when(applyObjectsRefsFilter.match(refName)).thenReturn(true);
+
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient, times(1))
+ .callSendObjects(any(), anyString(), anyLong(), revisionsDataCaptor.capture(), any());
+ List<List<RevisionData>> revisionsDataValues = revisionsDataCaptor.getAllValues();
+ assertThat(revisionsDataValues).hasSize(1);
+
+ List<RevisionData> firstRevisionsValues = revisionsDataValues.get(0);
+ assertThat(firstRevisionsValues).hasSize(1 + revisionDataParentObjectIds.size());
+ assertThat(firstRevisionsValues).contains(revisionData);
+ }
+
+ @Test
+ public void
+ shouldFallbackToApplyAllParentObjectsWhenSendBatchObjectNotAvailableAndParentObjectIsMissingOnMetaRef()
+ throws IOException {
+ Event event = generateBatchRefUpdateEvent("refs/changes/01/1/meta");
+ objectUnderTest.start();
+
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(false);
+ when(batchHttpResult.isSendBatchObjectAvailable()).thenReturn(false);
when(httpResult.isSuccessful()).thenReturn(false, true);
when(httpResult.isParentObjectMissing()).thenReturn(true, false);
- when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any()))
- .thenReturn(httpResult);
objectUnderTest.onEvent(event);
@@ -379,16 +469,18 @@
}
@Test
- public void shouldFallbackToApplyAllParentObjectsWhenParentObjectIsMissingOnAllowedRefs()
- throws IOException {
+ public void
+ shouldFallbackToApplyAllParentObjectsWhenSendBatchObjectNotAvailableAndParentObjectIsMissingOnAllowedRefs()
+ throws IOException {
String refName = "refs/tags/test-tag";
Event event = generateBatchRefUpdateEvent(refName);
objectUnderTest.start();
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(false);
+ when(batchHttpResult.isSendBatchObjectAvailable()).thenReturn(false);
when(httpResult.isSuccessful()).thenReturn(false, true);
when(httpResult.isParentObjectMissing()).thenReturn(true, false);
- when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any()))
- .thenReturn(httpResult);
when(applyObjectsRefsFilter.match(refName)).thenReturn(true);
objectUnderTest.onEvent(event);
@@ -407,6 +499,27 @@
}
@Test
+ public void shouldCallBatchFetchForAllTheRefsInTheBatchIfApplyObjectFails()
+ throws IOException, URISyntaxException {
+ Event event = generateBatchRefUpdateEvent("refs/changes/01/1/1", "refs/changes/02/1/1");
+ when(batchHttpResult.isSuccessful()).thenReturn(false);
+ when(batchHttpResult.isParentObjectMissing()).thenReturn(true);
+ when(applyObjectsRefsFilter.match(any())).thenReturn(true, true);
+ when(httpResult.isSuccessful()).thenReturn(true, false);
+ when(httpResult.isParentObjectMissing()).thenReturn(true);
+
+ objectUnderTest.start();
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient, times(2))
+ .callSendObjects(any(), anyString(), anyLong(), any(), any());
+ verify(fetchRestApiClient)
+ .callFetch(PROJECT, "refs/changes/01/1/1", new URIish("http://localhost:18080"));
+ verify(fetchRestApiClient)
+ .callFetch(PROJECT, "refs/changes/02/1/1", new URIish("http://localhost:18080"));
+ }
+
+ @Test
public void shouldSkipEventWhenMultiSiteVersionRef() throws IOException {
FileBasedConfig fileConfig =
new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
@@ -525,14 +638,12 @@
}
private void verifySendObjectOrdering(String firstRef, String secondRef) throws IOException {
- InOrder inOrder = inOrder(fetchRestApiClient);
+ verify(fetchRestApiClient)
+ .callBatchSendObject(any(), batchRefsCaptor.capture(), anyLong(), any());
+ List<BatchApplyObjectData> batchRefs = batchRefsCaptor.getValue();
- inOrder
- .verify(fetchRestApiClient)
- .callSendObjects(any(), eq(firstRef), anyLong(), any(), any());
- inOrder
- .verify(fetchRestApiClient)
- .callSendObjects(any(), eq(secondRef), anyLong(), any(), any());
+ assertThat(batchRefs.get(0).refName()).isEqualTo(firstRef);
+ assertThat(batchRefs.get(1).refName()).isEqualTo(secondRef);
}
private class TestEvent extends RefUpdatedEvent {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchApplyObjectActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchApplyObjectActionTest.java
new file mode 100644
index 0000000..d8dabf1
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchApplyObjectActionTest.java
@@ -0,0 +1,214 @@
+// Copyright (C) 2023 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.replication.pull.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
+import java.util.Collections;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BatchApplyObjectActionTest {
+
+ private static final long DUMMY_EVENT_TIMESTAMP = 1684875939;
+
+ private BatchApplyObjectAction batchApplyObjectAction;
+ private static final String LABEL = "instance-2-label";
+ private static final String REF_NAME = "refs/heads/master";
+ private static final String REF_META_NAME = "refs/meta/version";
+ private static final String SAMPLE_COMMIT_OBJECT_ID = "9f8d52853089a3cf00c02ff7bd0817bd4353a95a";
+ private static final String SAMPLE_TREE_OBJECT_ID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
+
+ private static final String SAMPLE_COMMIT_CONTENT =
+ "tree "
+ + SAMPLE_TREE_OBJECT_ID
+ + "\n"
+ + "parent 20eb48d28be86dc88fb4bef747f08de0fbefe12d\n"
+ + "author Gerrit User 1000000 <1000000@69ec38f0-350e-4d9c-96d4-bc956f2faaac> 1610471611 +0100\n"
+ + "committer Gerrit Code Review <root@maczech-XPS-15> 1610471611 +0100\n"
+ + "\n"
+ + "Update patch set 1\n"
+ + "\n"
+ + "Change has been successfully merged by Administrator\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Status: merged\n"
+ + "Tag: autogenerated:gerrit:merged\n"
+ + "Reviewer: Gerrit User 1000000 <1000000@69ec38f0-350e-4d9c-96d4-bc956f2faaac>\n"
+ + "Label: SUBM=+1\n"
+ + "Submission-id: 1904-1610471611558-783c0a2f\n"
+ + "Submitted-with: OK\n"
+ + "Submitted-with: OK: Code-Review: Gerrit User 1000000 <1000000@69ec38f0-350e-4d9c-96d4-bc956f2faaac>";
+
+ @Mock private ApplyObjectAction applyObjectAction;
+ @Mock private ProjectResource projectResource;
+
+ @Before
+ public void setup() {
+ batchApplyObjectAction = new BatchApplyObjectAction(applyObjectAction);
+ }
+
+ @Test
+ public void shouldDelegateToApplyObjectActionForEveryRevision() throws RestApiException {
+ RevisionInput first =
+ new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ RevisionInput second =
+ new RevisionInput(LABEL, "foo", DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+
+ batchApplyObjectAction.apply(projectResource, List.of(first, second));
+
+ verify(applyObjectAction).apply(projectResource, first);
+ verify(applyObjectAction).apply(projectResource, second);
+ }
+
+ @Test
+ public void shouldReturnOkResponseCodeWhenAllRevisionsAreProcessedSuccessfully()
+ throws RestApiException {
+ RevisionInput first =
+ new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ RevisionInput second =
+ new RevisionInput(LABEL, "foo", DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+
+ when(applyObjectAction.apply(projectResource, first))
+ .thenAnswer((Answer<Response<?>>) invocation -> Response.created(first));
+ when(applyObjectAction.apply(projectResource, second))
+ .thenAnswer((Answer<Response<?>>) invocation -> Response.created(second));
+
+ Response<?> response = batchApplyObjectAction.apply(projectResource, List.of(first, second));
+
+ assertThat(response.statusCode()).isEqualTo(SC_OK);
+ }
+
+ @Test
+ public void shouldReturnAListWithAllTheRevisionsInResponseBodyOnSuccess()
+ throws RestApiException {
+ RevisionInput first =
+ new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ RevisionInput second =
+ new RevisionInput(LABEL, "foo", DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ Response<?> firstResponse = Response.created(first);
+ Response<?> secondResponse = Response.created(second);
+
+ when(applyObjectAction.apply(projectResource, first))
+ .thenAnswer((Answer<Response<?>>) invocation -> firstResponse);
+ when(applyObjectAction.apply(projectResource, second))
+ .thenAnswer((Answer<Response<?>>) invocation -> secondResponse);
+
+ Response<?> response = batchApplyObjectAction.apply(projectResource, List.of(first, second));
+
+ assertThat((List<Response<?>>) response.value())
+ .isEqualTo(List.of(firstResponse, secondResponse));
+ }
+
+ @Test
+ public void shouldAcceptAMixOfCreatesAndDeletes() throws RestApiException {
+ RevisionInput delete = new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, null);
+ RevisionInput create =
+ new RevisionInput(LABEL, "foo", DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ Response<?> deleteResponse = Response.withStatusCode(HttpServletResponse.SC_NO_CONTENT, "");
+ Response<?> createResponse = Response.created(create);
+
+ when(applyObjectAction.apply(projectResource, delete))
+ .thenAnswer((Answer<Response<?>>) invocation -> deleteResponse);
+ when(applyObjectAction.apply(projectResource, create))
+ .thenAnswer((Answer<Response<?>>) invocation -> createResponse);
+
+ Response<?> response = batchApplyObjectAction.apply(projectResource, List.of(delete, create));
+
+ assertThat((List<Response<?>>) response.value())
+ .isEqualTo(List.of(deleteResponse, createResponse));
+ }
+
+ @Test
+ public void shouldReturnOneOkCodeEvenIfInputContainsBothCreatesAndDeletes()
+ throws RestApiException {
+ RevisionInput create =
+ new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ RevisionInput delete = new RevisionInput(LABEL, REF_META_NAME, DUMMY_EVENT_TIMESTAMP + 1, null);
+
+ List<RevisionInput> inputs = List.of(create, delete);
+
+ Response<?> response = batchApplyObjectAction.apply(projectResource, inputs);
+
+ assertThat(response.statusCode()).isEqualTo(SC_OK);
+ }
+
+ @Test(expected = RestApiException.class)
+ public void shouldThrowARestApiExceptionIfProcessingFailsForAnyOfTheRevisions()
+ throws RestApiException {
+ RevisionInput good =
+ new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ RevisionInput bad =
+ new RevisionInput(LABEL, "bad", DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+
+ when(applyObjectAction.apply(projectResource, good))
+ .thenAnswer((Answer<Response<?>>) invocation -> Response.created(good));
+ when(applyObjectAction.apply(projectResource, bad))
+ .thenThrow(new MergeConflictException("BOOM"));
+
+ batchApplyObjectAction.apply(projectResource, List.of(good, bad));
+ }
+
+ @Test
+ public void shouldStopProcessingWhenAFailureOccurs() throws RestApiException {
+ RevisionInput good =
+ new RevisionInput(LABEL, REF_NAME, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+ RevisionInput bad =
+ new RevisionInput(LABEL, "bad", DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
+
+ when(applyObjectAction.apply(projectResource, bad))
+ .thenThrow(new MergeConflictException("BOOM"));
+
+ try {
+ batchApplyObjectAction.apply(projectResource, List.of(bad, good));
+ } catch (MergeConflictException e) {
+ verify(applyObjectAction, never()).apply(projectResource, good);
+ }
+ }
+
+ private RevisionData createSampleRevisionData() {
+ RevisionObjectData commitData =
+ new RevisionObjectData(
+ SAMPLE_COMMIT_OBJECT_ID, Constants.OBJ_COMMIT, SAMPLE_COMMIT_CONTENT.getBytes());
+ RevisionObjectData treeData =
+ new RevisionObjectData(SAMPLE_TREE_OBJECT_ID, Constants.OBJ_TREE, new byte[] {});
+ return createSampleRevisionData(commitData, treeData);
+ }
+
+ private RevisionData createSampleRevisionData(
+ RevisionObjectData commitData, RevisionObjectData treeData) {
+ return new RevisionData(Collections.emptyList(), commitData, treeData, Lists.newArrayList());
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
index ca69f06..9b0106c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
@@ -91,6 +91,11 @@
}
@Test
+ public void shouldAuthenticateWhenBatchApplyObject() throws ServletException, IOException {
+ authenticateAndFilter("any-prefix/pull-replication~batch-apply-object", NO_QUERY_PARAMETERS);
+ }
+
+ @Test
public void shouldAuthenticateWhenDeleteProject() throws ServletException, IOException {
authenticateAndFilter("any-prefix/pull-replication~delete-project", NO_QUERY_PARAMETERS);
}
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());
+ }
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectDataTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectDataTest.java
new file mode 100644
index 0000000..bf74b56
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectDataTest.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 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.replication.pull.api.data;
+
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BatchApplyObjectDataTest {
+
+ @Mock private RevisionData revisionData;
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldFailIfRevisionDataIsPresentForADelete() {
+ BatchApplyObjectData.create("foo", Optional.of(revisionData), true);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
index 5de7e7d..e64bc6f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
@@ -30,6 +30,7 @@
import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
import com.googlesource.gerrit.plugins.replication.pull.Source;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
import com.googlesource.gerrit.plugins.replication.pull.filter.SyncRefsFilter;
@@ -37,7 +38,10 @@
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import org.apache.http.Header;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpPost;
@@ -430,19 +434,141 @@
assertAuthentication(httpPut);
}
+ @Test
+ public void shouldCallBatchSendObjectEndpoint() throws IOException, URISyntaxException {
+
+ List<BatchApplyObjectData> batchApplyObjects = new ArrayList<>();
+ batchApplyObjects.add(
+ BatchApplyObjectData.create(refName, Optional.of(createSampleRevisionData("a")), false));
+
+ objectUnderTest.callBatchSendObject(
+ Project.nameKey("test_repo"), batchApplyObjects, eventCreatedOn, new URIish(api));
+
+ verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
+
+ HttpPost httpPost = httpPostCaptor.getValue();
+ assertThat(httpPost.getURI().getHost()).isEqualTo("gerrit-host");
+ assertThat(httpPost.getURI().getPath())
+ .isEqualTo(
+ String.format(
+ "%s/projects/test_repo/pull-replication~batch-apply-object",
+ urlAuthenticationPrefix()));
+ assertAuthentication(httpPost);
+ }
+
+ @Test
+ public void shouldCallBatchApplyObjectEndpointWithAListOfRefsInPayload()
+ throws IOException, URISyntaxException {
+ List<BatchApplyObjectData> batchApplyObjects = new ArrayList<>();
+ RevisionData revisionA = createSampleRevisionData("a");
+ RevisionData revisionB = createSampleRevisionData("b");
+ String refNameB = "refs/heads/b";
+ batchApplyObjects.add(BatchApplyObjectData.create(refName, Optional.of(revisionA), false));
+ batchApplyObjects.add(BatchApplyObjectData.create(refNameB, Optional.of(revisionB), false));
+
+ objectUnderTest.callBatchSendObject(
+ Project.nameKey("test_repo"), batchApplyObjects, eventCreatedOn, new URIish(api));
+
+ verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
+
+ HttpPost httpPost = httpPostCaptor.getValue();
+
+ String expectedSendObjectsPayload =
+ "[{\"label\":\"Replication\",\"ref_name\":\""
+ + refName
+ + "\",\"event_created_on\":"
+ + eventCreatedOn
+ + ",\"revision_data\":{\"commit_object\":{\"sha1\":\""
+ + revisionA.getCommitObject().getSha1()
+ + "\",\"type\":1,\"content\":\"Y29tbWl0YWNvbnRlbnQ\\u003d\"},\"tree_object\":{\"sha1\":\""
+ + revisionA.getTreeObject().getSha1()
+ + "\",\"type\":2,\"content\":\"dHJlZWFjb250ZW50\"},\"blobs\":[{\"sha1\":\""
+ + revisionA.getBlobs().get(0).getSha1()
+ + "\",\"type\":3,\"content\":\"YmxvYmFjb250ZW50\"}]}},"
+ + "{\"label\":\"Replication\",\"ref_name\":\""
+ + refNameB
+ + "\",\"event_created_on\":"
+ + eventCreatedOn
+ + ",\"revision_data\":{\"commit_object\":{\"sha1\":\""
+ + revisionB.getCommitObject().getSha1()
+ + "\",\"type\":1,\"content\":\"Y29tbWl0YmNvbnRlbnQ\\u003d\"},\"tree_object\":{\"sha1\":\""
+ + revisionB.getTreeObject().getSha1()
+ + "\",\"type\":2,\"content\":\"dHJlZWJjb250ZW50\"},\"blobs\":[{\"sha1\":\""
+ + revisionB.getBlobs().get(0).getSha1()
+ + "\",\"type\":3,\"content\":\"YmxvYmJjb250ZW50\"}]}}]";
+ assertThat(readPayload(httpPost)).isEqualTo(expectedSendObjectsPayload);
+ }
+
+ @Test
+ public void shouldCallBatchApplyObjectEndpointWithNoRevisionDataForDeletes()
+ throws IOException, URISyntaxException {
+ List<BatchApplyObjectData> batchApplyObjects = new ArrayList<>();
+ batchApplyObjects.add(BatchApplyObjectData.create(refName, Optional.empty(), true));
+
+ objectUnderTest.callBatchSendObject(
+ Project.nameKey("test_repo"), batchApplyObjects, eventCreatedOn, new URIish(api));
+
+ verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
+
+ HttpPost httpPost = httpPostCaptor.getValue();
+
+ String expectedSendObjectsPayload =
+ "[{\"label\":\"Replication\",\"ref_name\":\""
+ + refName
+ + "\",\"event_created_on\":"
+ + eventCreatedOn
+ + "}]";
+ assertThat(readPayload(httpPost)).isEqualTo(expectedSendObjectsPayload);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldThrowExceptionIfDeleteFlagIsSetButRevisionDataIsPresentForBatchSendEndpoint()
+ throws IOException, URISyntaxException {
+ List<BatchApplyObjectData> batchApplyObjects = new ArrayList<>();
+ batchApplyObjects.add(
+ BatchApplyObjectData.create(refName, Optional.of(createSampleRevisionData()), true));
+
+ objectUnderTest.callBatchSendObject(
+ Project.nameKey("test_repo"), batchApplyObjects, eventCreatedOn, new URIish(api));
+ }
+
public String readPayload(HttpPost entity) throws UnsupportedOperationException, IOException {
ByteBuffer buf = IO.readWholeStream(entity.getEntity().getContent(), 1024);
return RawParseUtils.decode(buf.array(), buf.arrayOffset(), buf.limit()).trim();
}
- private RevisionData createSampleRevisionData() {
+ private RevisionData createSampleRevisionData(String prefix) {
+ String commitPrefix = "commit" + prefix;
+ String treePrefix = "tree" + prefix;
+ String blobPrefix = "blob" + prefix;
+ return createSampleRevisionData(
+ commitPrefix,
+ commitPrefix + "content",
+ treePrefix,
+ treePrefix + "content",
+ blobPrefix,
+ blobPrefix + "content");
+ }
+
+ private RevisionData createSampleRevisionData(
+ String commitObjectId,
+ String commitContent,
+ String treeObjectId,
+ String treeContent,
+ String blobObjectId,
+ String blobContent) {
RevisionObjectData commitData =
- new RevisionObjectData(commitObjectId, Constants.OBJ_COMMIT, commitObject.getBytes());
+ new RevisionObjectData(commitObjectId, Constants.OBJ_COMMIT, commitContent.getBytes());
RevisionObjectData treeData =
- new RevisionObjectData(treeObjectId, Constants.OBJ_TREE, treeObject.getBytes());
+ new RevisionObjectData(treeObjectId, Constants.OBJ_TREE, treeContent.getBytes());
RevisionObjectData blobData =
- new RevisionObjectData(blobObjectId, Constants.OBJ_BLOB, blobObject.getBytes());
+ new RevisionObjectData(blobObjectId, Constants.OBJ_BLOB, blobContent.getBytes());
return new RevisionData(
Collections.emptyList(), commitData, treeData, Lists.newArrayList(blobData));
}
+
+ private RevisionData createSampleRevisionData() {
+ return createSampleRevisionData(
+ commitObjectId, commitObject, treeObjectId, treeObject, blobObjectId, blobObject);
+ }
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResultTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResultTest.java
index d82f3a5..900723e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResultTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResultTest.java
@@ -30,21 +30,25 @@
public static Iterable<Object[]> data() {
return Arrays.asList(
new Object[][] {
- {HttpServletResponse.SC_OK, true},
- {HttpServletResponse.SC_CREATED, true},
- {HttpServletResponse.SC_ACCEPTED, true},
- {HttpServletResponse.SC_NO_CONTENT, true},
- {HttpServletResponse.SC_BAD_REQUEST, false},
- {HttpServletResponse.SC_CONFLICT, false}
+ {HttpServletResponse.SC_OK, true, true},
+ {HttpServletResponse.SC_CREATED, true, true},
+ {HttpServletResponse.SC_ACCEPTED, true, true},
+ {HttpServletResponse.SC_NO_CONTENT, true, true},
+ {HttpServletResponse.SC_BAD_REQUEST, false, true},
+ {HttpServletResponse.SC_NOT_FOUND, false, false},
+ {HttpServletResponse.SC_CONFLICT, false, true}
});
}
private Integer httpStatus;
private boolean isSuccessful;
+ private boolean isSendBatchObjectAvailable;
- public HttpResultTest(Integer httpStatus, Boolean isSuccessful) {
+ public HttpResultTest(
+ Integer httpStatus, Boolean isSuccessful, Boolean isSendBatchObjectAvailable) {
this.httpStatus = httpStatus;
this.isSuccessful = isSuccessful;
+ this.isSendBatchObjectAvailable = isSendBatchObjectAvailable;
}
@Test
@@ -52,4 +56,10 @@
HttpResult httpResult = new HttpResult(httpStatus, Optional.empty());
assertThat(httpResult.isSuccessful()).isEqualTo(isSuccessful);
}
+
+ @Test
+ public void httpResultIsSendBatchObjectAvailable() {
+ HttpResult httpResult = new HttpResult(httpStatus, Optional.empty());
+ assertThat(httpResult.isSendBatchObjectAvailable()).isEqualTo(isSendBatchObjectAvailable);
+ }
}