Merge branch 'stable-3.11'
* stable-3.11:
Prevent the synchronous replication of create-refs via apply-object
Flag ref-updates events creating a new ref
Fix the create/delete of ref-specs in FetchOne delta
Report deletions as succeeded in the fetch-ref-replicated events
Change-Id: I65c2b8e161dd5b165fd2929c6861aa34e8d71d35
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 5e7fda2..939a05b 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
@@ -228,14 +228,16 @@
// set may risk to have both update and delete for the same
// ref, which would be an issue: the delta is an unordered set
// and executing updates and deletes for the same ref may have
- // unexpected results, depending on which one you execute firt.
- //
- // Delete any previous ref-spec as delete or update for the same
- // refname so that the subsequent addition will make sure that
- // there is only one operation per refName.
- delta.remove(ref);
+ // unexpected results, depending on which one you execute first.
+
+ // Delete any previous ref-spec as delete
+ delta.remove(FetchRefSpec.fromRef(":" + ref.refName()));
+
+ // Delete any previous ref-spec as update
delta.remove(FetchRefSpec.fromRef(ref.refName()));
+ // Add the new ref as-is for making sure that
+ // there is only one operation per refName.
delta.add(ref);
repLog.trace("[{}] Added ref {} for replication from {}", taskIdHex, ref, uri);
}
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 495915d..b94189e 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
@@ -48,6 +48,7 @@
import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
import com.googlesource.gerrit.plugins.replication.pull.client.HttpResult;
import com.googlesource.gerrit.plugins.replication.pull.client.HttpResultUtils;
+import com.googlesource.gerrit.plugins.replication.pull.filter.ApplyObjectBannedCreateRefsFilter;
import com.googlesource.gerrit.plugins.replication.pull.filter.ApplyObjectsRefsFilter;
import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
import java.io.IOException;
@@ -107,6 +108,7 @@
private final String instanceId;
private final boolean useBatchUpdateEvents;
private ApplyObjectsRefsFilter applyObjectsRefsFilter;
+ private final ApplyObjectBannedCreateRefsFilter applyObjectsBannedCreateRefsFilter;
@Inject
ReplicationQueue(
@@ -122,6 +124,7 @@
@GerritInstanceId String instanceId,
@GerritServerConfig Config gerritConfig,
ApplyObjectsRefsFilter applyObjectsRefsFilter,
+ ApplyObjectBannedCreateRefsFilter applyObjectsBannedCreateRefsFilter,
ShutdownState shutdownState) {
workQueue = wq;
dispatcher = dis;
@@ -138,6 +141,7 @@
this.useBatchUpdateEvents =
gerritConfig.getBoolean("event", "stream-events", "enableBatchRefUpdatedEvents", false);
this.applyObjectsRefsFilter = applyObjectsRefsFilter;
+ this.applyObjectsBannedCreateRefsFilter = applyObjectsBannedCreateRefsFilter;
}
@Override
@@ -367,8 +371,9 @@
.map(ref -> toBatchApplyObject(project, ref, state))
.collect(Collectors.toList());
- if (!containsLargeOrDeletedRefs(refsBatch)) {
- return ((source) -> callBatchSendObject(source, project, refsBatch, eventCreatedOn, state));
+ if (!containsLargeOrDeletedRefs(refsBatch)
+ && !hasCreateRefsBannedFromApplyObject(refsBatch)) {
+ return (source -> callBatchSendObject(source, project, refsBatch, eventCreatedOn, state));
}
} catch (UncheckedIOException e) {
stateLog.error("Falling back to calling fetch", e, state);
@@ -381,7 +386,8 @@
try {
Optional<RevisionData> maybeRevisionData =
revReaderProvider.get().read(project, event.objectId(), event.refName(), 0);
- return BatchApplyObjectData.create(event.refName(), maybeRevisionData);
+ return BatchApplyObjectData.create(
+ event.refName(), maybeRevisionData, event.isDelete(), event.isCreate());
} catch (IOException e) {
stateLog.error(
String.format(
@@ -397,6 +403,13 @@
return batchApplyObjectData.stream().anyMatch(e -> e.revisionData().isEmpty());
}
+ private boolean hasCreateRefsBannedFromApplyObject(
+ List<BatchApplyObjectData> batchApplyObjectData) {
+ return batchApplyObjectData.stream()
+ .filter(BatchApplyObjectData::isCreate)
+ .anyMatch(e -> applyObjectsBannedCreateRefsFilter.match(e.refName()));
+ }
+
private Optional<HttpResult> callSendObject(
FetchApiClient fetchClient,
String remoteName,
@@ -820,9 +833,10 @@
String refName,
ObjectId objectId,
long eventCreatedOn,
- boolean isDelete) {
+ boolean isDelete,
+ boolean isCreate) {
return new AutoValue_ReplicationQueue_ReferenceUpdatedEvent(
- projectName, refName, objectId, eventCreatedOn, isDelete);
+ projectName, refName, objectId, eventCreatedOn, isDelete, isCreate);
}
static ReferenceUpdatedEvent from(RefUpdateAttribute refUpdateAttribute, long eventCreatedOn) {
@@ -831,7 +845,8 @@
refUpdateAttribute.refName,
ObjectId.fromString(refUpdateAttribute.newRev),
eventCreatedOn,
- ZEROS_OBJECTID.equals(refUpdateAttribute.newRev));
+ ZEROS_OBJECTID.equals(refUpdateAttribute.newRev),
+ ZEROS_OBJECTID.equals(refUpdateAttribute.oldRev));
}
public abstract String projectName();
@@ -843,6 +858,8 @@
public abstract long eventCreatedOn();
public abstract boolean isDelete();
+
+ public abstract boolean isCreate();
}
@FunctionalInterface
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 fe558e5..c3f91d6 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
@@ -73,7 +73,7 @@
name.get(),
r,
sourceLabel);
- return new RefUpdateState(r, RefUpdate.Result.IO_FAILURE);
+ return new RefUpdateState(":" + r, RefUpdate.Result.IO_FAILURE);
}
})
.toList();
@@ -95,7 +95,7 @@
taskIdRef,
refName,
name);
- return new RefUpdateState(refName, RefUpdate.Result.NO_CHANGE);
+ return new RefUpdateState(":" + refName, RefUpdate.Result.NO_CHANGE);
}
repLog.info(
@@ -112,7 +112,7 @@
Optional<Ref> ref = getRef(name, refName);
if (!ref.isPresent()) {
logger.atFine().log("[%s] Ref %s was not found in project %s", taskIdRef, refName, name);
- return new RefUpdateState(refName, RefUpdate.Result.NO_CHANGE);
+ return new RefUpdateState(":" + refName, RefUpdate.Result.NO_CHANGE);
}
RefUpdateState deleteResult = deleteRef(name, ref.get());
@@ -149,7 +149,7 @@
u.setForceUpdate(true);
result = u.delete();
- return new RefUpdateState(ref.getName(), result);
+ return new RefUpdateState(":" + ref.getName(), result);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java
index 5b17002..95ba9a9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/data/BatchApplyObjectData.java
@@ -20,9 +20,24 @@
@AutoValue
public abstract class BatchApplyObjectData {
- public static BatchApplyObjectData create(String refName, Optional<RevisionData> revisionData)
+ public static BatchApplyObjectData create(
+ String refName, Optional<RevisionData> revisionData, boolean isDelete, boolean isCreate)
throws IllegalArgumentException {
- return new AutoValue_BatchApplyObjectData(refName, revisionData, false);
+ return new AutoValue_BatchApplyObjectData(refName, revisionData, isDelete, isCreate);
+ }
+
+ public static BatchApplyObjectData newDeleteRef(String refName) throws IllegalArgumentException {
+ return create(refName, Optional.empty(), true, false);
+ }
+
+ public static BatchApplyObjectData newUpdateRef(
+ String refName, Optional<RevisionData> revisionData) throws IllegalArgumentException {
+ return create(refName, revisionData, false, false);
+ }
+
+ public static BatchApplyObjectData newCreateRef(
+ String refName, Optional<RevisionData> revisionData) throws IllegalArgumentException {
+ return create(refName, revisionData, false, true);
}
public abstract String refName();
@@ -31,6 +46,8 @@
public abstract boolean isDelete();
+ public abstract boolean isCreate();
+
@Override
public String toString() {
return String.format(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ApplyObjectBannedCreateRefsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ApplyObjectBannedCreateRefsFilter.java
new file mode 100644
index 0000000..d7c685a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ApplyObjectBannedCreateRefsFilter.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.filter;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.api.ReplicationConfig;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class ApplyObjectBannedCreateRefsFilter extends RefsFilter {
+ @Inject
+ public ApplyObjectBannedCreateRefsFilter(ReplicationConfig replicationConfig) {
+ super(replicationConfig);
+ }
+
+ @Override
+ protected List<String> getRefNamePatterns(Config cfg) {
+ String[] applyObjectBannedCreateRefs =
+ cfg.getStringList("replication", null, "applyObjectBannedCreateRefs");
+ return ImmutableList.copyOf(applyObjectBannedCreateRefs);
+ }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 546df2e..da350ad 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -132,6 +132,33 @@
: If true, the default fetch refspec will be set to use forced update to
the local repository when no refspec is given. By default, false.
+
+replication.applyObjectBannedCreateRefs
+: Specify which refs created should be banned from being
+ replicated synchronously via apply-object.
+
+ It can be provided more than once, and supports three formats: regular expressions,
+ wildcard matching, and single ref matching. All three formats match are case-sensitive.
+
+ Values starting with a caret `^` are treated as regular
+ expressions. For the regular expressions details please follow
+ official [java documentation](https://docs.oracle.com/javase/tutorial/essential/regex/).
+
+ Please note that regular expressions could also be used
+ with inverse match.
+
+ Values that are not regular expressions and end in `*` are
+ treated as wildcard matches. Wildcards match refs whose
+ name agrees from the beginning until the trailing `*`. So
+ `foo/b*` would match the refs `foo/b`, `foo/bar`, and
+ `foo/baz`, but neither `foobar`, nor `bar/foo/baz`.
+
+ Values that are neither regular expressions nor wildcards are
+ treated as single ref matches. So `foo/bar` matches only
+ the ref `foo/bar`, but no other refs.
+
+ By default, set to empty (all create refs are replicated synchronously).
+
replication.lockErrorMaxRetries
: Number of times to retry a replication operation if a lock
error is detected.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
index 73715d6..6ce3dbe 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
@@ -70,6 +70,7 @@
private final String TEST_PROJECT_NAME = "FetchOneTest";
private final Project.NameKey PROJECT_NAME = Project.NameKey.parse(TEST_PROJECT_NAME);
private final String TEST_REF = "refs/heads/refForReplicationTask";
+ private final String TEST_DELETE_REF = ":" + TEST_REF;
private final FetchRefSpec TEST_REF_SPEC = FetchRefSpec.fromRef(TEST_REF);
private final String URI_PATTERN = "http://test.com/" + TEST_PROJECT_NAME + ".git";
private final TestMetricMaker testMetricMaker = new TestMetricMaker();
@@ -178,6 +179,26 @@
}
@Test
+ public void shouldDeleteRefWhenAddingDeleteRefSpec() throws IOException {
+ setupRemoteConfigMock(List.of(ALL_REFS_SPEC));
+ objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
+ objectUnderTest.addRefs(refSpecsSetOf(TEST_DELETE_REF));
+
+ assertThat(objectUnderTest.getFetchRefSpecs())
+ .isEqualTo(List.of(FetchRefSpec.fromRef(TEST_DELETE_REF)));
+ }
+
+ @Test
+ public void shouldAddRefWhenAddingRefSpecToDeleteRefSpec() throws IOException {
+ setupRemoteConfigMock(List.of(ALL_REFS_SPEC));
+ objectUnderTest.addRefs(refSpecsSetOf(TEST_DELETE_REF));
+ objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
+
+ assertThat(objectUnderTest.getFetchRefSpecs())
+ .isEqualTo(List.of(FetchRefSpec.fromRef(TEST_REF + ":" + TEST_REF)));
+ }
+
+ @Test
public void shouldReturnExistingStates() {
assertThat(createTestStates(TEST_REF_SPEC, 1))
.isEqualTo(objectUnderTest.getStates().get(TEST_REF_SPEC));
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 1c8a8ea..7f4ca4e 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
@@ -57,6 +57,7 @@
import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
import com.googlesource.gerrit.plugins.replication.pull.client.HttpResult;
+import com.googlesource.gerrit.plugins.replication.pull.filter.ApplyObjectBannedCreateRefsFilter;
import com.googlesource.gerrit.plugins.replication.pull.filter.ApplyObjectsRefsFilter;
import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
import java.io.IOException;
@@ -88,6 +89,8 @@
private static final String TEST_REF_NAME = "refs/meta/heads/anyref";
private static final Project.NameKey PROJECT = Project.nameKey("defaultProject");
+ private static final String OLD_OBJECT_ID =
+ ObjectId.fromString("00f11fd1e3206333235603f889837bad2692da4b").getName();
private static final String NEW_OBJECT_ID =
ObjectId.fromString("3c1ddc050d7906adb0e29bc3bc46af8749b2f63b").getName();
@@ -110,6 +113,7 @@
@Mock HttpResult httpResult;
@Mock HttpResult batchHttpResult;
@Mock ApplyObjectsRefsFilter applyObjectsRefsFilter;
+ @Mock ApplyObjectBannedCreateRefsFilter applyObjectsBannedCreateRefsFilter;
@Mock Config config;
ApplyObjectMetrics applyObjectMetrics;
@@ -207,6 +211,7 @@
LOCAL_INSTANCE_ID,
config,
applyObjectsRefsFilter,
+ applyObjectsBannedCreateRefsFilter,
shutdownState);
}
@@ -238,6 +243,7 @@
LOCAL_INSTANCE_ID,
config,
applyObjectsRefsFilter,
+ applyObjectsBannedCreateRefsFilter,
shutdownState);
Event event = new TestEvent("refs/changes/01/1/meta");
@@ -332,6 +338,7 @@
LOCAL_INSTANCE_ID,
config,
applyObjectsRefsFilter,
+ applyObjectsBannedCreateRefsFilter,
shutdownState);
}
@@ -420,6 +427,34 @@
}
@Test
+ public void shouldCallApplyObjectWhenCreatedRefNotMatchesApplyObjectBannedCreateRefsFilter()
+ throws Exception {
+ String bannedRef = "refs/heads/banned";
+ Event event = generateBatchCreateRefEvent("refs/heads/not-banned");
+ objectUnderTest.start();
+ lenient().when(applyObjectsBannedCreateRefsFilter.match(eq(bannedRef))).thenReturn(true);
+
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient).callBatchSendObject(any(), any(), anyLong(), any());
+ verify(fetchRestApiClient, never()).callBatchFetch(any(), any(), any());
+ }
+
+ @Test
+ public void shouldFallbackToCallBatchFetchWhenCreatedRefMatchesApplyObjectBannedCreateRefsFilter()
+ throws Exception {
+ String bannedRef = "refs/heads/banned";
+ Event event = generateBatchCreateRefEvent("refs/heads/banned");
+ objectUnderTest.start();
+ when(applyObjectsBannedCreateRefsFilter.match(eq(bannedRef))).thenReturn(true);
+
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient).callBatchFetch(any(), any(), any());
+ verify(fetchRestApiClient, never()).callBatchSendObject(any(), any(), anyLong(), any());
+ }
+
+ @Test
public void shouldFallbackToApplyAllParentObjectsWhenParentObjectIsMissingOnMetaRef()
throws Exception {
Event event = generateBatchRefUpdateEvent("refs/changes/01/1/meta");
@@ -536,6 +571,7 @@
LOCAL_INSTANCE_ID,
config,
applyObjectsRefsFilter,
+ applyObjectsBannedCreateRefsFilter,
shutdownState);
Event event = generateBatchRefUpdateEvent("refs/multi-site/version");
objectUnderTest.onEvent(event);
@@ -618,13 +654,21 @@
}
private BatchRefUpdateEvent generateBatchRefUpdateEvent(String... refs) {
+ return generateBatchRefEvent(OLD_OBJECT_ID, NEW_OBJECT_ID, refs);
+ }
+
+ private BatchRefUpdateEvent generateBatchCreateRefEvent(String... refs) {
+ return generateBatchRefEvent(ObjectId.zeroId().name(), NEW_OBJECT_ID, refs);
+ }
+
+ private BatchRefUpdateEvent generateBatchRefEvent(String oldRev, String newRev, String[] refs) {
List<RefUpdateAttribute> refUpdates =
Arrays.stream(refs)
.map(
ref -> {
RefUpdateAttribute upd = new RefUpdateAttribute();
- upd.newRev = NEW_OBJECT_ID;
- upd.oldRev = ObjectId.zeroId().getName();
+ upd.newRev = newRev;
+ upd.oldRev = oldRev;
upd.project = PROJECT.get();
upd.refName = ref;
return upd;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
index 7f0df41..cfff177 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
@@ -624,7 +624,7 @@
List<BatchApplyObjectData> batchApplyObjects = new ArrayList<>();
batchApplyObjects.add(
- BatchApplyObjectData.create(refName, Optional.of(createSampleRevisionData("a"))));
+ BatchApplyObjectData.newUpdateRef(refName, Optional.of(createSampleRevisionData("a"))));
objectUnderTest.callBatchSendObject(
Project.nameKey("test_repo"), batchApplyObjects, eventCreatedOn, new URIish(api));
@@ -647,8 +647,8 @@
RevisionData revisionA = createSampleRevisionData("a");
RevisionData revisionB = createSampleRevisionData("b");
String refNameB = "refs/heads/b";
- batchApplyObjects.add(BatchApplyObjectData.create(refName, Optional.of(revisionA)));
- batchApplyObjects.add(BatchApplyObjectData.create(refNameB, Optional.of(revisionB)));
+ batchApplyObjects.add(BatchApplyObjectData.newUpdateRef(refName, Optional.of(revisionA)));
+ batchApplyObjects.add(BatchApplyObjectData.newUpdateRef(refNameB, Optional.of(revisionB)));
objectUnderTest.callBatchSendObject(
Project.nameKey("test_repo"), batchApplyObjects, eventCreatedOn, new URIish(api));