Merge branch 'stable-3.5' into stable-3.6 * stable-3.5: Do not notify failures on collisions Fixed erroneous increment of the rescheduled metric Ensure failures are only incremented in RunnableWithMetrics Record replication metrics for retrying tasks Report deletions as succeeded in the fetch-ref-replicated events NOOP fetches are to be considered succeded Properly propagate failure when ref deletion fails Accept remotes without `fetch` option on primary Pull-replication plugin should warn about inconsistent timeouts Remove events from beforeStartupEventsQueue after fire() Fix ordering of parameters for apply objects log Respect `remote.mirror` option in DeleteRefCommand Remove events from beforeStartupEventsQueue in-memory queue Reverse the order of storing commit, tree and blobs Do not honor gerrit.replicateOnStartup on primary side Do not record fetch replication metrics when all refs are excluded from fetch task Don't require remote.<name>.url on primary side Use GJF 1.7 for code-style verification Change-Id: I0cb49e9761e5783a024899db00078ca75338e77a
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 560f8ee..df39d5a 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
@@ -88,6 +88,7 @@ private final Set<TransportException> fetchFailures = Sets.newHashSetWithExpectedSize(4); private boolean fetchAllRefs; private Repository git; + private boolean isCollision; private boolean retrying; private int retryCount; private final int maxRetries; @@ -259,7 +260,7 @@ } private void statesCleanUp() { - if (!stateMap.isEmpty() && !isRetrying()) { + if (!stateMap.isEmpty() && !isRetrying() && !isCollision) { for (Map.Entry<String, ReplicationState> entry : stateMap.entries()) { entry .getValue() @@ -315,6 +316,7 @@ // we start replication (instead a new instance, with the same URI, is // created and scheduled for a future point in time.) // + isCollision = false; if (replicationType == ReplicationType.ASYNC && !pool.requestRunway(this)) { if (!canceled) { repLog.info( @@ -323,6 +325,7 @@ uri, pool.getInFlight(getURI()).map(FetchOne::getTaskIdHex).orElse("<unknown>")); pool.reschedule(this, Source.RetryReason.COLLISION); + isCollision = true; } return; } @@ -356,8 +359,9 @@ .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{}", taskIdHex, + replicationType, uri, elapsed, delay, @@ -501,6 +505,11 @@ boolean anyRefFailed = false; RefUpdate.Result lastRefUpdateResult = RefUpdate.Result.NO_CHANGE; + // NOOP fetches are considered as successes + if (refUpdates.isEmpty()) { + succeeded = true; + } + for (RefUpdateState u : refUpdates) { ReplicationState.RefFetchResult fetchStatus = ReplicationState.RefFetchResult.SUCCEEDED; Set<ReplicationState> logStates = new HashSet<>();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefReplicatedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefReplicatedEvent.java index 0eabf42..2c18983 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefReplicatedEvent.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefReplicatedEvent.java
@@ -14,6 +14,7 @@ package com.googlesource.gerrit.plugins.replication.pull; +import com.google.common.annotations.VisibleForTesting; import com.google.gerrit.entities.Project; import com.googlesource.gerrit.plugins.replication.events.RemoteRefReplicationEvent; import java.util.Objects; @@ -74,4 +75,9 @@ public String getRefName() { return ref; } + + @VisibleForTesting + public RefUpdate.Result getRefUpdateResult() { + return refUpdateResult; + } }
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 ef95596..ef1c2fd 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
@@ -547,13 +547,14 @@ private void fireBeforeStartupEvents() { Set<String> eventsReplayed = new HashSet<>(); ReferenceUpdatedEvent event; - while ((event = beforeStartupEventsQueue.poll()) != null) { + while ((event = beforeStartupEventsQueue.peek()) != null) { String eventKey = String.format("%s:%s", event.projectName(), event.refName()); if (!eventsReplayed.contains(eventKey)) { repLog.info("Firing pending task {}", event); fire(event); eventsReplayed.add(eventKey); } + beforeStartupEventsQueue.remove(event); } }
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 012a046..cc33ce9 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
@@ -628,8 +628,6 @@ fetchOp.getTaskIdHex(), fetchOp.getURI(), pendingFetchOp.getTaskIdHex()), fetchOp.getStatesAsArray()); - queueMetrics.incrementTaskRescheduled(this); - } else { // The one pending is one that is NOT retrying, it was just // scheduled believing no problem would happen. The one pending @@ -654,9 +652,8 @@ "[%s] Merging the pending fetch from [%s] with task [%s] and rescheduling", pendingFetchOp.getTaskIdHex(), pendingFetchOp.getURI(), fetchOp.getTaskIdHex()), pendingFetchOp.getStatesAsArray()); - - queueMetrics.incrementTaskMerged(this); } + queueMetrics.incrementTaskMerged(this); } if (pendingFetchOp == null || !pendingFetchOp.isRetrying()) { @@ -677,11 +674,13 @@ ? RefUpdate.Result.NOT_ATTEMPTED : RefUpdate.Result.REJECTED_OTHER_REASON; postReplicationFailedEvent(fetchOp, trackingRefUpdate); - queueMetrics.incrementTaskFailed(this); if (fetchOp.setToRetry()) { postReplicationScheduledEvent(fetchOp); - pool.schedule(fetchOp, config.getRetryDelay(), TimeUnit.MINUTES); + pool.schedule( + queueMetrics.runWithMetrics(this, fetchOp), + config.getRetryDelay(), + TimeUnit.MINUTES); queueMetrics.incrementTaskRetrying(this); } else { fetchOp.canceledByReplication(); @@ -906,6 +905,10 @@ return config.replicateProjectDeletions(); } + public boolean isMirror() { + return config.getRemoteConfig().isMirror(); + } + void scheduleUpdateHead(String apiUrl, Project.NameKey project, String newHead) { try { URIish apiURI = new URIish(apiUrl);
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 d7ae063..67ac6f1 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
@@ -79,14 +79,14 @@ return sourceConfigs.build(); } - private static List<RemoteConfig> allFetchRemotes(Config cfg) throws ConfigInvalidException { + private List<RemoteConfig> allFetchRemotes(Config cfg) throws ConfigInvalidException { Set<String> names = cfg.getSubsections("remote"); List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size()); for (String name : names) { try { final RemoteConfig remoteConfig = new RemoteConfig(cfg, name); - if (!remoteConfig.getFetchRefSpecs().isEmpty()) { + if (!isReplica || !remoteConfig.getFetchRefSpecs().isEmpty()) { result.add(remoteConfig); } else { logger.atFine().log(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java index 0e840bd..b6647bd 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
@@ -16,6 +16,7 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; import com.google.gerrit.server.config.ConfigUtil; import com.googlesource.gerrit.plugins.replication.RemoteConfiguration; import java.util.concurrent.TimeUnit; @@ -23,6 +24,7 @@ import org.eclipse.jgit.transport.RemoteConfig; public class SourceConfiguration implements RemoteConfiguration { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); static final int DEFAULT_REPLICATION_DELAY = 4; static final int DEFAULT_RESCHEDULE_DELAY = 3; static final int DEFAULT_SLOW_LATENCY_THRESHOLD_SECS = 900; @@ -64,6 +66,13 @@ apis = ImmutableList.copyOf(cfg.getStringList("remote", name, "apiUrl")); connectionTimeout = cfg.getInt("remote", name, "connectionTimeout", DEFAULT_CONNECTION_TIMEOUT_MS); + int connectionTimeoutInSec = connectionTimeout / 1000; + if (connectionTimeoutInSec < getRemoteConfig().getTimeout()) { + logger.atWarning().log( + "The connection timeout is currently set to %s sec, which is less than the timeout value of %s sec. " + + "To avoid potential issues, consider increasing the connection timeout to exceed the timeout value.", + connectionTimeoutInSec, getRemoteConfig().getTimeout()); + } idleTimeout = cfg.getInt("remote", name, "idleTimeout", DEFAULT_MAX_CONNECTION_INACTIVITY_MS); maxConnectionsPerRoute = cfg.getInt("replication", "maxConnectionsPerRoute", DEFAULT_CONNECTIONS_PER_ROUTE);
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 e49c8b6..5192351 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
@@ -35,6 +35,7 @@ import com.googlesource.gerrit.plugins.replication.pull.ReplicationState; import com.googlesource.gerrit.plugins.replication.pull.Source; import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection; +import com.googlesource.gerrit.plugins.replication.pull.api.exception.DeleteRefException; import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState; import java.io.IOException; import java.util.Optional; @@ -72,6 +73,18 @@ public void deleteRef(Project.NameKey name, String refName, String sourceLabel) throws IOException, RestApiException { + Source source = + sourcesCollection + .getByRemoteName(sourceLabel) + .orElseThrow( + () -> + new IllegalStateException( + String.format("Could not find URI for %s remote", sourceLabel))); + if (!source.isMirror()) { + repLog.info( + "Ignoring ref {} deletion from project {}, as mirror option is false", refName, name); + return; + } try { repLog.info("Delete ref from {} for project {}, ref name {}", sourceLabel, name, refName); Optional<ProjectState> projectState = projectCache.get(name); @@ -85,19 +98,12 @@ return; } - Source source = - sourcesCollection - .getByRemoteName(sourceLabel) - .orElseThrow( - () -> - new IllegalStateException( - String.format("Could not find URI for %s remote", sourceLabel))); URIish sourceUri = source.getURI(name); try { Context.setLocalEvent(true); - deleteRef(name, ref.get()); + RefUpdate.Result successResult = ensureSuccess(deleteRef(name, ref.get())); eventDispatcher .get() @@ -107,13 +113,17 @@ refName, sourceUri, ReplicationState.RefFetchResult.SUCCEEDED, - RefUpdate.Result.FORCED)); + successResult)); } catch (PermissionBackendException e) { logger.atSevere().withCause(e).log( "Unexpected error while trying to delete ref '%s' on project %s and notifying it", refName, name); throw RestApiException.wrap(e.getMessage(), e); } catch (IOException e) { + RefUpdate.Result refUpdateResult = + e instanceof DeleteRefException + ? ((DeleteRefException) e).getResult() + : RefUpdate.Result.LOCK_FAILURE; eventDispatcher .get() .postEvent( @@ -122,7 +132,7 @@ refName, sourceUri, ReplicationState.RefFetchResult.FAILED, - RefUpdate.Result.LOCK_FAILURE)); + refUpdateResult)); String message = String.format( "RefUpdate lock failure for: sourceLabel=%s, project=%s, refName=%s", @@ -158,7 +168,22 @@ u.setForceUpdate(true); result = u.delete(); - return new RefUpdateState(ref.getName(), result); + return new RefUpdateState(":" + ref.getName(), result); } } + + private static RefUpdate.Result ensureSuccess(RefUpdateState refUpdateState) + throws DeleteRefException { + switch (refUpdateState.getResult()) { + case NOT_ATTEMPTED: + case REJECTED: + case REJECTED_CURRENT_BRANCH: + case REJECTED_MISSING_OBJECT: + case LOCK_FAILURE: + case IO_FAILURE: + case REJECTED_OTHER_REASON: + throw new DeleteRefException("Failed ref deletion", refUpdateState.getResult()); + } + return refUpdateState.getResult(); + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/DeleteRefException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/DeleteRefException.java new file mode 100644 index 0000000..a1d6876 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/DeleteRefException.java
@@ -0,0 +1,33 @@ +// Copyright (C) 2024 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.exception; + +import java.io.IOException; +import org.eclipse.jgit.lib.RefUpdate; + +public class DeleteRefException extends IOException { + + private static final long serialVersionUID = 1L; + private final RefUpdate.Result result; + + public DeleteRefException(String msg, RefUpdate.Result result) { + super(msg); + this.result = result; + } + + public RefUpdate.Result getResult() { + return result; + } +}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 61d62b4..bffca58 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -439,6 +439,10 @@ when `replicatePermissions` is true, even if the push refspec is 'all refs'. +> NOTE: When using the pull-replication and replication plugins together, +> **NOT having** a `fetch` configuration for a remote, will also enable +> the standard _push_ replication for that remote. + [2]: #example_file remote.NAME.timeout
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 fc1b02c..e238528 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
@@ -15,10 +15,12 @@ package com.googlesource.gerrit.plugins.replication.pull.api; import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.gerrit.entities.Project; @@ -36,9 +38,11 @@ import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent; import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider; import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger; +import com.googlesource.gerrit.plugins.replication.pull.ReplicationState; import com.googlesource.gerrit.plugins.replication.pull.Source; import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection; import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject; +import java.io.IOException; import java.util.Optional; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; @@ -109,22 +113,53 @@ @Test public void shouldSendEventWhenDeletingRef() throws Exception { + when(source.isMirror()).thenReturn(true); + objectUnderTest.deleteRef(TEST_PROJECT_NAME, TEST_REF_NAME, TEST_SOURCE_LABEL); + assertFetchReplicatedEvent(ReplicationState.RefFetchResult.SUCCEEDED, Result.FORCED); + } + + @Test + public void shouldNotSendNotSendEventWhenMirroringIsDisabled() throws Exception { + when(source.isMirror()).thenReturn(false); + + objectUnderTest.deleteRef(TEST_PROJECT_NAME, TEST_REF_NAME, TEST_SOURCE_LABEL); + + verifyNoInteractions(eventDispatcher); + } + + @Test + public void shouldHandleNonExistingRef() throws Exception { + when(source.isMirror()).thenReturn(true); + when(refDb.exactRef(anyString())).thenReturn(null); + + objectUnderTest.deleteRef(TEST_PROJECT_NAME, NON_EXISTING_REF_NAME, TEST_SOURCE_LABEL); + + verify(eventDispatcher, never()).postEvent(any()); + } + + @Test + public void shouldThrowWhenRefDeletionFails() throws Exception { + when(source.isMirror()).thenReturn(true); + when(refUpdate.delete()).thenReturn(Result.LOCK_FAILURE); + + assertThrows( + IOException.class, + () -> objectUnderTest.deleteRef(TEST_PROJECT_NAME, TEST_REF_NAME, TEST_SOURCE_LABEL)); + + assertFetchReplicatedEvent(ReplicationState.RefFetchResult.FAILED, Result.LOCK_FAILURE); + } + + private void assertFetchReplicatedEvent( + ReplicationState.RefFetchResult refFetchResult, RefUpdate.Result result) throws Exception { verify(eventDispatcher).postEvent(eventCaptor.capture()); Event sentEvent = eventCaptor.getValue(); assertThat(sentEvent).isInstanceOf(FetchRefReplicatedEvent.class); FetchRefReplicatedEvent fetchEvent = (FetchRefReplicatedEvent) sentEvent; assertThat(fetchEvent.getProjectNameKey()).isEqualTo(TEST_PROJECT_NAME); assertThat(fetchEvent.getRefName()).isEqualTo(TEST_REF_NAME); - } - - @Test - public void shouldHandleNonExistingRef() throws Exception { - when(refDb.exactRef(anyString())).thenReturn(null); - - objectUnderTest.deleteRef(TEST_PROJECT_NAME, NON_EXISTING_REF_NAME, TEST_SOURCE_LABEL); - - verify(eventDispatcher, never()).postEvent(any()); + assertThat(fetchEvent.getStatus()).isEqualTo(refFetchResult.toString()); + assertThat(fetchEvent.getRefUpdateResult()).isEqualTo(result); } }