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();