davsclaus commented on code in PR #29781:
URL: https://github.com/apache/camel/pull/29781#discussion_r4560690789


##########
tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java:
##########
@@ -840,9 +857,91 @@ private List<RemoteRepository> resolutionRepositories(
                     session, extraRepos);
             repositories.addAll(resolvedExtraRepos);
         }
+        if (!unreachableRepositories.isEmpty()) {
+            // asking them again would only wait for the connect timeout again
+            repositories.removeIf(r -> 
unreachableRepositories.contains(repositoryUrl(r.getUrl())));
+        }
         return repositories;
     }
 
+    /**
+     * Sets the request timeout of Camel (unless configured, such as with the 
aether.connector.requestTimeout system
+     * property), and stops using a repository that cannot be connected to.
+     */
+    private RepositorySystemSession 
withTimeoutsAndReachability(RepositorySystemSession session) {
+        DefaultRepositorySystemSession answer = new 
DefaultRepositorySystemSession(session);
+        if 
(session.getConfigProperties().get(ConfigurationProperties.REQUEST_TIMEOUT) == 
null) {
+            answer.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 
requestTimeout);
+        }
+        
answer.setTransferListener(ChainedTransferListener.newInstance(session.getTransferListener(),
+                new UnreachableRepositoryListener(unreachableRepositories)));
+        return answer;
+    }
+
+    /**
+     * Notes the repositories that cannot be connected to: a host that does 
not resolve, refuses the connection, or does
+     * not answer within the connect timeout. A repository that is slow to 
send an artifact is still used.
+     */
+    static final class UnreachableRepositoryListener extends 
AbstractTransferListener {
+
+        private final Set<String> unreachable;
+
+        UnreachableRepositoryListener(Set<String> unreachable) {
+            this.unreachable = unreachable;
+        }
+
+        @Override
+        public void transferFailed(TransferEvent event) {
+            Throwable cause = connectFailure(event.getException());
+            if (cause != null && event.getResource() != null && 
event.getResource().getRepositoryUrl() != null) {
+                String url = 
repositoryUrl(event.getResource().getRepositoryUrl());
+                if (unreachable.add(url)) {
+                    LOG.warn("Cannot connect to Maven repository: {} due to: 
{}. The repository is not used anymore.",
+                            url, cause.getMessage() != null ? 
cause.getMessage() : cause.getClass().getName());
+                }
+            }
+        }
+
+        static Throwable connectFailure(Throwable e) {
+            // guard against a cause that points back to an exception in the 
chain
+            int depth = 0;
+            for (Throwable t = e; t != null && depth < 20; t = t.getCause(), 
depth++) {
+                if (t instanceof ConnectException || t instanceof 
UnknownHostException) {
+                    return t;
+                }
+                // the connect timeout of the JDK, and of Apache HttpClient 
(ConnectTimeoutException)
+                if (t instanceof SocketTimeoutException && "Connect timed 
out".equalsIgnoreCase(t.getMessage())
+                        || 
"org.apache.http.conn.ConnectTimeoutException".equals(t.getClass().getName())) {

Review Comment:
   Done differently in 212303df2aa5: the JDK socket case and the HTTP client 
case are now two separate `if`s, so there is no precedence to read.
   
   _Claude Code on behalf of Claus Ibsen (davsclaus)_



##########
tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java:
##########
@@ -1130,4 +1138,94 @@ private static void writePom(File repo, String 
artifactId, String body) throws E
                 """.formatted(artifactId, body);
         Files.writeString(new File(dir, artifactId + "-1.0.0.pom").toPath(), 
pom);
     }
+
+    @Test
+    void testUnreachableRepositoryIsNotUsedAnymore() throws Exception {
+        File repo = createDescriptorFixtureRepository();
+        File localRepo = new File(tempDir, "unreachable-local-repo");
+        // nothing listens on this port, so the connection is refused
+        String dead = "http://localhost:"; + 
AvailablePortFinder.getNextAvailable(20000, 30000) + "/maven";
+
+        try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            downloader.setMavenCentralEnabled(false);
+            downloader.setMavenApacheSnapshotEnabled(false);
+            downloader.setMavenSettingsLocation("false");
+            downloader.setRepos(dead + "," + repo.toURI());
+            downloader.build();
+            MavenDownloader customDownloader = 
downloader.customize(localRepo.getAbsolutePath(), 5000, 5000);
+
+            // an artifact no repository has: both are asked
+            assertThrows(MavenResolutionException.class,
+                    () -> 
customDownloader.resolveArtifacts(List.of("org.example.fixture:missing:1.0.0"), 
null, false,
+                            false));
+            assertEquals(Set.of(dead), 
downloader.getUnreachableRepositories());
+
+            // the other repository is still used
+            List<MavenArtifact> artifacts = customDownloader.resolveArtifacts(
+                    List.of("org.example.fixture:fixture-bom:pom:1.0.0"), 
null, false, false);
+            assertEquals(1, artifacts.size());
+
+            // the repository is not asked again, and the failure says so
+            MavenResolutionException e = 
assertThrows(MavenResolutionException.class,
+                    () -> 
customDownloader.resolveArtifacts(List.of("org.example.fixture:missing2:1.0.0"),
 null, false,
+                            false));
+            assertTrue(e.getRepositories().contains(dead + " (not 
reachable)"), e.getRepositories().toString());
+            assertFalse(e.getRepositories().contains(dead), 
e.getRepositories().toString());
+        }
+    }
+
+    @Test
+    void testRepositoryThatDoesNotAnswerFailsOnTheRequestTimeout() throws 
Exception {
+        File localRepo = new File(tempDir, "stalled-local-repo");
+        // accepts the connection, and never answers
+        try (ServerSocket server = new ServerSocket(0);
+             MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            List<Socket> accepted = new CopyOnWriteArrayList<>();
+            Thread acceptor = new Thread(() -> {
+                try {
+                    while (!server.isClosed()) {
+                        accepted.add(server.accept());
+                    }
+                } catch (Exception e) {
+                    // closed
+                }
+            });
+            acceptor.setDaemon(true);
+            acceptor.start();
+            String stalled = "http://localhost:"; + server.getLocalPort() + 
"/maven";
+
+            downloader.setMavenCentralEnabled(false);
+            downloader.setMavenApacheSnapshotEnabled(false);
+            downloader.setMavenSettingsLocation("false");
+            downloader.setRepos(stalled);
+            downloader.build();
+            assertEquals(60000, downloader.getRequestTimeout());
+            MavenDownloader customDownloader = 
downloader.customize(localRepo.getAbsolutePath(), 5000, 1000);
+
+            long start = System.nanoTime();
+            assertThrows(MavenResolutionException.class,
+                    () -> 
customDownloader.resolveArtifacts(List.of("org.example.fixture:stalled:1.0.0"), 
null, false,
+                            false));
+            assertTrue(TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - 
start) < 30);
+            // a slow repository is still used
+            assertTrue(downloader.getUnreachableRepositories().isEmpty());
+            for (Socket socket : accepted) {
+                socket.close();
+            }
+        }
+    }
+
+    @Test
+    void testConnectFailures() {
+        
assertNotNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(
+                new RuntimeException(new ConnectException("Connection 
refused"))));
+        
assertNotNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(new
 UnknownHostException("nope")));
+        
assertNotNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(
+                new SocketTimeoutException("Connect timed out")));
+        // a read timeout is a slow repository, not an unreachable one
+        
assertNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(
+                new SocketTimeoutException("Read timed out")));
+        
assertNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(new 
IOException("Not found")));
+        
assertNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(null));
+    }

Review Comment:
   Added in 212303df2aa5: a wrapped HttpClient 4 `ConnectTimeoutException`, and 
the JDK `HttpConnectTimeoutException`.
   
   _Claude Code on behalf of Claus Ibsen (davsclaus)_



##########
tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java:
##########
@@ -840,9 +857,91 @@ private List<RemoteRepository> resolutionRepositories(
                     session, extraRepos);
             repositories.addAll(resolvedExtraRepos);
         }
+        if (!unreachableRepositories.isEmpty()) {
+            // asking them again would only wait for the connect timeout again
+            repositories.removeIf(r -> 
unreachableRepositories.contains(repositoryUrl(r.getUrl())));
+        }
         return repositories;
     }
 
+    /**
+     * Sets the request timeout of Camel (unless configured, such as with the 
aether.connector.requestTimeout system
+     * property), and stops using a repository that cannot be connected to.
+     */
+    private RepositorySystemSession 
withTimeoutsAndReachability(RepositorySystemSession session) {
+        DefaultRepositorySystemSession answer = new 
DefaultRepositorySystemSession(session);
+        if 
(session.getConfigProperties().get(ConfigurationProperties.REQUEST_TIMEOUT) == 
null) {
+            answer.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 
requestTimeout);
+        }
+        
answer.setTransferListener(ChainedTransferListener.newInstance(session.getTransferListener(),
+                new UnreachableRepositoryListener(unreachableRepositories)));
+        return answer;
+    }
+
+    /**
+     * Notes the repositories that cannot be connected to: a host that does 
not resolve, refuses the connection, or does
+     * not answer within the connect timeout. A repository that is slow to 
send an artifact is still used.
+     */
+    static final class UnreachableRepositoryListener extends 
AbstractTransferListener {
+
+        private final Set<String> unreachable;
+
+        UnreachableRepositoryListener(Set<String> unreachable) {
+            this.unreachable = unreachable;
+        }
+
+        @Override
+        public void transferFailed(TransferEvent event) {
+            Throwable cause = connectFailure(event.getException());
+            if (cause != null && event.getResource() != null && 
event.getResource().getRepositoryUrl() != null) {
+                String url = 
repositoryUrl(event.getResource().getRepositoryUrl());
+                if (unreachable.add(url)) {
+                    LOG.warn("Cannot connect to Maven repository: {} due to: 
{}. The repository is not used anymore.",
+                            url, cause.getMessage() != null ? 
cause.getMessage() : cause.getClass().getName());
+                }
+            }
+        }
+
+        static Throwable connectFailure(Throwable e) {
+            // guard against a cause that points back to an exception in the 
chain
+            int depth = 0;
+            for (Throwable t = e; t != null && depth < 20; t = t.getCause(), 
depth++) {
+                if (t instanceof ConnectException || t instanceof 
UnknownHostException) {
+                    return t;
+                }
+                // the connect timeout of the JDK, and of Apache HttpClient 
(ConnectTimeoutException)
+                if (t instanceof SocketTimeoutException && "Connect timed 
out".equalsIgnoreCase(t.getMessage())
+                        || 
"org.apache.http.conn.ConnectTimeoutException".equals(t.getClass().getName())) {
+                    return t;

Review Comment:
   Rather than only a comment, the check in 212303df2aa5 now matches a class 
whose simple name ends in `ConnectTimeoutException`, so HttpClient 4, 
HttpClient 5 (`org.apache.hc.client5.http.ConnectTimeoutException`) and the JDK 
`HttpConnectTimeoutException` are all detected without depending on any of 
them; the comment lists the three.
   
   _Claude Code on behalf of Claus Ibsen (davsclaus)_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to