Merge branch 'stable-3.12' * stable-3.12: Introduce tests for ReplicationTaskId thread-local Introduce ReplicationTaskId as thread-local Change-Id: Ia6028c2e8634f0b99af78bde25c4db591a8509dc
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 a43ce4a..490073d 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
@@ -15,6 +15,7 @@ package com.googlesource.gerrit.plugins.replication.pull; import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog; +import static com.googlesource.gerrit.plugins.replication.pull.ReplicationTaskId.withTaskId; import static java.util.concurrent.TimeUnit.NANOSECONDS; import com.google.common.annotations.VisibleForTesting; @@ -380,7 +381,7 @@ long startedAt = context.getStartTime(); long delay = NANOSECONDS.toMillis(startedAt - createdAt); git = gitManager.openRepository(projectName); - List<FetchRefSpec> fetchRefSpecs = runImpl(); + List<FetchRefSpec> fetchRefSpecs = withTaskId(taskIdHex, this::runImpl); if (fetchRefSpecs.isEmpty()) { repLog.info( @@ -580,18 +581,22 @@ * @return The list of refSpecs to fetch */ public List<FetchRefSpec> getFetchRefSpecs(boolean lock) throws IOException { - List<FetchRefSpec> configRefSpecs = - config.getFetchRefSpecs().stream().map(FetchRefSpec::fromRefSpec).toList(); + return withTaskId( + taskIdHex, + () -> { + List<FetchRefSpec> configRefSpecs = + config.getFetchRefSpecs().stream().map(FetchRefSpec::fromRefSpec).toList(); - if (delta.isEmpty() && replicationFetchFilter().isEmpty()) { - return configRefSpecs; - } + if (delta.isEmpty() && replicationFetchFilter().isEmpty()) { + return configRefSpecs; + } - return runRefsFilter(computeDeltaIfNeeded(), lock).stream() - .map(ref -> refToFetchRefSpec(ref, configRefSpecs)) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); + return runRefsFilter(computeDeltaIfNeeded(), lock).stream() + .map(ref -> refToFetchRefSpec(ref, configRefSpecs)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + }); } public void unlockRefSpecs(Map<String, AutoCloseable> locks) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationTaskId.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationTaskId.java new file mode 100644 index 0000000..40b3ec0 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationTaskId.java
@@ -0,0 +1,46 @@ +// Copyright (C) 2026 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.common.Nullable; +import java.io.IOException; + +public class ReplicationTaskId { + private static final ThreadLocal<String> currentTaskId = new ThreadLocal<>(); + + @FunctionalInterface + public interface ReplicationBody<T> { + T apply() throws IOException; + } + + static <T> T withTaskId(String taskId, ReplicationBody<T> body) throws IOException { + String oldTaskId = currentTaskId.get(); + currentTaskId.set(taskId); + try { + return body.apply(); + } finally { + if (oldTaskId == null) { + currentTaskId.remove(); + } else { + currentTaskId.set(oldTaskId); + } + } + } + + @Nullable + public static String get() { + return currentTaskId.get(); + } +}
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 50d91ff..5547b60 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
@@ -369,6 +369,25 @@ } @Test + public void shouldSetReplicationTaskIdDuringRefsFiltering() throws Exception { + assertThat(ReplicationTaskId.get()).isNull(); + + setupObjectUnderTestMocksForFetchingTestRef(); + + objectUnderTest.setReplicationFetchFilter(replicationFilter); + when(replicationFilter.get()) + .thenReturn( + (projectName, fetchRefs) -> { + assertThat(ReplicationTaskId.get()).isEqualTo(objectUnderTest.getTaskIdHex()); + return Set.of(TEST_REF); + }); + + objectUnderTest.run(); + + assertThat(ReplicationTaskId.get()).isNull(); + } + + @Test public void fetchWithoutDelta_shouldPassNewRefsToFilter() throws Exception { setupMocks(true); String REMOTE_REF = "refs/heads/remote"; @@ -890,6 +909,15 @@ assertThat(refsToDelete(fetchRefSpecs)).isEqualTo(Set.of("refs/something/someref")); } + private void setupObjectUnderTestMocksForFetchingTestRef() throws Exception { + setupMocks(true); + Set<FetchRefSpec> refSpecs = Set.of(TEST_REF_SPEC); + setupFetchFactoryMock( + List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()), + Optional.of(List.of(TEST_REF))); + objectUnderTest.addRefs(refSpecs); + } + private void setupRequestScopeMock() { when(scoper.scope(any())) .thenAnswer(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationTaskIdTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationTaskIdTest.java new file mode 100644 index 0000000..4889d2a --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationTaskIdTest.java
@@ -0,0 +1,69 @@ +// Copyright (C) 2026 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 static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static com.googlesource.gerrit.plugins.replication.pull.ReplicationTaskId.withTaskId; + +import java.io.IOException; +import org.junit.Test; + +public class ReplicationTaskIdTest { + String OUTER_TASK_ID = "outer"; + String INNER_TASK_ID = "inner"; + + @Test + public void withTaskIdShouldPreserveIdUponNestedCalls() throws IOException { + String unused = + withTaskId( + OUTER_TASK_ID, + () -> { + withTaskId( + INNER_TASK_ID, + () -> { + assertThat(ReplicationTaskId.get()).isEqualTo(INNER_TASK_ID); + return "unused"; + }); + assertThat(ReplicationTaskId.get()).isEqualTo(OUTER_TASK_ID); + return "unused"; + }); + assertThat(ReplicationTaskId.get()).isNull(); + } + + @Test + public void withTaskIdShouldPreserveIdUponNestedCallsWithExceptions() throws IOException { + assertThrows( + IOException.class, + () -> + withTaskId( + OUTER_TASK_ID, + () -> { + try { + withTaskId( + INNER_TASK_ID, + () -> { + assertThat(ReplicationTaskId.get()).isEqualTo(INNER_TASK_ID); + throw new IOException("exception"); + }); + } catch (IOException e) { + assertThat(ReplicationTaskId.get()).isEqualTo(OUTER_TASK_ID); + throw e; + } + return "unused"; + })); + assertThat(ReplicationTaskId.get()).isNull(); + } +}