Merge branch 'stable-3.3' into stable-3.4
* stable-3.3:
Do not rely on System.nanoTime for E2E metrics
Introduce the apply-objects REST-API for the whole '/meta' chain
Fix the processing of an empty HTML response body from REST-API
Fix issue with ref deletion and global-refdb
Fix issue with fetching all refs after project creation
Always fallback to fetch when ApplyObject REST-API fails
Log the reason why a ref object wasn't loaded by RevisionReader
Consider any HTTP 2xx response code from REST-API as success
Return NO_CONTENT when removing a ref through ApplyObject
Introduce E2E fetch REST-API metrics
Fix ApplyObjectIT.shouldApplyRefMetaObject test for apply object
Introduce E2E apply object REST-API metrics
Add missing @Override to parseRemotes
Add more logging for the apply object REST-API
Support ApplyObject of non-commit refs
Change-Id: I721852cb091ad12c2fc94cb124b01968b0bad78d
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ApplyObjectMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ApplyObjectMetrics.java
index 78b6ddb..78745bb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ApplyObjectMetrics.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ApplyObjectMetrics.java
@@ -26,25 +26,33 @@
@Singleton
public class ApplyObjectMetrics {
private final Timer1<String> executionTime;
+ private final Timer1<String> end2EndTime;
@Inject
ApplyObjectMetrics(@PluginName String pluginName, MetricMaker metricMaker) {
- Field<String> SOURCE_FIELD =
+ Field<String> field =
Field.ofString(
- "source",
+ "pull_replication",
(metadataBuilder, fieldValue) ->
metadataBuilder
.pluginName(pluginName)
- .addPluginMetadata(PluginMetadata.create("source", fieldValue)))
+ .addPluginMetadata(PluginMetadata.create("pull_replication", fieldValue)))
.build();
-
executionTime =
metricMaker.newTimer(
"apply_object_latency",
new Description("Time spent applying object from remote source.")
.setCumulative()
.setUnit(Description.Units.MILLISECONDS),
- SOURCE_FIELD);
+ field);
+
+ end2EndTime =
+ metricMaker.newTimer(
+ "apply_object_end_2_end_latency",
+ new Description("Time spent for e2e replication with the apply object REST API")
+ .setCumulative()
+ .setUnit(Description.Units.MILLISECONDS),
+ field);
}
/**
@@ -56,4 +64,14 @@
public Timer1.Context<String> start(String name) {
return executionTime.start(name);
}
+
+ /**
+ * Start the replication latency timer from a source.
+ *
+ * @param name the source name.
+ * @return the timer context.
+ */
+ public Timer1.Context<String> startEnd2End(String name) {
+ return end2EndTime.start(name);
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java
index bcd86d7..42310ff 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java
@@ -21,6 +21,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlesource.gerrit.plugins.replication.ReplicationFilter;
+import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.transport.URIish;
@@ -91,7 +92,7 @@
for (Source cfg : sources.getAll()) {
if (cfg.wouldFetchProject(project)) {
for (URIish uri : cfg.getURIs(project, urlMatch)) {
- cfg.schedule(project, FetchOne.ALL_REFS, uri, state, replicationType);
+ cfg.schedule(project, FetchOne.ALL_REFS, uri, state, replicationType, Optional.empty());
}
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
index 9b2f905..4fad8f9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
import com.googlesource.gerrit.plugins.replication.pull.fetch.Fetch;
import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchFactory;
import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
@@ -69,7 +70,8 @@
static final String ID_KEY = "fetchOneId";
interface Factory {
- FetchOne create(Project.NameKey d, URIish u);
+ FetchOne create(
+ Project.NameKey d, URIish u, Optional<PullReplicationApiRequestMetrics> apiRequestMetrics);
}
private final GitRepositoryManager gitManager;
@@ -94,6 +96,7 @@
private final FetchReplicationMetrics metrics;
private final AtomicBoolean canceledWhileRunning;
private final FetchFactory fetchFactory;
+ private final Optional<PullReplicationApiRequestMetrics> apiRequestMetrics;
@Inject
FetchOne(
@@ -106,7 +109,8 @@
FetchReplicationMetrics m,
FetchFactory fetchFactory,
@Assisted Project.NameKey d,
- @Assisted URIish u) {
+ @Assisted URIish u,
+ @Assisted Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
gitManager = grm;
pool = s;
config = c.getRemoteConfig();
@@ -122,6 +126,7 @@
canceledWhileRunning = new AtomicBoolean(false);
this.fetchFactory = fetchFactory;
maxRetries = s.getMaxRetries();
+ this.apiRequestMetrics = apiRequestMetrics;
}
@Override
@@ -299,12 +304,17 @@
git = gitManager.openRepository(projectName);
runImpl();
long elapsed = NANOSECONDS.toMillis(context.stop());
+ Optional<Long> elapsedEnd2End =
+ apiRequestMetrics
+ .flatMap(metrics -> metrics.stop(config.getName()))
+ .map(NANOSECONDS::toMillis);
repLog.info(
- "Replication from {} completed in {}ms, {}ms delay, {} retries",
+ "Replication from {} completed in {}ms, {}ms delay, {} retries{}",
uri,
elapsed,
delay,
- retryCount);
+ retryCount,
+ elapsedEnd2End.map(el -> String.format(", E2E %dms", el)).orElse(""));
} catch (RepositoryNotFoundException e) {
stateLog.error(
"Cannot replicate " + projectName + "; Local repository error: " + e.getMessage(),
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchReplicationMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchReplicationMetrics.java
index e952252..22bb073 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchReplicationMetrics.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchReplicationMetrics.java
@@ -23,10 +23,12 @@
import com.google.gerrit.server.logging.PluginMetadata;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
@Singleton
public class FetchReplicationMetrics {
private final Timer1<String> executionTime;
+ private final Timer1<String> end2EndExecutionTime;
private final Histogram1<String> executionDelay;
private final Histogram1<String> executionRetries;
@@ -34,11 +36,11 @@
FetchReplicationMetrics(@PluginName String pluginName, MetricMaker metricMaker) {
Field<String> SOURCE_FIELD =
Field.ofString(
- "source",
+ "pull_replication",
(metadataBuilder, fieldValue) ->
metadataBuilder
.pluginName(pluginName)
- .addPluginMetadata(PluginMetadata.create("source", fieldValue)))
+ .addPluginMetadata(PluginMetadata.create("pull_replication", fieldValue)))
.build();
executionTime =
@@ -49,6 +51,14 @@
.setUnit(Description.Units.MILLISECONDS),
SOURCE_FIELD);
+ end2EndExecutionTime =
+ metricMaker.newTimer(
+ "replication_end_2_end_latency",
+ new Description("Time spent end-2-end fetching from remote source.")
+ .setCumulative()
+ .setUnit(Description.Units.MILLISECONDS),
+ SOURCE_FIELD);
+
executionDelay =
metricMaker.newHistogram(
"replication_delay",
@@ -77,6 +87,26 @@
}
/**
+ * Start the end-to-end replication latency timer from a source.
+ *
+ * @param name the source name.
+ * @return the timer context.
+ */
+ public Timer1.Context<String> startEnd2End(String name) {
+ return end2EndExecutionTime.start(name);
+ }
+
+ /**
+ * Record the end-to-end replication latency timer from a source.
+ *
+ * @param name the source name.
+ * @param metricNanos the timer value in nanos
+ */
+ public void recordEnd2End(String name, long metricNanos) {
+ end2EndExecutionTime.record(name, metricNanos, TimeUnit.NANOSECONDS);
+ }
+
+ /**
* Record the replication delay and retry metrics for a source.
*
* @param name the source name.
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 cfe01e2..f3e38fd 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
@@ -15,14 +15,17 @@
package com.googlesource.gerrit.plugins.replication.pull;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Queues;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.HeadUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.metrics.Timer1.Context;
import com.google.gerrit.server.events.EventDispatcher;
import com.google.gerrit.server.git.WorkQueue;
import com.google.inject.Inject;
@@ -36,7 +39,10 @@
import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
@@ -46,7 +52,11 @@
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.apache.http.client.ClientProtocolException;
+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.ObjectId;
import org.eclipse.jgit.transport.URIish;
import org.slf4j.Logger;
@@ -74,7 +84,9 @@
private FetchApiClient.Factory fetchClientFactory;
private Integer fetchCallsTimeout;
private ExcludedRefsFilter refsFilter;
- private RevisionReader revisionReader;
+ private Provider<RevisionReader> revReaderProvider;
+ private final ApplyObjectMetrics applyObjectMetrics;
+ private final FetchReplicationMetrics fetchMetrics;
@Inject
ReplicationQueue(
@@ -84,7 +96,9 @@
ReplicationStateListeners sl,
FetchApiClient.Factory fetchClientFactory,
ExcludedRefsFilter refsFilter,
- RevisionReader revReader) {
+ Provider<RevisionReader> revReaderProvider,
+ ApplyObjectMetrics applyObjectMetrics,
+ FetchReplicationMetrics fetchMetrics) {
workQueue = wq;
dispatcher = dis;
sources = rd;
@@ -92,7 +106,9 @@
beforeStartupEventsQueue = Queues.newConcurrentLinkedQueue();
this.fetchClientFactory = fetchClientFactory;
this.refsFilter = refsFilter;
- this.revisionReader = revReader;
+ this.revReaderProvider = revReaderProvider;
+ this.applyObjectMetrics = applyObjectMetrics;
+ this.fetchMetrics = fetchMetrics;
}
@Override
@@ -225,9 +241,19 @@
CallFunction call = getCallFunction(project, objectId, refName, isDelete, state);
return (source) -> {
+ boolean callSuccessful;
try {
- call.call(source);
- } catch (MissingParentObjectException e) {
+ callSuccessful = call.call(source);
+ } 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),
+ e);
+ callSuccessful = false;
+ }
+
+ if (!callSuccessful) {
callFetch(source, project, refName, state);
}
};
@@ -244,10 +270,18 @@
}
try {
- Optional<RevisionData> revisionData = revisionReader.read(project, objectId, refName);
+ Optional<RevisionData> revisionData =
+ revReaderProvider.get().read(project, objectId, refName, 0);
+ repLog.info(
+ "RevisionData is {} for {}:{}",
+ revisionData.map(RevisionData::toString).orElse("ABSENT"),
+ project,
+ refName);
+
if (revisionData.isPresent()) {
return ((source) ->
- callSendObject(source, project, refName, isDelete, revisionData.get(), state));
+ callSendObject(
+ source, project, refName, isDelete, Arrays.asList(revisionData.get()), state));
}
} catch (InvalidObjectIdException | IOException e) {
stateLog.error(
@@ -261,94 +295,189 @@
return (source) -> callFetch(source, project, refName, state);
}
- private void callSendObject(
+ private boolean callSendObject(
Source source,
Project.NameKey project,
String refName,
boolean isDelete,
- RevisionData revision,
+ List<RevisionData> revision,
ReplicationState state)
throws MissingParentObjectException {
+ 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 apply object to {} for {}:{} - {}",
+ apiUrl,
+ project,
+ refName,
+ revision);
+ Context<String> apiTimer = applyObjectMetrics.startEnd2End(source.getRemoteConfigName());
+ HttpResult result =
+ isDelete
+ ? fetchClient.callSendObject(project, refName, isDelete, null, uri)
+ : fetchClient.callSendObjects(project, refName, revision, uri);
+ boolean resultSuccessful = result.isSuccessful();
+ repLog.info(
+ "Pull replication REST API apply object to {} COMPLETED for {}:{} - {}, HTTP Result:"
+ + " {} - time:{} ms",
+ apiUrl,
+ project,
+ refName,
+ revision,
+ result,
+ apiTimer.stop() / 1000000.0);
- HttpResult result = fetchClient.callSendObject(project, refName, isDelete, revision, uri);
- if (isProjectMissing(result, project) && source.isCreateMissingRepositories()) {
+ if (!resultSuccessful
+ && result.isProjectMissing(project)
+ && source.isCreateMissingRepositories()) {
result = initProject(project, uri, fetchClient, result);
+ repLog.info("Missing project {} created, HTTP Result:{}", project, result);
}
- if (!result.isSuccessful()) {
- repLog.warn(
- String.format(
- "Pull replication rest api apply object call failed. Endpoint url: %s, reason:%s",
- apiUrl, result.getMessage().orElse("unknown")));
+
+ if (!resultSuccessful) {
if (result.isParentObjectMissing()) {
+
+ if (RefNames.isNoteDbMetaRef(refName) && revision.size() == 1) {
+ List<RevisionData> allRevisions =
+ fetchWholeMetaHistory(project, refName, revision.get(0));
+ repLog.info(
+ "Pull replication REST API apply object to {} for {}:{} - {}",
+ apiUrl,
+ project,
+ refName,
+ allRevisions);
+ return callSendObject(source, project, refName, isDelete, allRevisions, state);
+ }
+
throw new MissingParentObjectException(
project, refName, source.getRemoteConfigName());
}
}
+
+ resultIsSuccessful &= resultSuccessful;
} catch (URISyntaxException e) {
+ repLog.warn(
+ "Pull replication REST API apply object to {} *FAILED* for {}:{} - {}",
+ apiUrl,
+ project,
+ refName,
+ revision,
+ e);
stateLog.error(String.format("Cannot parse pull replication api url:%s", apiUrl), state);
+ resultIsSuccessful = false;
} catch (IOException e) {
+ repLog.warn(
+ "Pull replication REST API apply object to {} *FAILED* for {}:{} - {}",
+ apiUrl,
+ project,
+ refName,
+ revision,
+ e);
stateLog.error(
String.format(
- "Exception during the pull replication fetch rest api call. Endpoint url:%s, message:%s",
+ "Exception during the pull replication fetch rest api call. Endpoint url:%s,"
+ + " message:%s",
apiUrl, e.getMessage()),
e,
state);
+ resultIsSuccessful = false;
}
}
}
+
+ return resultIsSuccessful;
}
- private void callFetch(
+ private List<RevisionData> fetchWholeMetaHistory(
+ NameKey project, String refName, RevisionData revision)
+ throws RepositoryNotFoundException, MissingObjectException, IncorrectObjectTypeException,
+ CorruptObjectException, IOException {
+ RevisionReader revisionReader = revReaderProvider.get();
+ Optional<RevisionData> revisionDataWithParents =
+ revisionReader.read(project, refName, Integer.MAX_VALUE);
+
+ ImmutableList.Builder<RevisionData> revisionDataBuilder = ImmutableList.builder();
+ List<ObjectId> parentObjectIds =
+ revisionDataWithParents
+ .map(RevisionData::getParentObjetIds)
+ .orElse(Collections.emptyList());
+ for (ObjectId parentObjectId : parentObjectIds) {
+ revisionReader.read(project, parentObjectId, refName, 0).ifPresent(revisionDataBuilder::add);
+ }
+
+ revisionDataBuilder.add(revision);
+
+ return revisionDataBuilder.build();
+ }
+
+ private boolean callFetch(
Source source, Project.NameKey project, String refName, 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);
- HttpResult result = fetchClient.callFetch(project, refName, uri);
- if (isProjectMissing(result, project) && source.isCreateMissingRepositories()) {
+ 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, timer.getStartTime());
+ 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 (!result.isSuccessful()) {
+ 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",
+ "Exception during the pull replication fetch rest api call. Endpoint url:%s,"
+ + " message:%s",
apiUrl, e.getMessage()),
e,
state);
+ resultIsSuccessful = false;
}
}
}
+
+ return resultIsSuccessful;
}
public boolean retry(int attempt, int maxRetries) {
return maxRetries == 0 || attempt < maxRetries;
}
- private Boolean isProjectMissing(HttpResult result, Project.NameKey project) {
- return !result.isSuccessful() && result.isProjectMissing(project);
- }
-
private HttpResult initProject(
Project.NameKey project, URIish uri, FetchApiClient fetchClient, HttpResult result)
throws IOException, ClientProtocolException {
HttpResult initProjectResult = fetchClient.initProject(project, uri);
if (initProjectResult.isSuccessful()) {
- result = fetchClient.callFetch(project, "refs/*", uri);
+ result = fetchClient.callFetch(project, FetchOne.ALL_REFS, uri);
} else {
String errorMessage = initProjectResult.getMessage().map(e -> " - Error: " + e).orElse("");
repLog.error("Cannot create project " + project + errorMessage);
@@ -399,6 +528,6 @@
@FunctionalInterface
private interface CallFunction {
- void call(Source source) throws MissingParentObjectException;
+ boolean call(Source source) throws MissingParentObjectException;
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java
index 468c5fc..db46b23 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java
@@ -17,6 +17,7 @@
import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
@@ -24,6 +25,9 @@
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.diff.DiffEntry;
@@ -36,6 +40,7 @@
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
@@ -45,8 +50,11 @@
public class RevisionReader {
private static final String CONFIG_MAX_API_PAYLOAD_SIZE = "maxApiPayloadSize";
private static final Long DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES = 10000L;
+ static final String CONFIG_MAX_API_HISTORY_DEPTH = "maxApiHistoryDepth";
+ private static final int DEFAULT_MAX_API_HISTORY_DEPTH = 128;
private GitRepositoryManager gitRepositoryManager;
private Long maxRefSize;
+ private final int maxDepth;
@Inject
public RevisionReader(GitRepositoryManager gitRepositoryManager, ReplicationConfig cfg) {
@@ -54,17 +62,49 @@
this.maxRefSize =
cfg.getConfig()
.getLong("replication", CONFIG_MAX_API_PAYLOAD_SIZE, DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES);
+ this.maxDepth =
+ cfg.getConfig()
+ .getInt("replication", CONFIG_MAX_API_HISTORY_DEPTH, DEFAULT_MAX_API_HISTORY_DEPTH);
}
- public Optional<RevisionData> read(Project.NameKey project, ObjectId objectId, String refName)
+ public Optional<RevisionData> read(
+ Project.NameKey project, String refName, int maxParentObjectIds)
+ throws RepositoryNotFoundException, MissingObjectException, IncorrectObjectTypeException,
+ CorruptObjectException, IOException {
+ return read(project, null, refName, maxParentObjectIds);
+ }
+
+ public Optional<RevisionData> read(
+ Project.NameKey project,
+ @Nullable ObjectId refObjectId,
+ String refName,
+ int maxParentObjectIds)
throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
RepositoryNotFoundException, IOException {
try (Repository git = gitRepositoryManager.openRepository(project)) {
Long totalRefSize = 0l;
+ Ref ref = git.exactRef(refName);
+ if (ref == null) {
+ return Optional.empty();
+ }
+
+ ObjectId objectId = refObjectId == null ? ref.getObjectId() : refObjectId;
+
ObjectLoader commitLoader = git.open(objectId);
totalRefSize += commitLoader.getSize();
- verifySize(totalRefSize, commitLoader);
+ verifySize(project, refName, objectId, totalRefSize, commitLoader);
+
+ if (commitLoader.getType() == Constants.OBJ_BLOB) {
+ return Optional.of(
+ new RevisionData(
+ Collections.emptyList(),
+ null,
+ null,
+ Arrays.asList(
+ new RevisionObjectData(
+ objectId.name(), Constants.OBJ_BLOB, commitLoader.getCachedBytes()))));
+ }
if (commitLoader.getType() != Constants.OBJ_COMMIT) {
repLog.trace(
@@ -77,28 +117,34 @@
RevCommit commit = RevCommit.parse(commitLoader.getCachedBytes());
RevisionObjectData commitRev =
- new RevisionObjectData(commit.getType(), commitLoader.getCachedBytes());
+ new RevisionObjectData(objectId.name(), commit.getType(), commitLoader.getCachedBytes());
RevTree tree = commit.getTree();
- ObjectLoader treeLoader = git.open(commit.getTree().toObjectId());
+ ObjectId treeObjectId = commit.getTree().toObjectId();
+ ObjectLoader treeLoader = git.open(treeObjectId);
totalRefSize += treeLoader.getSize();
- verifySize(totalRefSize, treeLoader);
+ verifySize(project, refName, treeObjectId, totalRefSize, treeLoader);
RevisionObjectData treeRev =
- new RevisionObjectData(tree.getType(), treeLoader.getCachedBytes());
+ new RevisionObjectData(treeObjectId.name(), tree.getType(), treeLoader.getCachedBytes());
List<RevisionObjectData> blobs = Lists.newLinkedList();
try (TreeWalk walk = new TreeWalk(git)) {
if (commit.getParentCount() > 0) {
List<DiffEntry> diffEntries = readDiffs(git, commit, tree, walk);
- blobs = readBlobs(git, totalRefSize, diffEntries);
+ blobs = readBlobs(project, refName, git, totalRefSize, diffEntries);
} else {
walk.setRecursive(true);
walk.addTree(tree);
- blobs = readBlobs(git, totalRefSize, walk);
+ blobs = readBlobs(project, refName, git, totalRefSize, walk);
}
}
- return Optional.of(new RevisionData(commitRev, treeRev, blobs));
+
+ List<ObjectId> parentObjectIds =
+ getParentObjectIds(git, commit.getParents(), 0, Math.min(maxDepth, maxParentObjectIds));
+ Collections.reverse(parentObjectIds);
+
+ return Optional.of(new RevisionData(parentObjectIds, commitRev, treeRev, blobs));
} catch (LargeObjectException e) {
repLog.trace(
"Ref {} size for project {} is greater than configured '{}'",
@@ -109,6 +155,32 @@
}
}
+ private List<ObjectId> getParentObjectIds(
+ Repository git, RevCommit[] commit, int parentsDepth, int maxParentObjectIds)
+ throws MissingObjectException, IncorrectObjectTypeException, IOException {
+ if (commit == null || commit.length == 0) {
+ return Collections.emptyList();
+ }
+
+ ArrayList<ObjectId> parentObjectIds = new ArrayList<>();
+ for (RevCommit revCommit : commit) {
+ if (parentsDepth < maxParentObjectIds) {
+ parentObjectIds.add(revCommit.getId());
+ parentsDepth++;
+
+ ObjectLoader ol = git.open(revCommit.getId(), Constants.OBJ_COMMIT);
+ RevCommit[] commitParents = RevCommit.parse(ol.getCachedBytes()).getParents();
+
+ List<ObjectId> nestedParentObjectIds =
+ getParentObjectIds(git, commitParents, parentsDepth, maxParentObjectIds);
+ parentObjectIds.addAll(nestedParentObjectIds);
+ parentsDepth += nestedParentObjectIds.size();
+ }
+ }
+
+ return parentObjectIds;
+ }
+
private List<DiffEntry> readDiffs(Repository git, RevCommit commit, RevTree tree, TreeWalk walk)
throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
IOException {
@@ -117,7 +189,8 @@
return DiffEntry.scan(walk, true);
}
- private List<RevisionObjectData> readBlobs(Repository git, Long totalRefSize, TreeWalk walk)
+ private List<RevisionObjectData> readBlobs(
+ Project.NameKey projectName, String refName, Repository git, Long totalRefSize, TreeWalk walk)
throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
IOException {
List<RevisionObjectData> blobs = Lists.newLinkedList();
@@ -125,26 +198,33 @@
ObjectId objectId = walk.getObjectId(0);
ObjectLoader objectLoader = git.open(objectId);
totalRefSize += objectLoader.getSize();
- verifySize(totalRefSize, objectLoader);
+ verifySize(projectName, refName, objectId, totalRefSize, objectLoader);
RevisionObjectData rev =
- new RevisionObjectData(objectLoader.getType(), objectLoader.getCachedBytes());
+ new RevisionObjectData(
+ objectId.name(), objectLoader.getType(), objectLoader.getCachedBytes());
blobs.add(rev);
}
return blobs;
}
private List<RevisionObjectData> readBlobs(
- Repository git, Long totalRefSize, List<DiffEntry> diffEntries)
+ Project.NameKey projectName,
+ String refName,
+ Repository git,
+ Long totalRefSize,
+ List<DiffEntry> diffEntries)
throws MissingObjectException, IOException {
List<RevisionObjectData> blobs = Lists.newLinkedList();
for (DiffEntry diffEntry : diffEntries) {
if (!ChangeType.DELETE.equals(diffEntry.getChangeType())) {
- ObjectLoader objectLoader = git.open(diffEntry.getNewId().toObjectId());
+ ObjectId diffObjectId = diffEntry.getNewId().toObjectId();
+ ObjectLoader objectLoader = git.open(diffObjectId);
totalRefSize += objectLoader.getSize();
- verifySize(totalRefSize, objectLoader);
+ verifySize(projectName, refName, diffObjectId, totalRefSize, objectLoader);
RevisionObjectData rev =
- new RevisionObjectData(objectLoader.getType(), objectLoader.getCachedBytes());
+ new RevisionObjectData(
+ diffObjectId.name(), objectLoader.getType(), objectLoader.getCachedBytes());
blobs.add(rev);
}
}
@@ -159,9 +239,44 @@
return parentCommit.getTree();
}
- private void verifySize(Long totalRefSize, ObjectLoader loader) throws LargeObjectException {
- if (loader.isLarge() || totalRefSize > maxRefSize) {
- throw new LargeObjectException();
+ private void verifySize(
+ Project.NameKey projectName,
+ String refName,
+ ObjectId objectId,
+ Long totalRefSize,
+ ObjectLoader loader)
+ throws LargeObjectException {
+ if (loader.isLarge()) {
+ repLog.warn(
+ "Objects associated with {}:{} ({}) are too big to fit into the object loader's memory",
+ projectName,
+ refName,
+ objectTypeToString(loader.getType()));
+ throw new LargeObjectException(objectId);
+ }
+
+ if (totalRefSize > maxRefSize) {
+ repLog.warn(
+ "Objects associated with {}:{} ({}) use {} bytes, over the maximum limit of {} bytes",
+ projectName,
+ refName,
+ objectTypeToString(loader.getType()),
+ totalRefSize,
+ maxRefSize);
+ throw new LargeObjectException(objectId);
+ }
+ }
+
+ private static String objectTypeToString(int type) {
+ switch (type) {
+ case Constants.OBJ_BLOB:
+ return "BLOB";
+ case Constants.OBJ_COMMIT:
+ return "COMMIT";
+ case Constants.OBJ_TREE:
+ return "TREE";
+ default:
+ return "type:" + type;
}
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
index 71ec666..6bf4c21 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
@@ -61,6 +61,7 @@
import com.google.inject.servlet.RequestScoped;
import com.googlesource.gerrit.plugins.replication.RemoteSiteUser;
import com.googlesource.gerrit.plugins.replication.ReplicationFilter;
+import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
import com.googlesource.gerrit.plugins.replication.pull.fetch.BatchFetchClient;
import com.googlesource.gerrit.plugins.replication.pull.fetch.CGitFetch;
import com.googlesource.gerrit.plugins.replication.pull.fetch.CGitFetchValidator;
@@ -392,9 +393,10 @@
Project.NameKey project,
String ref,
ReplicationState state,
- ReplicationType replicationType) {
+ ReplicationType replicationType,
+ Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
URIish uri = getURI(project);
- return schedule(project, ref, uri, state, replicationType);
+ return schedule(project, ref, uri, state, replicationType, apiRequestMetrics);
}
public Future<?> schedule(
@@ -402,7 +404,8 @@
String ref,
URIish uri,
ReplicationState state,
- ReplicationType replicationType) {
+ ReplicationType replicationType,
+ Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
repLog.info("scheduling replication {}:{} => {}", uri, ref, project);
if (!shouldReplicate(project, ref, state)) {
@@ -438,7 +441,7 @@
FetchOne e = pending.get(uri);
Future<?> f = CompletableFuture.completedFuture(null);
if (e == null) {
- e = opFactory.create(project, uri);
+ e = opFactory.create(project, uri, apiRequestMetrics);
addRef(e, ref);
e.addState(ref, state);
pending.put(uri, e);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
index 6dfed44..a8799c2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
@@ -35,6 +35,7 @@
/* (non-Javadoc)
* @see com.googlesource.gerrit.plugins.replication.ConfigParser#parseRemotes(org.eclipse.jgit.lib.Config)
*/
+ @Override
public List<RemoteConfiguration> parseRemotes(Config config) throws ConfigInvalidException {
if (config.getSections().isEmpty()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
index 5132f41..04cc9e8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
@@ -14,6 +14,8 @@
package com.googlesource.gerrit.plugins.replication.pull.api;
+import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
+
import com.google.common.base.Strings;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -29,19 +31,20 @@
import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
import java.io.IOException;
import java.util.Objects;
+import javax.servlet.http.HttpServletResponse;
public class ApplyObjectAction implements RestModifyView<ProjectResource, RevisionInput> {
- private final ApplyObjectCommand command;
+ private final ApplyObjectCommand applyObjectCommand;
private final DeleteRefCommand deleteRefCommand;
private final FetchPreconditions preConditions;
@Inject
public ApplyObjectAction(
- ApplyObjectCommand command,
+ ApplyObjectCommand applyObjectCommand,
DeleteRefCommand deleteRefCommand,
FetchPreconditions preConditions) {
- this.command = command;
+ this.applyObjectCommand = applyObjectCommand;
this.deleteRefCommand = deleteRefCommand;
this.preConditions = preConditions;
}
@@ -50,42 +53,78 @@
public Response<?> apply(ProjectResource resource, RevisionInput input) throws RestApiException {
if (!preConditions.canCallFetchApi()) {
- throw new AuthException("not allowed to call fetch command");
+ throw new AuthException("Not allowed to call fetch command");
}
+ if (Strings.isNullOrEmpty(input.getLabel())) {
+ throw new BadRequestException("Source label cannot be null or empty");
+ }
+ if (Strings.isNullOrEmpty(input.getRefName())) {
+ throw new BadRequestException("Ref-update refname cannot be null or empty");
+ }
+
try {
- if (Strings.isNullOrEmpty(input.getLabel())) {
- throw new BadRequestException("Source label cannot be null or empty");
- }
- if (Strings.isNullOrEmpty(input.getRefName())) {
- throw new BadRequestException("Ref-update refname cannot be null or empty");
- }
+ repLog.info(
+ "Apply object API from {} for {}:{} - {}",
+ resource.getNameKey(),
+ input.getLabel(),
+ input.getRefName(),
+ input.getRevisionData());
if (Objects.isNull(input.getRevisionData())) {
deleteRefCommand.deleteRef(resource.getNameKey(), input.getRefName(), input.getLabel());
- return Response.created(input);
+ repLog.info(
+ "Apply object API - REF DELETED - from {} for {}:{} - {}",
+ resource.getNameKey(),
+ input.getLabel(),
+ input.getRefName(),
+ input.getRevisionData());
+ return Response.withStatusCode(HttpServletResponse.SC_NO_CONTENT, "");
}
- if (Objects.isNull(input.getRevisionData().getCommitObject())
- || Objects.isNull(input.getRevisionData().getCommitObject().getContent())
- || input.getRevisionData().getCommitObject().getContent().length == 0
- || Objects.isNull(input.getRevisionData().getCommitObject().getType())) {
- throw new BadRequestException("Ref-update commit object cannot be null or empty");
+ try {
+ input.validate();
+ } catch (IllegalArgumentException e) {
+ BadRequestException bre =
+ new BadRequestException("Ref-update with invalid input: " + e.getMessage(), e);
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ input.getRevisionData(),
+ bre);
+ throw bre;
}
- if (Objects.isNull(input.getRevisionData().getTreeObject())
- || Objects.isNull(input.getRevisionData().getTreeObject().getContent())
- || Objects.isNull(input.getRevisionData().getTreeObject().getType())) {
- throw new BadRequestException("Ref-update tree object cannot be null");
- }
-
- command.applyObject(
+ applyObjectCommand.applyObject(
resource.getNameKey(), input.getRefName(), input.getRevisionData(), input.getLabel());
return Response.created(input);
} catch (MissingParentObjectException e) {
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ input.getRevisionData(),
+ e);
throw new ResourceConflictException(e.getMessage(), e);
} catch (NumberFormatException | IOException e) {
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ input.getRevisionData(),
+ e);
throw RestApiException.wrap(e.getMessage(), e);
} catch (RefUpdateException e) {
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ input.getRevisionData(),
+ e);
throw new UnprocessableEntityException(e.getMessage());
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java
index 276aa16..b27d8b7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java
@@ -37,6 +37,7 @@
import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Set;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.transport.RefSpec;
@@ -70,12 +71,25 @@
}
public void applyObject(
- Project.NameKey name, String refName, RevisionData revisionData, String sourceLabel)
+ Project.NameKey name, String refName, RevisionData revisionsData, String sourceLabel)
+ throws IOException, RefUpdateException, MissingParentObjectException {
+ applyObjects(name, refName, new RevisionData[] {revisionsData}, sourceLabel);
+ }
+
+ public void applyObjects(
+ Project.NameKey name, String refName, RevisionData[] revisionsData, String sourceLabel)
throws IOException, RefUpdateException, MissingParentObjectException {
- repLog.info("Apply object from {} for project {}, ref name {}", sourceLabel, name, refName);
+ repLog.info(
+ "Apply object from {} for {}:{} - {}",
+ sourceLabel,
+ name,
+ refName,
+ Arrays.toString(revisionsData));
Timer1.Context<String> context = metrics.start(sourceLabel);
- RefUpdateState refUpdateState = applyObject.apply(name, new RefSpec(refName), revisionData);
+
+ RefUpdateState refUpdateState = applyObject.apply(name, new RefSpec(refName), revisionsData);
+
long elapsed = NANOSECONDS.toMillis(context.stop());
try {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java
new file mode 100644
index 0000000..a1e1f5b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+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.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import javax.servlet.http.HttpServletResponse;
+
+public class ApplyObjectsAction implements RestModifyView<ProjectResource, RevisionsInput> {
+
+ private final ApplyObjectCommand command;
+ private final DeleteRefCommand deleteRefCommand;
+ private final FetchPreconditions preConditions;
+
+ @Inject
+ public ApplyObjectsAction(
+ ApplyObjectCommand command,
+ DeleteRefCommand deleteRefCommand,
+ FetchPreconditions preConditions) {
+ this.command = command;
+ this.deleteRefCommand = deleteRefCommand;
+ this.preConditions = preConditions;
+ }
+
+ @Override
+ public Response<?> apply(ProjectResource resource, RevisionsInput input) throws RestApiException {
+ if (!preConditions.canCallFetchApi()) {
+ throw new AuthException("not allowed to call fetch command");
+ }
+
+ try {
+ if (Strings.isNullOrEmpty(input.getLabel())) {
+ throw new BadRequestException("Source label cannot be null or empty");
+ }
+ if (Strings.isNullOrEmpty(input.getRefName())) {
+ throw new BadRequestException("Ref-update refname cannot be null or empty");
+ }
+
+ repLog.info(
+ "Apply object API from {} for {}:{} - {}",
+ resource.getNameKey(),
+ input.getLabel(),
+ input.getRefName(),
+ Arrays.toString(input.getRevisionsData()));
+
+ if (Objects.isNull(input.getRevisionsData())) {
+ deleteRefCommand.deleteRef(resource.getNameKey(), input.getRefName(), input.getLabel());
+ repLog.info(
+ "Apply object API - REF DELETED - from {} for {}:{}",
+ resource.getNameKey(),
+ input.getLabel(),
+ input.getRefName());
+ return Response.withStatusCode(HttpServletResponse.SC_NO_CONTENT, "");
+ }
+
+ try {
+ input.validate();
+ } catch (IllegalArgumentException e) {
+ BadRequestException bre =
+ new BadRequestException("Ref-update with invalid input: " + e.getMessage(), e);
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ Arrays.toString(input.getRevisionsData()),
+ bre);
+ throw bre;
+ }
+
+ command.applyObjects(
+ resource.getNameKey(), input.getRefName(), input.getRevisionsData(), input.getLabel());
+ return Response.created(input);
+ } catch (MissingParentObjectException e) {
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ Arrays.toString(input.getRevisionsData()),
+ e);
+ throw new ResourceConflictException(e.getMessage(), e);
+ } catch (NumberFormatException | IOException e) {
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ Arrays.toString(input.getRevisionsData()),
+ e);
+ throw RestApiException.wrap(e.getMessage(), e);
+ } catch (RefUpdateException e) {
+ repLog.error(
+ "Apply object API *FAILED* from {} for {}:{} - {}",
+ input.getLabel(),
+ resource.getNameKey(),
+ input.getRefName(),
+ Arrays.toString(input.getRevisionsData()),
+ e);
+ throw new UnprocessableEntityException(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
index 0c5e016..2a3a79d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -19,41 +19,52 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.restapi.project.DeleteRef;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.replication.pull.Context;
import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
import java.io.IOException;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
public class DeleteRefCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final PullReplicationStateLogger fetchStateLog;
- private final DeleteRef deleteRef;
+ private final ApplyObject applyObject;
private final DynamicItem<EventDispatcher> eventDispatcher;
private final ProjectCache projectCache;
+ private final PermissionBackend permissionBackend;
+ private final LocalDiskRepositoryManager gitManager;
@Inject
public DeleteRefCommand(
PullReplicationStateLogger fetchStateLog,
ProjectCache projectCache,
- DeleteRef deleteRef,
- DynamicItem<EventDispatcher> eventDispatcher) {
+ ApplyObject applyObject,
+ PermissionBackend permissionBackend,
+ DynamicItem<EventDispatcher> eventDispatcher,
+ LocalDiskRepositoryManager gitManager) {
this.fetchStateLog = fetchStateLog;
this.projectCache = projectCache;
- this.deleteRef = deleteRef;
+ this.applyObject = applyObject;
this.eventDispatcher = eventDispatcher;
+ this.permissionBackend = permissionBackend;
+ this.gitManager = gitManager;
}
public void deleteRef(Project.NameKey name, String refName, String sourceLabel)
@@ -66,8 +77,15 @@
}
try {
+ projectState.get().checkStatePermitsWrite();
+ permissionBackend
+ .currentUser()
+ .project(projectState.get().getNameKey())
+ .ref(refName)
+ .check(RefPermission.DELETE);
+
Context.setLocalEvent(true);
- deleteRef.deleteSingleRef(projectState.get(), refName);
+ deleteRef(name, refName);
eventDispatcher
.get()
@@ -83,7 +101,7 @@
"Unexpected error while trying to delete ref '%s' on project %s and notifying it",
refName, name);
throw RestApiException.wrap(e.getMessage(), e);
- } catch (ResourceConflictException e) {
+ } catch (IOException e) {
eventDispatcher
.get()
.postEvent(
@@ -110,4 +128,18 @@
throw RestApiException.wrap(e.getMessage(), e);
}
}
+
+ private RefUpdateState deleteRef(Project.NameKey name, String refName) throws IOException {
+
+ try (Repository repository = gitManager.openRepository(name)) {
+ RefUpdate.Result result;
+ RefUpdate u = repository.updateRef(refName);
+ u.setExpectedOldObjectId(repository.exactRef(refName).getObjectId());
+ u.setNewObjectId(ObjectId.zeroId());
+ u.setForceUpdate(true);
+
+ result = u.delete();
+ return new RefUpdateState(refName, result);
+ }
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
index 2c9583a..fdb4f8f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -104,7 +104,10 @@
@SuppressWarnings("unchecked")
WorkQueue.Task<Void> task =
(WorkQueue.Task<Void>)
- workQueue.getDefaultQueue().submit(fetchJobFactory.create(project, input));
+ workQueue
+ .getDefaultQueue()
+ .submit(
+ fetchJobFactory.create(project, input, PullReplicationApiRequestMetrics.get()));
Optional<String> url =
urlFormatter
.get()
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
index e1ac9ac..3a502ef 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
@@ -54,19 +54,28 @@
this.eventDispatcher = eventDispatcher;
}
- public void fetchAsync(Project.NameKey name, String label, String refName)
+ public void fetchAsync(
+ Project.NameKey name,
+ String label,
+ String refName,
+ PullReplicationApiRequestMetrics apiRequestMetrics)
throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
TimeoutException {
- fetch(name, label, refName, ASYNC);
+ fetch(name, label, refName, ASYNC, Optional.of(apiRequestMetrics));
}
public void fetchSync(Project.NameKey name, String label, String refName)
throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
TimeoutException {
- fetch(name, label, refName, SYNC);
+ fetch(name, label, refName, SYNC, Optional.empty());
}
- private void fetch(Project.NameKey name, String label, String refName, ReplicationType fetchType)
+ private void fetch(
+ Project.NameKey name,
+ String label,
+ String refName,
+ ReplicationType fetchType,
+ Optional<PullReplicationApiRequestMetrics> apiRequestMetrics)
throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
TimeoutException {
ReplicationState state =
@@ -82,7 +91,7 @@
try {
state.markAllFetchTasksScheduled();
- Future<?> future = source.get().schedule(name, refName, state, fetchType);
+ Future<?> future = source.get().schedule(name, refName, state, fetchType, apiRequestMetrics);
future.get(source.get().getTimeout(), TimeUnit.SECONDS);
} catch (ExecutionException
| IllegalStateException
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
index f533734..9478d5c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
@@ -26,25 +26,31 @@
private static final FluentLogger log = FluentLogger.forEnclosingClass();
public interface Factory {
- FetchJob create(Project.NameKey project, FetchAction.Input input);
+ FetchJob create(
+ Project.NameKey project, FetchAction.Input input, PullReplicationApiRequestMetrics metrics);
}
private FetchCommand command;
private Project.NameKey project;
private FetchAction.Input input;
+ private final PullReplicationApiRequestMetrics metrics;
@Inject
public FetchJob(
- FetchCommand command, @Assisted Project.NameKey project, @Assisted FetchAction.Input input) {
+ FetchCommand command,
+ @Assisted Project.NameKey project,
+ @Assisted FetchAction.Input input,
+ PullReplicationApiRequestMetrics metrics) {
this.command = command;
this.project = project;
this.input = input;
+ this.metrics = metrics;
}
@Override
public void run() {
try {
- command.fetchAsync(project, input.label, input.refName);
+ command.fetchAsync(project, input.label, input.refName, metrics);
} catch (InterruptedException
| ExecutionException
| RemoteConfigurationMissingException
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
index b2ef28d..b140cb4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
@@ -38,5 +38,9 @@
} else {
serveRegex("/init-project/.*$").with(ProjectInitializationAction.class);
}
+
+ DynamicSet.bind(binder(), AllRequestFilter.class)
+ .to(PullReplicationApiMetricsFilter.class)
+ .in(Scopes.SINGLETON);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiMetricsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiMetricsFilter.java
new file mode 100644
index 0000000..3858db2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiMetricsFilter.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2022 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.httpd.AllRequestFilter;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class PullReplicationApiMetricsFilter extends AllRequestFilter {
+ private final Provider<PullReplicationApiRequestMetrics> apiRequestMetrics;
+
+ @Inject
+ public PullReplicationApiMetricsFilter(
+ Provider<PullReplicationApiRequestMetrics> apiRequestMetrics) {
+ this.apiRequestMetrics = apiRequestMetrics;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ PullReplicationApiRequestMetrics requestMetrics = apiRequestMetrics.get();
+ requestMetrics.start((HttpServletRequest) request);
+ PullReplicationApiRequestMetrics.set(requestMetrics);
+
+ chain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
index e7b3a7f..d1d28a6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
@@ -31,6 +31,7 @@
bind(UpdateHeadAction.class).in(Scopes.SINGLETON);
post(PROJECT_KIND, "fetch").to(FetchAction.class);
post(PROJECT_KIND, "apply-object").to(ApplyObjectAction.class);
+ post(PROJECT_KIND, "apply-objects").to(ApplyObjectsAction.class);
delete(PROJECT_KIND, "delete-project").to(ProjectDeletionAction.class);
put(PROJECT_KIND, "HEAD").to(UpdateHeadAction.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiRequestMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiRequestMetrics.java
new file mode 100644
index 0000000..8e4e43f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiRequestMetrics.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2022 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 java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.gerrit.server.events.Event;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.pull.FetchReplicationMetrics;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.servlet.http.HttpServletRequest;
+
+public class PullReplicationApiRequestMetrics {
+ private static final ThreadLocal<PullReplicationApiRequestMetrics> localApiRequestMetrics =
+ new ThreadLocal<>();
+
+ public static final String HTTP_HEADER_X_START_TIME_NANOS = "X-StartTimeNanos";
+
+ private Optional<Long> startTimeNanos = Optional.empty();
+ private final AtomicBoolean initialised = new AtomicBoolean();
+ private final FetchReplicationMetrics metrics;
+
+ public static PullReplicationApiRequestMetrics get() {
+ return localApiRequestMetrics.get();
+ }
+
+ public static void set(PullReplicationApiRequestMetrics metrics) {
+ localApiRequestMetrics.set(metrics);
+ }
+
+ @Inject
+ public PullReplicationApiRequestMetrics(FetchReplicationMetrics metrics) {
+ this.metrics = metrics;
+ }
+
+ public void start(HttpServletRequest req) {
+ if (!initialised.compareAndSet(false, true)) {
+ throw new IllegalStateException("PullReplicationApiRequestMetrics already initialised");
+ }
+
+ startTimeNanos =
+ Optional.ofNullable(req.getHeader(HTTP_HEADER_X_START_TIME_NANOS))
+ .map(Long::parseLong)
+ /* Adjust with the system's nanotime for preventing negative execution times
+ * due to a clock skew between the client and the server timestamp.
+ */
+ .map(nanoTime -> Math.min(currentTimeNanos(), nanoTime));
+ }
+
+ public void start(Event event) {
+ if (!initialised.compareAndSet(false, true)) {
+ throw new IllegalStateException("PullReplicationApiRequestMetrics already initialised");
+ }
+ startTimeNanos = Optional.of(event.eventCreatedOn * 1000 * 1000 * 1000);
+ }
+
+ public Optional<Long> stop(String replicationSourceName) {
+ return startTimeNanos.map(
+ start -> {
+ long elapsed = currentTimeNanos() - start;
+ metrics.recordEnd2End(replicationSourceName, elapsed);
+ return elapsed;
+ });
+ }
+
+ private long currentTimeNanos() {
+ return MILLISECONDS.toNanos(System.currentTimeMillis());
+ }
+}
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 4af2bf7..0a9b266 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
@@ -55,6 +55,7 @@
import com.google.inject.TypeLiteral;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.InitProjectException;
import java.io.BufferedReader;
import java.io.EOFException;
@@ -75,6 +76,7 @@
private FetchAction fetchAction;
private ApplyObjectAction applyObjectAction;
+ private ApplyObjectsAction applyObjectsAction;
private ProjectInitializationAction projectInitializationAction;
private UpdateHeadAction updateHEADAction;
private ProjectDeletionAction projectDeletionAction;
@@ -87,6 +89,7 @@
public PullReplicationFilter(
FetchAction fetchAction,
ApplyObjectAction applyObjectAction,
+ ApplyObjectsAction applyObjectsAction,
ProjectInitializationAction projectInitializationAction,
UpdateHeadAction updateHEADAction,
ProjectDeletionAction projectDeletionAction,
@@ -95,6 +98,7 @@
@PluginName String pluginName) {
this.fetchAction = fetchAction;
this.applyObjectAction = applyObjectAction;
+ this.applyObjectsAction = applyObjectsAction;
this.projectInitializationAction = projectInitializationAction;
this.updateHEADAction = updateHEADAction;
this.projectDeletionAction = projectDeletionAction;
@@ -127,6 +131,12 @@
} else {
httpResponse.sendError(SC_UNAUTHORIZED);
}
+ } else if (isApplyObjectsAction(httpRequest)) {
+ if (userProvider.get().isIdentifiedUser()) {
+ writeResponse(httpResponse, doApplyObjects(httpRequest));
+ } else {
+ httpResponse.sendError(SC_UNAUTHORIZED);
+ }
} else if (isInitProjectAction(httpRequest)) {
if (userProvider.get().isIdentifiedUser()) {
if (!checkAcceptHeader(httpRequest, httpResponse)) {
@@ -199,6 +209,16 @@
}
@SuppressWarnings("unchecked")
+ private Response<Map<String, Object>> doApplyObjects(HttpServletRequest httpRequest)
+ throws RestApiException, IOException, PermissionBackendException {
+ RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class));
+ IdString id = getProjectName(httpRequest);
+ ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
+
+ return (Response<Map<String, Object>>) applyObjectsAction.apply(projectResource, input);
+ }
+
+ @SuppressWarnings("unchecked")
private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception {
HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class));
ProjectResource projectResource =
@@ -293,6 +313,10 @@
return httpRequest.getRequestURI().endsWith("pull-replication~apply-object");
}
+ private boolean isApplyObjectsAction(HttpServletRequest httpRequest) {
+ return httpRequest.getRequestURI().endsWith("pull-replication~apply-objects");
+ }
+
private boolean isFetchAction(HttpServletRequest httpRequest) {
return httpRequest.getRequestURI().endsWith("pull-replication~fetch");
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionData.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionData.java
index bcd4e05..ffe98da 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionData.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionData.java
@@ -15,8 +15,11 @@
package com.googlesource.gerrit.plugins.replication.pull.api.data;
import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
public class RevisionData {
+ private transient List<ObjectId> parentObjectIds;
+
private RevisionObjectData commitObject;
private RevisionObjectData treeObject;
@@ -24,14 +27,20 @@
private List<RevisionObjectData> blobs;
public RevisionData(
+ List<ObjectId> parentObjectIds,
RevisionObjectData commitObject,
RevisionObjectData treeObject,
List<RevisionObjectData> blobs) {
+ this.parentObjectIds = parentObjectIds;
this.commitObject = commitObject;
this.treeObject = treeObject;
this.blobs = blobs;
}
+ public List<ObjectId> getParentObjetIds() {
+ return parentObjectIds;
+ }
+
public RevisionObjectData getCommitObject() {
return commitObject;
}
@@ -43,4 +52,15 @@
public List<RevisionObjectData> getBlobs() {
return blobs;
}
+
+ @Override
+ public String toString() {
+ return "{"
+ + (commitObject != null ? "commitObject=" + commitObject : "")
+ + " "
+ + (treeObject != null ? "treeObject=" + treeObject : "")
+ + " "
+ + (blobs != null && !blobs.isEmpty() ? "blobs=" + blobs : "")
+ + "}";
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionInput.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionInput.java
index bc3e218..c18e11d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionInput.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionInput.java
@@ -14,6 +14,10 @@
package com.googlesource.gerrit.plugins.replication.pull.api.data;
+import java.util.List;
+import java.util.Objects;
+import org.eclipse.jgit.lib.Constants;
+
public class RevisionInput {
private String label;
@@ -38,4 +42,44 @@
public RevisionData getRevisionData() {
return revisionData;
}
+
+ public void validate() {
+ validate(refName, revisionData);
+ }
+
+ static void validate(String refName, RevisionData revisionData) {
+ // Non-heads refs can point to non-commit objects
+ if (!refName.startsWith(Constants.R_HEADS)
+ && Objects.isNull(revisionData.getCommitObject())
+ && Objects.isNull(revisionData.getTreeObject())) {
+
+ List<RevisionObjectData> blobs = revisionData.getBlobs();
+
+ if (Objects.isNull(blobs) || blobs.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Ref " + refName + " cannot have a null or empty list of BLOBs associated");
+ }
+
+ if (blobs.size() > 1) {
+ throw new IllegalArgumentException("Ref " + refName + " has more than one BLOB associated");
+ }
+
+ return;
+ }
+
+ if (Objects.isNull(revisionData.getCommitObject())
+ || Objects.isNull(revisionData.getCommitObject().getContent())
+ || revisionData.getCommitObject().getContent().length == 0
+ || Objects.isNull(revisionData.getCommitObject().getType())) {
+ throw new IllegalArgumentException(
+ "Commit object for ref " + refName + " cannot be null or empty");
+ }
+
+ if (Objects.isNull(revisionData.getTreeObject())
+ || Objects.isNull(revisionData.getTreeObject().getContent())
+ || Objects.isNull(revisionData.getTreeObject().getType())) {
+ throw new IllegalArgumentException(
+ "Ref-update tree object for ref " + refName + " cannot be null");
+ }
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionObjectData.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionObjectData.java
index 02ba06c..b21f495 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionObjectData.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionObjectData.java
@@ -15,12 +15,15 @@
package com.googlesource.gerrit.plugins.replication.pull.api.data;
import java.util.Base64;
+import org.eclipse.jgit.lib.Constants;
public class RevisionObjectData {
+ private final String sha1;
private final Integer type;
private final String content;
- public RevisionObjectData(int type, byte[] content) {
+ public RevisionObjectData(String sha1, int type, byte[] content) {
+ this.sha1 = sha1;
this.type = type;
this.content = content == null ? "" : Base64.getEncoder().encodeToString(content);
}
@@ -32,4 +35,29 @@
public byte[] getContent() {
return Base64.getDecoder().decode(content);
}
+
+ public String getSha1() {
+ return sha1;
+ }
+
+ @Override
+ public String toString() {
+ String typeStr;
+ switch (type) {
+ case Constants.OBJ_BLOB:
+ typeStr = "BLOB";
+ break;
+ case Constants.OBJ_COMMIT:
+ typeStr = "COMMIT";
+ break;
+ case Constants.OBJ_TREE:
+ typeStr = "TREE";
+ break;
+ default:
+ typeStr = "type:" + type;
+ break;
+ }
+
+ return sha1 + " (" + typeStr + ")";
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionsInput.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionsInput.java
new file mode 100644
index 0000000..2361f6b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/RevisionsInput.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2022 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.Arrays;
+
+public class RevisionsInput {
+ private String label;
+
+ private String refName;
+
+ private RevisionData[] revisionsData;
+
+ public RevisionsInput(String label, String refName, RevisionData[] revisionsData) {
+ this.label = label;
+ this.refName = refName;
+ this.revisionsData = revisionsData;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getRefName() {
+ return refName;
+ }
+
+ public RevisionData[] getRevisionsData() {
+ return revisionsData;
+ }
+
+ public void validate() {
+ for (RevisionData revisionData : revisionsData) {
+ RevisionInput.validate(refName, revisionData);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RevisionsInput { "
+ + label
+ + ":"
+ + refName
+ + " - "
+ + Arrays.toString(revisionsData)
+ + "}";
+ }
+}
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 476a35b..6000eb9 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
@@ -14,10 +14,13 @@
package com.googlesource.gerrit.plugins.replication.pull.client;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
import com.google.gerrit.entities.Project;
import com.googlesource.gerrit.plugins.replication.pull.Source;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import java.io.IOException;
+import java.util.List;
import org.apache.http.client.ClientProtocolException;
import org.eclipse.jgit.transport.URIish;
@@ -27,9 +30,15 @@
FetchApiClient create(Source source);
}
- HttpResult callFetch(Project.NameKey project, String refName, URIish targetUri)
+ HttpResult callFetch(
+ Project.NameKey project, String refName, URIish targetUri, long startTimeNanos)
throws ClientProtocolException, IOException;
+ default HttpResult callFetch(Project.NameKey project, String refName, URIish targetUri)
+ throws ClientProtocolException, IOException {
+ return callFetch(project, refName, targetUri, MILLISECONDS.toNanos(System.currentTimeMillis()));
+ }
+
HttpResult initProject(Project.NameKey project, URIish uri) throws IOException;
HttpResult deleteProject(Project.NameKey project, URIish apiUri) throws IOException;
@@ -43,4 +52,8 @@
RevisionData revisionData,
URIish targetUri)
throws ClientProtocolException, IOException;
+
+ HttpResult callSendObjects(
+ Project.NameKey project, String refName, List<RevisionData> revisionData, URIish targetUri)
+ throws ClientProtocolException, IOException;
}
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 f1d486d..9b33c81 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
@@ -23,6 +23,7 @@
import com.google.common.net.MediaType;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.config.GerritInstanceId;
@@ -33,11 +34,14 @@
import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
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.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
import com.googlesource.gerrit.plugins.replication.pull.filter.SyncRefsFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.Optional;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
@@ -100,7 +104,8 @@
* @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#callFetch(com.google.gerrit.entities.Project.NameKey, java.lang.String, org.eclipse.jgit.transport.URIish)
*/
@Override
- public HttpResult callFetch(Project.NameKey project, String refName, URIish targetUri)
+ public HttpResult callFetch(
+ Project.NameKey project, String refName, URIish targetUri, long startTimeNanos)
throws ClientProtocolException, IOException {
String url =
String.format(
@@ -115,6 +120,9 @@
instanceId, refName, callAsync),
StandardCharsets.UTF_8));
post.addHeader(new BasicHeader("Content-Type", "application/json"));
+ post.addHeader(
+ PullReplicationApiRequestMetrics.HTTP_HEADER_X_START_TIME_NANOS,
+ Long.toString(startTimeNanos));
return httpClientFactory.create(source).execute(post, this, getContext(targetUri));
}
@@ -179,10 +187,7 @@
}
RevisionInput input = new RevisionInput(instanceId, refName, revisionData);
- String url =
- String.format(
- "%s/a/projects/%s/%s~apply-object",
- targetUri.toString(), Url.encode(project.get()), pluginName);
+ String url = formatUrl(project, targetUri, "apply-object");
HttpPost post = new HttpPost(url);
post.setEntity(new StringEntity(GSON.toJson(input)));
@@ -190,6 +195,32 @@
return httpClientFactory.create(source).execute(post, this, getContext(targetUri));
}
+ @Override
+ public HttpResult callSendObjects(
+ NameKey project, String refName, List<RevisionData> revisionData, URIish targetUri)
+ throws ClientProtocolException, IOException {
+ if (revisionData.size() == 1) {
+ return callSendObject(project, refName, false, revisionData.get(0), targetUri);
+ }
+
+ RevisionData[] inputData = new RevisionData[revisionData.size()];
+ RevisionsInput input = new RevisionsInput(instanceId, refName, revisionData.toArray(inputData));
+
+ String url = formatUrl(project, targetUri, "apply-objects");
+ HttpPost post = new HttpPost(url);
+ post.setEntity(new StringEntity(GSON.toJson(input)));
+ post.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString()));
+ return httpClientFactory.create(source).execute(post, this, getContext(targetUri));
+ }
+
+ private String formatUrl(Project.NameKey project, URIish targetUri, String api) {
+ String url =
+ String.format(
+ "%s/a/projects/%s/%s~%s",
+ targetUri.toString(), Url.encode(project.get()), pluginName, api);
+ return url;
+ }
+
private void requireNull(Object object, String string) {
if (object != null) {
throw new IllegalArgumentException(string);
@@ -198,13 +229,20 @@
@Override
public HttpResult handleResponse(HttpResponse response) {
- Optional<String> responseBody = Optional.empty();
- try {
- responseBody = Optional.ofNullable(EntityUtils.toString(response.getEntity()));
- } catch (ParseException | IOException e) {
- logger.atSevere().withCause(e).log("Unable get response body from %s", response.toString());
- }
+ Optional<String> responseBody =
+ Optional.ofNullable(response.getEntity())
+ .flatMap(
+ body -> {
+ try {
+ return Optional.of(EntityUtils.toString(body));
+ } catch (ParseException | IOException e) {
+ logger.atSevere().withCause(e).log(
+ "Unable get response body from %s", response.toString());
+ return Optional.empty();
+ }
+ });
+
return new HttpResult(response.getStatusLine().getStatusCode(), responseBody);
}
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 bd164df..ec9d65f 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,9 +15,6 @@
package com.googlesource.gerrit.plugins.replication.pull.client;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
-import static javax.servlet.http.HttpServletResponse.SC_CREATED;
-import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
-import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.gerrit.entities.Project;
import java.util.Optional;
@@ -36,7 +33,7 @@
}
public boolean isSuccessful() {
- return responseCode == SC_CREATED || responseCode == SC_NO_CONTENT || responseCode == SC_OK;
+ return responseCode / 100 == 2; // Any 2xx response code is a success
}
public boolean isProjectMissing(Project.NameKey projectName) {
@@ -47,4 +44,11 @@
public boolean isParentObjectMissing() {
return responseCode == SC_CONFLICT;
}
+
+ @Override
+ public String toString() {
+ return isSuccessful()
+ ? "OK"
+ : "FAILED" + ", status=" + responseCode + message.map(s -> " '" + s + "'").orElse("");
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
index 76e3cae..0f092e5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
@@ -30,11 +30,13 @@
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.googlesource.gerrit.plugins.replication.pull.FetchOne;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory;
import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction;
+import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
import org.eclipse.jgit.lib.ObjectId;
public class StreamEventListener implements EventListener {
@@ -45,17 +47,20 @@
private ProjectInitializationAction projectInitializationAction;
private Factory fetchJobFactory;
+ private final Provider<PullReplicationApiRequestMetrics> metricsProvider;
@Inject
public StreamEventListener(
@Nullable @GerritInstanceId String instanceId,
ProjectInitializationAction projectInitializationAction,
WorkQueue workQueue,
- FetchJob.Factory fetchJobFactory) {
+ FetchJob.Factory fetchJobFactory,
+ Provider<PullReplicationApiRequestMetrics> metricsProvider) {
this.instanceId = instanceId;
this.projectInitializationAction = projectInitializationAction;
this.workQueue = workQueue;
this.fetchJobFactory = fetchJobFactory;
+ this.metricsProvider = metricsProvider;
requireNonNull(
Strings.emptyToNull(this.instanceId), "gerrit.instanceId cannot be null or empty");
@@ -64,13 +69,16 @@
@Override
public void onEvent(Event event) {
if (!instanceId.equals(event.instanceId)) {
+ PullReplicationApiRequestMetrics metrics = metricsProvider.get();
+ metrics.start(event);
if (event instanceof RefUpdatedEvent) {
RefUpdatedEvent refUpdatedEvent = (RefUpdatedEvent) event;
if (!isProjectDelete(refUpdatedEvent)) {
fetchRefsAsync(
refUpdatedEvent.getRefName(),
refUpdatedEvent.instanceId,
- refUpdatedEvent.getProjectNameKey());
+ refUpdatedEvent.getProjectNameKey(),
+ metrics);
}
}
if (event instanceof ProjectCreatedEvent) {
@@ -80,7 +88,8 @@
fetchRefsAsync(
FetchOne.ALL_REFS,
projectCreatedEvent.instanceId,
- projectCreatedEvent.getProjectNameKey());
+ projectCreatedEvent.getProjectNameKey(),
+ metrics);
} catch (AuthException | PermissionBackendException e) {
logger.atSevere().withCause(e).log(
"Cannot initialise project:%s", projectCreatedEvent.projectName);
@@ -94,11 +103,15 @@
&& ObjectId.zeroId().equals(ObjectId.fromString(event.refUpdate.get().newRev));
}
- protected void fetchRefsAsync(String refName, String sourceInstanceId, NameKey projectNameKey) {
+ protected void fetchRefsAsync(
+ String refName,
+ String sourceInstanceId,
+ NameKey projectNameKey,
+ PullReplicationApiRequestMetrics metrics) {
FetchAction.Input input = new FetchAction.Input();
input.refName = refName;
input.label = sourceInstanceId;
- workQueue.getDefaultQueue().submit(fetchJobFactory.create(projectNameKey, input));
+ workQueue.getDefaultQueue().submit(fetchJobFactory.create(projectNameKey, input, metrics));
}
private String getProjectRepositoryName(ProjectCreatedEvent projectCreatedEvent) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
index 03362bf..2bb1caf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
@@ -41,35 +41,51 @@
this.gitManager = gitManager;
}
- public RefUpdateState apply(Project.NameKey name, RefSpec refSpec, RevisionData revisionData)
+ public RefUpdateState apply(Project.NameKey name, RefSpec refSpec, RevisionData[] revisionsData)
throws MissingParentObjectException, IOException {
try (Repository git = gitManager.openRepository(name)) {
- ObjectId newObjectID = null;
+ ObjectId refHead = null;
+ RefUpdate ru = git.updateRef(refSpec.getSource());
try (ObjectInserter oi = git.newObjectInserter()) {
- RevisionObjectData commitObject = revisionData.getCommitObject();
- RevCommit commit = RevCommit.parse(commitObject.getContent());
- for (RevCommit parent : commit.getParents()) {
- if (!git.getObjectDatabase().has(parent.getId())) {
- throw new MissingParentObjectException(name, refSpec.getSource(), parent.getId());
+ for (RevisionData revisionData : revisionsData) {
+
+ ObjectId newObjectID = null;
+ RevisionObjectData commitObject = revisionData.getCommitObject();
+
+ if (commitObject != null) {
+ RevCommit commit = RevCommit.parse(commitObject.getContent());
+ for (RevCommit parent : commit.getParents()) {
+ if (!git.getObjectDatabase().has(parent.getId())) {
+ throw new MissingParentObjectException(name, refSpec.getSource(), parent.getId());
+ }
+ }
+ refHead = newObjectID = oi.insert(commitObject.getType(), commitObject.getContent());
+
+ RevisionObjectData treeObject = revisionData.getTreeObject();
+ oi.insert(treeObject.getType(), treeObject.getContent());
+ }
+
+ for (RevisionObjectData rev : revisionData.getBlobs()) {
+ ObjectId blobObjectId = oi.insert(rev.getType(), rev.getContent());
+ if (newObjectID == null) {
+ newObjectID = blobObjectId;
+ }
+ refHead = newObjectID;
+ }
+
+ oi.flush();
+
+ if (commitObject == null) {
+ // Non-commits must be forced as they do not have a graph associated
+ ru.setForceUpdate(true);
}
}
- newObjectID = oi.insert(commitObject.getType(), commitObject.getContent());
- RevisionObjectData treeObject = revisionData.getTreeObject();
- oi.insert(treeObject.getType(), treeObject.getContent());
-
- for (RevisionObjectData rev : revisionData.getBlobs()) {
- oi.insert(rev.getType(), rev.getContent());
- }
-
- oi.flush();
+ ru.setNewObjectId(refHead);
+ RefUpdate.Result result = ru.update();
+ return new RefUpdateState(refSpec.getSource(), result);
}
- RefUpdate ru = git.updateRef(refSpec.getSource());
- ru.setNewObjectId(newObjectID);
- RefUpdate.Result result = ru.update();
-
- return new RefUpdateState(refSpec.getSource(), result);
}
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationAsyncIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationAsyncIT.java
new file mode 100644
index 0000000..58be58e
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationAsyncIT.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2022 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;
+
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+ name = "pull-replication",
+ sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+ httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
+public class PullReplicationAsyncIT extends PullReplicationIT {
+ @Inject private SitePaths sitePaths;
+
+ @Override
+ public void setUpTestPlugin() throws Exception {
+ FileBasedConfig config =
+ new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
+ config.setString("replication", null, "syncRefs", "^$");
+ config.save();
+
+ super.setUpTestPlugin(true);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index 13b3460..62f42c3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -51,6 +51,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
+import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
@@ -79,11 +80,12 @@
@UseLocalDisk
@TestPlugin(
name = "pull-replication",
- sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule")
+ sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+ httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
public class PullReplicationIT extends LightweightPluginDaemonTest {
private static final Optional<String> ALL_PROJECTS = Optional.empty();
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final int TEST_REPLICATION_DELAY = 60;
+ private static final int TEST_REPLICATION_DELAY = 1;
private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000);
private static final String TEST_REPLICATION_SUFFIX = "suffix1";
private static final String TEST_REPLICATION_REMOTE = "remote1";
@@ -97,10 +99,17 @@
@Override
public void setUpTestPlugin() throws Exception {
+ setUpTestPlugin(false);
+ }
+
+ protected void setUpTestPlugin(boolean loadExisting) throws Exception {
gitPath = sitePaths.site_path.resolve("git");
- config =
- new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
+ File configFile = sitePaths.etc_dir.resolve("replication.config").toFile();
+ config = new FileBasedConfig(configFile, FS.DETECTED);
+ if (loadExisting && configFile.exists()) {
+ config.load();
+ }
setReplicationSource(
TEST_REPLICATION_REMOTE,
TEST_REPLICATION_SUFFIX,
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 4e831bc..48e0e71 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
@@ -17,9 +17,12 @@
import static com.google.common.truth.Truth.assertThat;
import static java.nio.file.Files.createTempDirectory;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -35,6 +38,7 @@
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener.Event;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.events.EventDispatcher;
import com.google.gerrit.server.git.WorkQueue;
@@ -49,6 +53,8 @@
import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import org.apache.http.client.ClientProtocolException;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -78,10 +84,17 @@
@Mock AccountInfo accountInfo;
@Mock RevisionReader revReader;
@Mock RevisionData revisionData;
+ @Mock HttpResult successfulHttpResult;
+ @Mock HttpResult fetchHttpResult;
+ @Mock RevisionData revisionDataWithParents;
+ List<ObjectId> revisionDataParentObjectIds;
@Mock HttpResult httpResult;
+ ApplyObjectMetrics applyObjectMetrics;
+ FetchReplicationMetrics fetchMetrics;
@Captor ArgumentCaptor<String> stringCaptor;
@Captor ArgumentCaptor<Project.NameKey> projectNameKeyCaptor;
+ @Captor ArgumentCaptor<List<RevisionData>> revisionsDataCaptor;
private ExcludedRefsFilter refsFilter;
private ReplicationQueue objectUnderTest;
@@ -102,16 +115,51 @@
when(source.getApis()).thenReturn(apis);
when(sourceCollection.getAll()).thenReturn(Lists.newArrayList(source));
when(rd.get()).thenReturn(sourceCollection);
- when(revReader.read(any(), any(), anyString())).thenReturn(Optional.of(revisionData));
+ lenient()
+ .when(revReader.read(any(), any(), anyString(), eq(0)))
+ .thenReturn(Optional.of(revisionData));
+ lenient().when(revReader.read(any(), anyString(), eq(0))).thenReturn(Optional.of(revisionData));
+ lenient()
+ .when(revReader.read(any(), any(), anyString(), eq(Integer.MAX_VALUE)))
+ .thenReturn(Optional.of(revisionDataWithParents));
+ lenient()
+ .when(revReader.read(any(), anyString(), eq(Integer.MAX_VALUE)))
+ .thenReturn(Optional.of(revisionDataWithParents));
+ revisionDataParentObjectIds =
+ Arrays.asList(
+ ObjectId.fromString("9f8d52853089a3cf00c02ff7bd0817bd4353a95a"),
+ ObjectId.fromString("b5d7bcf1d1c5b0f0726d10a16c8315f06f900bfb"));
+ when(revisionDataWithParents.getParentObjetIds()).thenReturn(revisionDataParentObjectIds);
+
when(fetchClientFactory.create(any())).thenReturn(fetchRestApiClient);
- when(fetchRestApiClient.callSendObject(any(), anyString(), anyBoolean(), any(), any()))
+ lenient()
+ .when(fetchRestApiClient.callSendObject(any(), anyString(), anyBoolean(), any(), any()))
.thenReturn(httpResult);
- when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(httpResult);
+ lenient()
+ .when(fetchRestApiClient.callSendObjects(any(), anyString(), any(), any()))
+ .thenReturn(httpResult);
+ when(fetchRestApiClient.callFetch(any(), anyString(), any(), anyLong()))
+ .thenReturn(fetchHttpResult);
+ when(fetchRestApiClient.initProject(any(), any())).thenReturn(successfulHttpResult);
+ when(successfulHttpResult.isSuccessful()).thenReturn(true);
when(httpResult.isSuccessful()).thenReturn(true);
+ when(fetchHttpResult.isSuccessful()).thenReturn(true);
when(httpResult.isProjectMissing(any())).thenReturn(false);
+ applyObjectMetrics = new ApplyObjectMetrics("pull-replication", new DisabledMetricMaker());
+ fetchMetrics = new FetchReplicationMetrics("pull-replication", new DisabledMetricMaker());
+
objectUnderTest =
- new ReplicationQueue(wq, rd, dis, sl, fetchClientFactory, refsFilter, revReader);
+ new ReplicationQueue(
+ wq,
+ rd,
+ dis,
+ sl,
+ fetchClientFactory,
+ refsFilter,
+ () -> revReader,
+ applyObjectMetrics,
+ fetchMetrics);
}
@Test
@@ -120,7 +168,7 @@
objectUnderTest.start();
objectUnderTest.onGitReferenceUpdated(event);
- verify(fetchRestApiClient).callSendObject(any(), anyString(), eq(false), any(), any());
+ verify(fetchRestApiClient).callSendObjects(any(), anyString(), any(), any());
}
@Test
@@ -155,7 +203,7 @@
objectUnderTest.start();
objectUnderTest.onGitReferenceUpdated(event);
- verify(fetchRestApiClient).callSendObject(any(), anyString(), eq(false), any(), any());
+ verify(fetchRestApiClient).callSendObjects(any(), anyString(), any(), any());
}
@Test
@@ -164,11 +212,11 @@
Event event = new TestEvent("refs/changes/01/1/meta");
objectUnderTest.start();
- when(revReader.read(any(), any(), anyString())).thenThrow(IOException.class);
+ when(revReader.read(any(), any(), anyString(), anyInt())).thenThrow(IOException.class);
objectUnderTest.onGitReferenceUpdated(event);
- verify(fetchRestApiClient).callFetch(any(), anyString(), any());
+ verify(fetchRestApiClient).callFetch(any(), anyString(), any(), anyLong());
}
@Test
@@ -177,27 +225,53 @@
Event event = new TestEvent("refs/changes/01/1/1");
objectUnderTest.start();
- when(revReader.read(any(), any(), anyString())).thenReturn(Optional.empty());
+ when(revReader.read(any(), any(), anyString(), anyInt())).thenReturn(Optional.empty());
objectUnderTest.onGitReferenceUpdated(event);
- verify(fetchRestApiClient).callFetch(any(), anyString(), any());
+ verify(fetchRestApiClient).callFetch(any(), anyString(), any(), anyLong());
}
@Test
public void shouldFallbackToCallFetchWhenParentObjectIsMissing()
throws ClientProtocolException, IOException {
- Event event = new TestEvent("refs/changes/01/1/meta");
+ Event event = new TestEvent("refs/changes/01/1/1");
objectUnderTest.start();
when(httpResult.isSuccessful()).thenReturn(false);
when(httpResult.isParentObjectMissing()).thenReturn(true);
- when(fetchRestApiClient.callSendObject(any(), anyString(), eq(false), any(), any()))
+ when(fetchRestApiClient.callSendObjects(any(), anyString(), any(), any()))
.thenReturn(httpResult);
objectUnderTest.onGitReferenceUpdated(event);
- verify(fetchRestApiClient).callFetch(any(), anyString(), any());
+ verify(fetchRestApiClient).callFetch(any(), anyString(), any(), anyLong());
+ }
+
+ @Test
+ public void shouldFallbackToApplyAllParentObjectsWhenParentObjectIsMissingOnMetaRef()
+ throws ClientProtocolException, IOException {
+ Event event = new TestEvent("refs/changes/01/1/meta");
+ objectUnderTest.start();
+
+ when(httpResult.isSuccessful()).thenReturn(false, true);
+ when(httpResult.isParentObjectMissing()).thenReturn(true, false);
+ when(fetchRestApiClient.callSendObjects(any(), anyString(), any(), any()))
+ .thenReturn(httpResult);
+
+ objectUnderTest.onGitReferenceUpdated(event);
+
+ verify(fetchRestApiClient, times(2))
+ .callSendObjects(any(), anyString(), revisionsDataCaptor.capture(), any());
+ List<List<RevisionData>> revisionsDataValues = revisionsDataCaptor.getAllValues();
+ assertThat(revisionsDataValues).hasSize(2);
+
+ List<RevisionData> firstRevisionsValues = revisionsDataValues.get(0);
+ assertThat(firstRevisionsValues).hasSize(1);
+ assertThat(firstRevisionsValues).contains(revisionData);
+
+ List<RevisionData> secondRevisionsValues = revisionsDataValues.get(1);
+ assertThat(secondRevisionsValues).hasSize(1 + revisionDataParentObjectIds.size());
}
@Test
@@ -242,7 +316,16 @@
refsFilter = new ExcludedRefsFilter(replicationConfig);
objectUnderTest =
- new ReplicationQueue(wq, rd, dis, sl, fetchClientFactory, refsFilter, revReader);
+ new ReplicationQueue(
+ wq,
+ rd,
+ dis,
+ sl,
+ fetchClientFactory,
+ refsFilter,
+ () -> revReader,
+ applyObjectMetrics,
+ fetchMetrics);
Event event = new TestEvent("refs/multi-site/version");
objectUnderTest.onGitReferenceUpdated(event);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java
index e300613..1d8520f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java
@@ -24,12 +24,14 @@
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Change.Id;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.notedb.Sequences;
import com.google.inject.Scopes;
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
@@ -38,11 +40,13 @@
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
import java.io.IOException;
+import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.junit.Before;
import org.junit.Test;
@@ -53,9 +57,12 @@
public class RevisionReaderIT extends LightweightPluginDaemonTest {
RevisionReader objectUnderTest;
+ ReplicationFileBasedConfig replicationConfig;
+
@Before
public void setup() {
objectUnderTest = plugin.getSysInjector().getInstance(RevisionReader.class);
+ replicationConfig = plugin.getSysInjector().getInstance(ReplicationFileBasedConfig.class);
}
@Test
@@ -64,7 +71,7 @@
String refName = RefNames.changeMetaRef(pushResult.getChange().getId());
Optional<RevisionData> revisionDataOption =
- refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId));
+ refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId, 0));
assertThat(revisionDataOption.isPresent()).isTrue();
RevisionData revisionData = revisionDataOption.get();
@@ -80,9 +87,65 @@
assertThat(revisionData.getBlobs()).isEmpty();
}
- private Optional<RevisionData> readRevisionFromObjectUnderTest(String refName, ObjectId objId) {
+ @Test
+ public void shouldReadRefMetaObjectWithMaxNumberOfParents() throws Exception {
+ int numberOfParents = 3;
+ setReplicationConfig(numberOfParents);
+ Result pushResult = createChange();
+ Id changeId = pushResult.getChange().getId();
+ String refName = RefNames.changeMetaRef(pushResult.getChange().getId());
+
+ addMultipleComments(numberOfParents, changeId);
+
+ Optional<RevisionData> revisionDataOption =
+ refObjectId(refName)
+ .flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId, numberOfParents));
+
+ assertThat(revisionDataOption.isPresent()).isTrue();
+ List<ObjectId> parentObjectIds = revisionDataOption.get().getParentObjetIds();
+ assertThat(parentObjectIds).hasSize(numberOfParents);
+ }
+
+ @Test
+ public void shouldReadRefMetaObjectLimitedToMaxNumberOfParents() throws Exception {
+ int numberOfParents = 3;
+ setReplicationConfig(numberOfParents);
+ Result pushResult = createChange();
+ Id changeId = pushResult.getChange().getId();
+ String refName = RefNames.changeMetaRef(pushResult.getChange().getId());
+
+ addMultipleComments(numberOfParents + 1, changeId);
+
+ Optional<RevisionData> revisionDataOption =
+ refObjectId(refName)
+ .flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId, numberOfParents));
+
+ assertThat(revisionDataOption.isPresent()).isTrue();
+ List<ObjectId> parentObjectIds = revisionDataOption.get().getParentObjetIds();
+ assertThat(parentObjectIds).hasSize(numberOfParents);
+ }
+
+ private void addMultipleComments(int numberOfParents, Id changeId) throws RestApiException {
+ for (int i = 0; i < numberOfParents; i++) {
+ addComment(changeId);
+ }
+ }
+
+ private void setReplicationConfig(int numberOfParents) throws IOException {
+ FileBasedConfig config = (FileBasedConfig) replicationConfig.getConfig();
+ config.setInt(
+ "replication", null, RevisionReader.CONFIG_MAX_API_HISTORY_DEPTH, numberOfParents);
+ config.save();
+ }
+
+ private void addComment(Id changeId) throws RestApiException {
+ gApi.changes().id(changeId.get()).current().review(new ReviewInput().message("foo"));
+ }
+
+ private Optional<RevisionData> readRevisionFromObjectUnderTest(
+ String refName, ObjectId objId, int maxParentsDepth) {
try {
- return objectUnderTest.read(project, objId, refName);
+ return objectUnderTest.read(project, objId, refName, maxParentsDepth);
} catch (Exception e) {
throw new IllegalStateException(e);
}
@@ -107,7 +170,7 @@
gApi.changes().id(changeId.get()).current().review(reviewInput);
Optional<RevisionData> revisionDataOption =
- refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId));
+ refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId, 0));
assertThat(revisionDataOption.isPresent()).isTrue();
RevisionData revisionData = revisionDataOption.get();
@@ -131,7 +194,7 @@
createChange().assertOkStatus();
String refName = RefNames.REFS_SEQUENCES + Sequences.NAME_CHANGES;
Optional<RevisionData> revisionDataOption =
- refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId));
+ refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId, 0));
Truth8.assertThat(revisionDataOption).isEmpty();
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
index e6f788a..7cef485 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -149,7 +149,8 @@
protected Optional<RevisionData> createRevisionData(NameKey projectName, String refName)
throws Exception {
try (Repository repository = repoManager.openRepository(projectName)) {
- return revisionReader.read(projectName, repository.exactRef(refName).getObjectId(), refName);
+ return revisionReader.read(
+ projectName, repository.exactRef(refName).getObjectId(), refName, 0);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
index 56e1429..814ba76 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
@@ -35,6 +35,9 @@
import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
@@ -49,11 +52,18 @@
String label = "instance-2-label";
String url = "file:///gerrit-host/instance-1/git/${name}.git";
String refName = "refs/heads/master";
+ String refMetaName = "refs/meta/version";
String location = "http://gerrit-host/a/config/server/tasks/08d173e9";
int taskId = 1234;
+ private String sampleCommitObjectId = "9f8d52853089a3cf00c02ff7bd0817bd4353a95a";
+ private String sampleTreeObjectId = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
+ private String sampleBlobObjectId = "b5d7bcf1d1c5b0f0726d10a16c8315f06f900bfb";
+
private String sampleCommitContent =
- "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n"
+ "tree "
+ + sampleTreeObjectId
+ + "\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"
@@ -92,6 +102,21 @@
assertThat(response.statusCode()).isEqualTo(SC_CREATED);
}
+ @Test
+ public void shouldReturnCreatedResponseCodeForBlob() throws RestApiException {
+ byte[] blobData = "foo".getBytes(StandardCharsets.UTF_8);
+ RevisionInput inputParams =
+ new RevisionInput(
+ label,
+ refMetaName,
+ createSampleRevisionDataBlob(
+ new RevisionObjectData(sampleBlobObjectId, Constants.OBJ_BLOB, blobData)));
+
+ Response<?> response = applyObjectAction.apply(projectResource, inputParams);
+
+ assertThat(response.statusCode()).isEqualTo(SC_CREATED);
+ }
+
@SuppressWarnings("cast")
@Test
public void shouldReturnSourceUrlAndrefNameAsAResponseBody() throws Exception {
@@ -131,8 +156,10 @@
@Test(expected = BadRequestException.class)
public void shouldThrowBadRequestExceptionWhenMissingCommitObjectData() throws Exception {
- RevisionObjectData commitData = new RevisionObjectData(Constants.OBJ_COMMIT, null);
- RevisionObjectData treeData = new RevisionObjectData(Constants.OBJ_TREE, new byte[] {});
+ RevisionObjectData commitData =
+ new RevisionObjectData(sampleCommitObjectId, Constants.OBJ_COMMIT, null);
+ RevisionObjectData treeData =
+ new RevisionObjectData(sampleTreeObjectId, Constants.OBJ_TREE, new byte[] {});
RevisionInput inputParams =
new RevisionInput(label, refName, createSampleRevisionData(commitData, treeData));
@@ -142,7 +169,8 @@
@Test(expected = BadRequestException.class)
public void shouldThrowBadRequestExceptionWhenMissingTreeObject() throws Exception {
RevisionObjectData commitData =
- new RevisionObjectData(Constants.OBJ_COMMIT, sampleCommitContent.getBytes());
+ new RevisionObjectData(
+ sampleCommitObjectId, Constants.OBJ_COMMIT, sampleCommitContent.getBytes());
RevisionInput inputParams =
new RevisionInput(label, refName, createSampleRevisionData(commitData, null));
@@ -175,13 +203,19 @@
private RevisionData createSampleRevisionData() {
RevisionObjectData commitData =
- new RevisionObjectData(Constants.OBJ_COMMIT, sampleCommitContent.getBytes());
- RevisionObjectData treeData = new RevisionObjectData(Constants.OBJ_TREE, new byte[] {});
+ new RevisionObjectData(
+ sampleCommitObjectId, Constants.OBJ_COMMIT, sampleCommitContent.getBytes());
+ RevisionObjectData treeData =
+ new RevisionObjectData(sampleTreeObjectId, Constants.OBJ_TREE, new byte[] {});
return createSampleRevisionData(commitData, treeData);
}
private RevisionData createSampleRevisionData(
RevisionObjectData commitData, RevisionObjectData treeData) {
- return new RevisionData(commitData, treeData, Lists.newArrayList());
+ return new RevisionData(Collections.emptyList(), commitData, treeData, Lists.newArrayList());
+ }
+
+ private RevisionData createSampleRevisionDataBlob(RevisionObjectData blob) {
+ return new RevisionData(Collections.emptyList(), null, null, Arrays.asList(blob));
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java
index 51051c0..d73a6e7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java
@@ -38,6 +38,7 @@
import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
import java.io.IOException;
+import java.util.Collections;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.RefUpdate;
import org.junit.Before;
@@ -55,6 +56,10 @@
private static final NameKey TEST_PROJECT_NAME = Project.nameKey("test-project");
private static final String TEST_REMOTE_NAME = "test-remote-name";
+ private String sampleCommitObjectId = "9f8d52853089a3cf00c02ff7bd0817bd4353a95a";
+ private String sampleTreeObjectId = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
+ private String sampleBlobObjectId = "b5d7bcf1d1c5b0f0726d10a16c8315f06f900bfb";
+
@Mock private PullReplicationStateLogger fetchStateLog;
@Mock private ApplyObject applyObject;
@Mock private ApplyObjectMetrics metrics;
@@ -93,8 +98,10 @@
}
private RevisionData createSampleRevisionData() {
- RevisionObjectData commitData = new RevisionObjectData(Constants.OBJ_COMMIT, new byte[] {});
- RevisionObjectData treeData = new RevisionObjectData(Constants.OBJ_TREE, new byte[] {});
- return new RevisionData(commitData, treeData, Lists.newArrayList());
+ RevisionObjectData commitData =
+ new RevisionObjectData(sampleCommitObjectId, Constants.OBJ_COMMIT, new byte[] {});
+ RevisionObjectData treeData =
+ new RevisionObjectData(sampleTreeObjectId, Constants.OBJ_TREE, new byte[] {});
+ return new RevisionData(Collections.emptyList(), commitData, treeData, Lists.newArrayList());
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
index daf2001..4415a4b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -24,12 +25,22 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
+import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
+import com.google.gerrit.server.permissions.PermissionBackend.WithUser;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.restapi.project.DeleteRef;
import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
import java.util.Optional;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -48,19 +59,42 @@
@Mock private DynamicItem<EventDispatcher> eventDispatcherDataItem;
@Mock private EventDispatcher eventDispatcher;
@Mock private ProjectCache projectCache;
- @Mock private DeleteRef deleteRef;
+ @Mock private ApplyObject applyObject;
@Mock private ProjectState projectState;
+ @Mock private PermissionBackend permissionBackend;
+ @Mock private WithUser currentUser;
+ @Mock private ForProject forProject;
+ @Mock private ForRef forRef;
+ @Mock private LocalDiskRepositoryManager gitManager;
+ @Mock private RefUpdate refUpdate;
+ @Mock private Repository repository;
+ @Mock private Ref currentRef;
+ @Mock private RefDatabase refDb;
@Captor ArgumentCaptor<Event> eventCaptor;
private DeleteRefCommand objectUnderTest;
@Before
- public void setup() {
+ public void setup() throws Exception {
when(eventDispatcherDataItem.get()).thenReturn(eventDispatcher);
when(projectCache.get(any())).thenReturn(Optional.of(projectState));
+ when(permissionBackend.currentUser()).thenReturn(currentUser);
+ when(currentUser.project(any())).thenReturn(forProject);
+ when(forProject.ref(any())).thenReturn(forRef);
+ when(gitManager.openRepository(any())).thenReturn(repository);
+ when(repository.updateRef(any())).thenReturn(refUpdate);
+ when(repository.getRefDatabase()).thenReturn(refDb);
+ when(refDb.exactRef(anyString())).thenReturn(currentRef);
+ when(refUpdate.delete()).thenReturn(Result.FORCED);
objectUnderTest =
- new DeleteRefCommand(fetchStateLog, projectCache, deleteRef, eventDispatcherDataItem);
+ new DeleteRefCommand(
+ fetchStateLog,
+ projectCache,
+ applyObject,
+ permissionBackend,
+ eventDispatcherDataItem,
+ gitManager);
}
@Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
index 8fd4b78..ce0b9d3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
@@ -67,7 +67,7 @@
@Before
public void setup() {
- when(fetchJobFactory.create(any(), any())).thenReturn(fetchJob);
+ when(fetchJobFactory.create(any(), any(), any())).thenReturn(fetchJob);
when(workQueue.getDefaultQueue()).thenReturn(exceutorService);
when(urlFormatter.getRestUrl(anyString())).thenReturn(Optional.of(location));
when(exceutorService.submit(any(Runnable.class)))
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java
index e1ad565..9af2d10 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java
@@ -35,6 +35,7 @@
import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException;
import java.net.URISyntaxException;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -56,6 +57,7 @@
@Mock Source source;
@Mock SourcesCollection sources;
@Mock DynamicItem<EventDispatcher> eventDispatcher;
+ @Mock PullReplicationApiRequestMetrics apiRequestMetrics;
@SuppressWarnings("rawtypes")
@Mock
@@ -76,7 +78,7 @@
when(fetchReplicationStateFactory.create(any())).thenReturn(state);
when(source.getRemoteConfigName()).thenReturn(label);
when(sources.getAll()).thenReturn(Lists.newArrayList(source));
- when(source.schedule(eq(projectName), eq(REF_NAME_TO_FETCH), eq(state), any()))
+ when(source.schedule(eq(projectName), eq(REF_NAME_TO_FETCH), eq(state), any(), any()))
.thenReturn(CompletableFuture.completedFuture(null));
objectUnderTest =
new FetchCommand(fetchReplicationStateFactory, fetchStateLog, sources, eventDispatcher);
@@ -88,16 +90,18 @@
TimeoutException {
objectUnderTest.fetchSync(projectName, label, REF_NAME_TO_FETCH);
- verify(source, times(1)).schedule(projectName, REF_NAME_TO_FETCH, state, SYNC);
+ verify(source, times(1))
+ .schedule(projectName, REF_NAME_TO_FETCH, state, SYNC, Optional.empty());
}
@Test
public void shouldScheduleRefFetchWithDelay()
throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
TimeoutException {
- objectUnderTest.fetchAsync(projectName, label, REF_NAME_TO_FETCH);
+ objectUnderTest.fetchAsync(projectName, label, REF_NAME_TO_FETCH, apiRequestMetrics);
- verify(source, times(1)).schedule(projectName, REF_NAME_TO_FETCH, state, ASYNC);
+ verify(source, times(1))
+ .schedule(projectName, REF_NAME_TO_FETCH, state, ASYNC, Optional.of(apiRequestMetrics));
}
@Test
@@ -106,7 +110,8 @@
TimeoutException {
objectUnderTest.fetchSync(projectName, label, REF_NAME_TO_FETCH);
- verify(source, times(1)).schedule(projectName, REF_NAME_TO_FETCH, state, SYNC);
+ verify(source, times(1))
+ .schedule(projectName, REF_NAME_TO_FETCH, state, SYNC, Optional.empty());
verify(state, times(1)).markAllFetchTasksScheduled();
}
@@ -123,7 +128,8 @@
public void shouldUpdateStateWhenInterruptedException()
throws InterruptedException, ExecutionException, TimeoutException {
when(future.get(anyLong(), eq(TimeUnit.SECONDS))).thenThrow(new InterruptedException());
- when(source.schedule(projectName, REF_NAME_TO_FETCH, state, SYNC)).thenReturn(future);
+ when(source.schedule(projectName, REF_NAME_TO_FETCH, state, SYNC, Optional.empty()))
+ .thenReturn(future);
InterruptedException e =
assertThrows(
@@ -138,7 +144,8 @@
throws InterruptedException, ExecutionException, TimeoutException {
when(future.get(anyLong(), eq(TimeUnit.SECONDS)))
.thenThrow(new ExecutionException(new Exception()));
- when(source.schedule(projectName, REF_NAME_TO_FETCH, state, SYNC)).thenReturn(future);
+ when(source.schedule(projectName, REF_NAME_TO_FETCH, state, SYNC, Optional.empty()))
+ .thenReturn(future);
ExecutionException e =
assertThrows(
@@ -152,7 +159,8 @@
public void shouldUpdateStateWhenTimeoutException()
throws InterruptedException, ExecutionException, TimeoutException {
when(future.get(anyLong(), eq(TimeUnit.SECONDS))).thenThrow(new TimeoutException());
- when(source.schedule(projectName, REF_NAME_TO_FETCH, state, SYNC)).thenReturn(future);
+ when(source.schedule(projectName, REF_NAME_TO_FETCH, state, SYNC, Optional.empty()))
+ .thenReturn(future);
TimeoutException e =
assertThrows(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
index f0b8b99..00904c7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
@@ -38,6 +38,7 @@
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
+import java.util.Collections;
import java.util.Optional;
import org.apache.http.Header;
import org.apache.http.client.ClientProtocolException;
@@ -89,10 +90,22 @@
Header expectedHeader = new BasicHeader("Content-Type", "application/json");
SyncRefsFilter syncRefsFilter;
+ String commitObjectId = "9f8d52853089a3cf00c02ff7bd0817bd4353a95a";
+ String treeObjectId = "77814d216a6cab2ddb9f2877fbbd0febdc0fa608";
+ String blobObjectId = "bb383f5249c68a4cc8c82bdd1228b4a8883ff6e8";
+
String expectedSendObjectPayload =
- "{\"label\":\"Replication\",\"ref_name\":\"refs/heads/master\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"dHJlZSA3NzgxNGQyMTZhNmNhYjJkZGI5ZjI4NzdmYmJkMGZlYmRjMGZhNjA4CnBhcmVudCA5ODNmZjFhM2NmNzQ3MjVhNTNhNWRlYzhkMGMwNjEyMjEyOGY1YThkCmF1dGhvciBHZXJyaXQgVXNlciAxMDAwMDAwIDwxMDAwMDAwQDY5ZWMzOGYwLTM1MGUtNGQ5Yy05NmQ0LWJjOTU2ZjJmYWFhYz4gMTYxMDU3ODY0OCArMDEwMApjb21taXR0ZXIgR2Vycml0IENvZGUgUmV2aWV3IDxyb290QG1hY3plY2gtWFBTLTE1PiAxNjEwNTc4NjQ4ICswMTAwCgpVcGRhdGUgcGF0Y2ggc2V0IDEKClBhdGNoIFNldCAxOgoKKDEgY29tbWVudCkKClBhdGNoLXNldDogMQo\\u003d\"},\"tree_object\":{\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}}";
+ "{\"label\":\"Replication\",\"ref_name\":\"refs/heads/master\",\"revision_data\":{\"commit_object\":{\"sha1\":\""
+ + commitObjectId
+ + "\",\"type\":1,\"content\":\"dHJlZSA3NzgxNGQyMTZhNmNhYjJkZGI5ZjI4NzdmYmJkMGZlYmRjMGZhNjA4CnBhcmVudCA5ODNmZjFhM2NmNzQ3MjVhNTNhNWRlYzhkMGMwNjEyMjEyOGY1YThkCmF1dGhvciBHZXJyaXQgVXNlciAxMDAwMDAwIDwxMDAwMDAwQDY5ZWMzOGYwLTM1MGUtNGQ5Yy05NmQ0LWJjOTU2ZjJmYWFhYz4gMTYxMDU3ODY0OCArMDEwMApjb21taXR0ZXIgR2Vycml0IENvZGUgUmV2aWV3IDxyb290QG1hY3plY2gtWFBTLTE1PiAxNjEwNTc4NjQ4ICswMTAwCgpVcGRhdGUgcGF0Y2ggc2V0IDEKClBhdGNoIFNldCAxOgoKKDEgY29tbWVudCkKClBhdGNoLXNldDogMQo\\u003d\"},\"tree_object\":{\"sha1\":\""
+ + treeObjectId
+ + "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\""
+ + blobObjectId
+ + "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}}";
String commitObject =
- "tree 77814d216a6cab2ddb9f2877fbbd0febdc0fa608\n"
+ "tree "
+ + treeObjectId
+ + "\n"
+ "parent 983ff1a3cf74725a53a5dec8d0c06122128f5a8d\n"
+ "author Gerrit User 1000000 <1000000@69ec38f0-350e-4d9c-96d4-bc956f2faaac> 1610578648 +0100\n"
+ "committer Gerrit Code Review <root@maczech-XPS-15> 1610578648 +0100\n"
@@ -105,7 +118,7 @@
+ "\n"
+ "Patch-set: 1\n";
String treeObject =
- "100644 blob bb383f5249c68a4cc8c82bdd1228b4a8883ff6e8 f75a69004a93b4ccc8ce215c12808636c2b75675";
+ "100644 blob " + blobObjectId + " f75a69004a93b4ccc8ce215c12808636c2b75675";
String blobObject =
"{\n"
+ " \"comments\": [\n"
@@ -460,9 +473,12 @@
private RevisionData createSampleRevisionData() {
RevisionObjectData commitData =
- new RevisionObjectData(Constants.OBJ_COMMIT, commitObject.getBytes());
- RevisionObjectData treeData = new RevisionObjectData(Constants.OBJ_TREE, treeObject.getBytes());
- RevisionObjectData blobData = new RevisionObjectData(Constants.OBJ_BLOB, blobObject.getBytes());
- return new RevisionData(commitData, treeData, Lists.newArrayList(blobData));
+ new RevisionObjectData(commitObjectId, Constants.OBJ_COMMIT, commitObject.getBytes());
+ RevisionObjectData treeData =
+ new RevisionObjectData(treeObjectId, Constants.OBJ_TREE, treeObject.getBytes());
+ RevisionObjectData blobData =
+ new RevisionObjectData(blobObjectId, Constants.OBJ_BLOB, blobObject.getBytes());
+ return new RevisionData(
+ Collections.emptyList(), commitData, treeData, Lists.newArrayList(blobData));
}
}
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
new file mode 100644
index 0000000..d82f3a5
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResultTest.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2022 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.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import java.util.Optional;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class HttpResultTest {
+
+ @Parameterized.Parameters(name = "HTTP Status = {0} is successful: {1}")
+ 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}
+ });
+ }
+
+ private Integer httpStatus;
+ private boolean isSuccessful;
+
+ public HttpResultTest(Integer httpStatus, Boolean isSuccessful) {
+ this.httpStatus = httpStatus;
+ this.isSuccessful = isSuccessful;
+ }
+
+ @Test
+ public void httpResultIsSuccessful() {
+ HttpResult httpResult = new HttpResult(httpStatus, Optional.empty());
+ assertThat(httpResult.isSuccessful()).isEqualTo(isSuccessful);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
index 655575f..c673011 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
@@ -34,6 +34,7 @@
import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction;
+import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
@@ -58,16 +59,18 @@
@Mock private FetchJob fetchJob;
@Mock private FetchJob.Factory fetchJobFactory;
@Captor ArgumentCaptor<Input> inputCaptor;
+ @Mock private PullReplicationApiRequestMetrics metrics;
private StreamEventListener objectUnderTest;
@Before
public void setup() {
when(workQueue.getDefaultQueue()).thenReturn(executor);
- when(fetchJobFactory.create(eq(Project.nameKey(TEST_PROJECT)), any())).thenReturn(fetchJob);
+ when(fetchJobFactory.create(eq(Project.nameKey(TEST_PROJECT)), any(), any()))
+ .thenReturn(fetchJob);
objectUnderTest =
new StreamEventListener(
- INSTANCE_ID, projectInitializationAction, workQueue, fetchJobFactory);
+ INSTANCE_ID, projectInitializationAction, workQueue, fetchJobFactory, () -> metrics);
}
@Test
@@ -107,7 +110,7 @@
objectUnderTest.onEvent(event);
- verify(fetchJobFactory).create(eq(Project.nameKey(TEST_PROJECT)), inputCaptor.capture());
+ verify(fetchJobFactory).create(eq(Project.nameKey(TEST_PROJECT)), inputCaptor.capture(), any());
Input input = inputCaptor.getValue();
assertThat(input.label).isEqualTo(REMOTE_INSTANCE_ID);
@@ -136,7 +139,7 @@
objectUnderTest.onEvent(event);
- verify(fetchJobFactory).create(eq(Project.nameKey(TEST_PROJECT)), inputCaptor.capture());
+ verify(fetchJobFactory).create(eq(Project.nameKey(TEST_PROJECT)), inputCaptor.capture(), any());
Input input = inputCaptor.getValue();
assertThat(input.label).isEqualTo(REMOTE_INSTANCE_ID);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
index e2a162e..161830b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
@@ -79,14 +79,36 @@
Optional<RevisionData> revisionData =
reader.read(
- Project.nameKey(testRepoProjectName), pushResult.getCommit().toObjectId(), refName);
+ Project.nameKey(testRepoProjectName), pushResult.getCommit().toObjectId(), refName, 0);
RefSpec refSpec = new RefSpec(refName);
- objectUnderTest.apply(project, refSpec, revisionData.get());
+ objectUnderTest.apply(project, refSpec, toArray(revisionData));
try (Repository repo = repoManager.openRepository(project);
TestRepository<Repository> testRepo = new TestRepository<>(repo); ) {
Optional<RevisionData> newRevisionData =
- reader.read(project, repo.exactRef(refName).getObjectId(), refName);
+ reader.read(project, repo.exactRef(refName).getObjectId(), refName, 0);
+ compareObjects(revisionData.get(), newRevisionData);
+ testRepo.fsck();
+ }
+ }
+
+ @Test
+ public void shouldApplyRefSequencesChanges() throws Exception {
+ String testRepoProjectName = project + TEST_REPLICATION_SUFFIX;
+ testRepo = cloneProject(createTestProject(testRepoProjectName));
+
+ createChange();
+ String seqChangesRef = RefNames.REFS_SEQUENCES + "changes";
+
+ Optional<RevisionData> revisionData = reader.read(allProjects, seqChangesRef, 0);
+
+ RefSpec refSpec = new RefSpec(seqChangesRef);
+ objectUnderTest.apply(project, refSpec, toArray(revisionData));
+ try (Repository repo = repoManager.openRepository(project);
+ TestRepository<Repository> testRepo = new TestRepository<>(repo); ) {
+
+ Optional<RevisionData> newRevisionData =
+ reader.read(project, repo.exactRef(seqChangesRef).getObjectId(), seqChangesRef, 0);
compareObjects(revisionData.get(), newRevisionData);
testRepo.fsck();
}
@@ -105,8 +127,8 @@
NameKey testRepoKey = Project.nameKey(testRepoProjectName);
try (Repository repo = repoManager.openRepository(testRepoKey)) {
Optional<RevisionData> revisionData =
- reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName);
- objectUnderTest.apply(project, refSpec, revisionData.get());
+ reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName, 0);
+ objectUnderTest.apply(project, refSpec, toArray(revisionData));
}
ReviewInput reviewInput = new ReviewInput();
@@ -117,12 +139,12 @@
try (Repository repo = repoManager.openRepository(project);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
Optional<RevisionData> revisionDataWithComment =
- reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName);
+ reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName, 0);
- objectUnderTest.apply(project, refSpec, revisionDataWithComment.get());
+ objectUnderTest.apply(project, refSpec, toArray(revisionDataWithComment));
Optional<RevisionData> newRevisionData =
- reader.read(project, repo.exactRef(refName).getObjectId(), refName);
+ reader.read(project, repo.exactRef(refName).getObjectId(), refName, 0);
compareObjects(revisionDataWithComment.get(), newRevisionData);
@@ -147,12 +169,12 @@
gApi.changes().id(changeId.get()).current().review(reviewInput);
Optional<RevisionData> revisionData =
- reader.read(createTestProject, repo.exactRef(refName).getObjectId(), refName);
+ reader.read(createTestProject, repo.exactRef(refName).getObjectId(), refName, 0);
RefSpec refSpec = new RefSpec(refName);
assertThrows(
MissingParentObjectException.class,
- () -> objectUnderTest.apply(project, refSpec, revisionData.get()));
+ () -> objectUnderTest.apply(project, refSpec, toArray(revisionData)));
}
}
@@ -173,6 +195,9 @@
}
private void compareContent(RevisionObjectData expected, RevisionObjectData actual) {
+ if (expected == actual) {
+ return;
+ }
assertThat(actual.getType()).isEqualTo(expected.getType());
assertThat(Bytes.asList(actual.getContent()))
.containsExactlyElementsIn(Bytes.asList(expected.getContent()))
@@ -205,4 +230,10 @@
bind(ApplyObject.class);
}
}
+
+ private RevisionData[] toArray(Optional<RevisionData> optional) {
+ ImmutableList.Builder<RevisionData> listBuilder = ImmutableList.builder();
+ optional.ifPresent(listBuilder::add);
+ return listBuilder.build().toArray(new RevisionData[1]);
+ }
}