You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2026/10/20 16:55:53 UTC
(maven-resolver) branch master updated: GH-2196: Avoid warning for same-session repository fall-through (#2197)
This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new 94b42a399 GH-2196: Avoid warning for same-session repository fall-through (#2197)
94b42a399 is described below
commit 94b42a3998784ee1d218566391fcffb0a1f25886
Author: Efe <go...@gmail.com>
AuthorDate: Tue Oct 20 19:55:46 2026 +0300
GH-2196: Avoid warning for same-session repository fall-through (#2197)
`DefaultUpdateCheckManager` now tracks artifact not-found markers written during the current `RepositorySystemSession`. When a sibling repository later provides the artifact, a marker written in the same session is still expired but logged at DEBUG. A marker from an earlier session continues to produce a warning.
Fixes #2196
### Testing
- `mvn -pl maven-resolver-impl -am -Dtest=DefaultUpdateCheckManagerTest -Dsurefire.failIfNoSpecifiedTests=false test` — 38 tests passed
- `mvn -pl maven-resolver-impl -am test` — 585 tests passed, 5 skipped
- `mvn -pl maven-resolver-impl -am spotless:check` — passed
- `git diff --check` — passed
---
- [x] I hereby declare this contribution to be licensed under the [Apache License Version 2.0, January 2004](https://www.apache.org/licenses/LICENSE-2.0)
- [x] I hereby declare that I have filed an [Apache Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf)
---
.../internal/impl/DefaultUpdateCheckManager.java | 70 ++++++++++++++++++----
.../impl/DefaultUpdateCheckManagerTest.java | 61 +++++++++++++++++++
2 files changed, 119 insertions(+), 12 deletions(-)
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
index 9eab98a89..aed94a7b7 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
@@ -77,6 +77,14 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
}
});
+ // instance bound private key
+ static final Object SESSION_NOT_FOUNDS = Keys.of(new Object() {
+ @Override
+ public String toString() {
+ return "updateCheckManager.notFounds";
+ }
+ });
+
/**
* Manages the session state, i.e. influences if the same download requests to artifacts/metadata will happen
* multiple times within the same RepositorySystemSession. If "enabled" will enable the session state. If "bypass"
@@ -455,6 +463,25 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
((Map<String, Boolean>) checkedFiles).put(updateKey, Boolean.TRUE);
}
+ private String getNotFoundKey(Path touchPath, String dataKey) {
+ return touchPath.toAbsolutePath() + "|" + dataKey;
+ }
+
+ /**
+ * Records not-found markers written during this session so that an ordinary repository fall-through is not
+ * logged as a warning when a sibling repository later provides the artifact.
+ */
+ @SuppressWarnings("unchecked")
+ private void setNotFound(RepositorySystemSession session, String notFoundKey) {
+ Object notFounds = session.getData().computeIfAbsent(SESSION_NOT_FOUNDS, ConcurrentHashMap::newKeySet);
+ ((Set<String>) notFounds).add(notFoundKey);
+ }
+
+ private boolean isNotFoundInSession(RepositorySystemSession session, String notFoundKey) {
+ Object notFounds = session.getData().get(SESSION_NOT_FOUNDS);
+ return notFounds instanceof Set && ((Set<?>) notFounds).contains(notFoundKey);
+ }
+
private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) {
return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
}
@@ -476,9 +503,13 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
String transferKey = getTransferKey(session, check.getRepository());
setUpdated(session, updateKey);
- Map<String, String> staleSiblingNotFounds = check.getException() == null
- ? getStaleSiblingNotFounds(read(touchPath), dataKey, check.getItem(), check.getRepository())
- : Collections.emptyMap();
+ Map<String, String> staleSiblingNotFounds = Collections.emptyMap();
+ if (check.getException() == null) {
+ staleSiblingNotFounds = getStaleSiblingNotFounds(
+ session, read(touchPath), touchPath, dataKey, check.getItem(), check.getRepository());
+ } else if (check.getException() instanceof ArtifactNotFoundException) {
+ setNotFound(session, getNotFoundKey(touchPath, dataKey));
+ }
Properties props = write(touchPath, dataKey, transferKey, check.getException(), staleSiblingNotFounds);
if (Files.exists(artifactPath) && !hasErrors(props)) {
@@ -492,23 +523,38 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
* that issued it, so a single spoofed or transient "not found" answer would otherwise durably reroute
* resolution of the artifact to whatever lower-prioritized repository serves it. Expiring the marker forces a
* confirming re-check of the suppressed repository (subject to the update policy) the next time it is
- * consulted, and the reroute is surfaced at WARN level instead of happening silently.
+ * consulted, and the reroute is surfaced at WARN level instead of happening silently. A marker written during
+ * the current session is the ordinary fall-through between repositories and is logged at DEBUG instead.
*/
private Map<String, String> getStaleSiblingNotFounds(
- Properties props, String dataKey, Artifact artifact, RemoteRepository repository) {
+ RepositorySystemSession session,
+ Properties props,
+ Path touchPath,
+ String dataKey,
+ Artifact artifact,
+ RemoteRepository repository) {
Map<String, String> removals = null;
for (Object k : props.keySet()) {
String key = k.toString();
if (key.endsWith(ERROR_KEY_SUFFIX)) {
String otherDataKey = key.substring(0, key.length() - ERROR_KEY_SUFFIX.length());
if (!otherDataKey.equals(dataKey) && NOT_FOUND.equals(props.getProperty(key))) {
- LOGGER.warn(
- "{} was downloaded from {} while a cached not-found from a previous attempt suppresses"
- + " re-checking {}; expiring the cached not-found so that repository is checked"
- + " again on the next resolution",
- artifact,
- repository.getUrl(),
- otherDataKey);
+ if (isNotFoundInSession(session, getNotFoundKey(touchPath, otherDataKey))) {
+ LOGGER.debug(
+ "{} was downloaded from {} after {} answered not-found earlier in this session;"
+ + " not caching that not-found",
+ artifact,
+ repository.getUrl(),
+ otherDataKey);
+ } else {
+ LOGGER.warn(
+ "{} was downloaded from {} while a not-found cached by a previous session suppresses"
+ + " re-checking {}; expiring the cached not-found so that repository is"
+ + " checked again on the next resolution",
+ artifact,
+ repository.getUrl(),
+ otherDataKey);
+ }
if (removals == null) {
removals = new HashMap<>();
}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
index cae98bf71..a479f3ec8 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
@@ -18,9 +18,12 @@
*/
package org.eclipse.aether.internal.impl;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
+import java.io.PrintStream;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;
@@ -46,11 +49,15 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
import static org.junit.jupiter.api.Assertions.*;
/**
*/
+@ResourceLock(value = Resources.SYSTEM_ERR, mode = ResourceAccessMode.READ_WRITE)
public class DefaultUpdateCheckManagerTest {
private static final long HOUR = 60L * 60L * 1000L;
@@ -98,6 +105,22 @@ public class DefaultUpdateCheckManagerTest {
static void resetSessionData(RepositorySystemSession session) {
session.getData().set(DefaultUpdateCheckManager.SESSION_CHECKS, null);
+ session.getData().set(DefaultUpdateCheckManager.SESSION_NOT_FOUNDS, null);
+ }
+
+ /**
+ * Runs the action while capturing the slf4j-simple output written to {@code System.err}.
+ */
+ private static String captureStdErr(Runnable action) throws Exception {
+ PrintStream original = System.err;
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try (PrintStream capture = new PrintStream(buffer, true, StandardCharsets.UTF_8.name())) {
+ System.setErr(capture);
+ action.run();
+ } finally {
+ System.setErr(original);
+ }
+ return buffer.toString(StandardCharsets.UTF_8.name());
}
private UpdateCheck<Metadata, MetadataTransferException> newMetadataCheck() {
@@ -167,6 +190,44 @@ public class DefaultUpdateCheckManagerTest {
assertEquals("some error", props.getProperty("file:///other-repo/.error"));
}
+ @Test
+ void testTouchArtifactDoesNotWarnAboutSiblingNotFoundFromSameSession() throws Exception {
+ File touchFile = new File(artifact.getFile().getPath() + ".lastUpdated");
+
+ RemoteRepository other = new RemoteRepository.Builder("other", "default", "file:///other-repo").build();
+ UpdateCheck<Artifact, ArtifactTransferException> notFound = newArtifactCheck();
+ notFound.setRepository(other);
+ notFound.setAuthoritativeRepository(other);
+ notFound.setException(new ArtifactNotFoundException(artifact, other));
+ manager.touchArtifact(session, notFound);
+ assertTrue(touchFile.exists());
+
+ UpdateCheck<Artifact, ArtifactTransferException> success = newArtifactCheck();
+ String log = captureStdErr(() -> manager.touchArtifact(session, success));
+ assertFalse(log.contains("while a not-found cached by a previous session suppresses re-checking"), log);
+ assertFalse(touchFile.exists());
+ }
+
+ @Test
+ void testTouchArtifactWarnsAboutSiblingNotFoundFromPreviousSession() throws Exception {
+ File touchFile = new File(artifact.getFile().getPath() + ".lastUpdated");
+
+ RemoteRepository other = new RemoteRepository.Builder("other", "default", "file:///other-repo").build();
+ UpdateCheck<Artifact, ArtifactTransferException> notFound = newArtifactCheck();
+ notFound.setRepository(other);
+ notFound.setAuthoritativeRepository(other);
+ notFound.setException(new ArtifactNotFoundException(artifact, other));
+ manager.touchArtifact(session, notFound);
+ assertTrue(touchFile.exists());
+
+ resetSessionData(session);
+ UpdateCheck<Artifact, ArtifactTransferException> success = newArtifactCheck();
+ String log = captureStdErr(() -> manager.touchArtifact(session, success));
+ assertTrue(log.contains("while a not-found cached by a previous session suppresses re-checking"), log);
+ assertTrue(log.contains("file:///other-repo/"), log);
+ assertFalse(touchFile.exists());
+ }
+
@Test
void testCheckMetadataFailOnNoFile() {
UpdateCheck<Metadata, MetadataTransferException> check = newMetadataCheck();