From b22f266da6bdf45defcf35c585185ce417b937cf Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Wed, 15 Jul 2026 12:47:42 +0300 Subject: [PATCH] SOLR-7177: include the target URL when a ConcurrentUpdateSolrClient update fails to connect On a connection failure the runner passed the raw exception to the error handler with no indication of which server it was talking to. Wrap a non-Solr exception in a SolrServerException carrying the base URL, matching what the synchronous clients already do. A RemoteSolrException already carries its URL, so it is passed through unchanged. --- .../SOLR-7177-cusc-connection-error-url.yml | 11 +++ .../impl/ConcurrentUpdateBaseSolrClient.java | 8 +- .../ConcurrentUpdateSolrClientTestBase.java | 84 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/SOLR-7177-cusc-connection-error-url.yml diff --git a/changelog/unreleased/SOLR-7177-cusc-connection-error-url.yml b/changelog/unreleased/SOLR-7177-cusc-connection-error-url.yml new file mode 100644 index 00000000000..f9743b7fcf5 --- /dev/null +++ b/changelog/unreleased/SOLR-7177-cusc-connection-error-url.yml @@ -0,0 +1,11 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc + +title: > + ConcurrentUpdateSolrClient now identifies the target server when an update fails to connect, + instead of reporting a bare connection error with no URL. +type: fixed +authors: + - name: Serhiy Bzhezytskyy +links: + - name: SOLR-7177 + url: https://issues.apache.org/jira/browse/SOLR-7177 diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java index ce4c8673bb4..5baeaec60a9 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java @@ -340,8 +340,14 @@ void sendUpdateStream() throws Exception { } catch (OutOfMemoryError | InterruptedException e) { throw e; - } catch (Throwable e) { + } catch (RemoteSolrException e) { handleError(e, docIds, collection); + } catch (Throwable e) { + handleError( + new SolrServerException( + "Error from server at " + basePath + ": " + e.getMessage(), e), + docIds, + collection); } finally { try { consumeFully(rspBody); diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientTestBase.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientTestBase.java index 7a4c387e408..c3e9184cec5 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientTestBase.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/ConcurrentUpdateSolrClientTestBase.java @@ -399,6 +399,90 @@ public void onError(Throwable ex) { "the per-id callback should not fire for a non-document error", 0, richCalls.get()); } + /** + * A connection failure surfaces the target URL so the failing server is identifiable, rather than + * a bare socket error with no context. + */ + @Test + public void testConnectionErrorIncludesUrl() throws Exception { + String unreachable = "http://127.0.0.1:1/solr"; // nothing listening -> connect failure + List errors = new CopyOnWriteArrayList<>(); + ConcurrentUpdateBaseSolrClient.UpdateErrorHandler handler = + new ConcurrentUpdateBaseSolrClient.UpdateErrorHandler() { + @Override + public void onError(Throwable ex, List ids, String collection) { + errors.add(ex); + } + + @Override + public void onError(Throwable ex) { + errors.add(ex); + } + }; + + try (var httpClient = solrClient(null); + var concurrentClient = + errorHandlerConcurrentClient(unreachable, 10, 2, httpClient, handler)) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", "doc-1"); + concurrentClient.add("collection1", doc); + concurrentClient.blockUntilFinished(); + } + + assertFalse("expected a connection error to be reported", errors.isEmpty()); + boolean anyMentionsUrl = + errors.stream() + .anyMatch( + e -> { + for (Throwable t = e; t != null; t = t.getCause()) { + if (t.getMessage() != null && t.getMessage().contains("127.0.0.1:1")) { + return true; + } + } + return false; + }); + assertTrue( + "connection error should identify the target server; got: " + errors, anyMentionsUrl); + } + + /** + * A connection error is reported as a SolrServerException that preserves the original cause, so + * SolrCmdDistributor's checkRetry can unwrap it via getRootCause to decide whether to retry. + */ + @Test + public void testConnectionErrorIsSolrServerExceptionPreservingCause() throws Exception { + String unreachable = "http://127.0.0.1:1/solr"; + List errors = new CopyOnWriteArrayList<>(); + ConcurrentUpdateBaseSolrClient.UpdateErrorHandler handler = + new ConcurrentUpdateBaseSolrClient.UpdateErrorHandler() { + @Override + public void onError(Throwable ex, List ids, String collection) { + errors.add(ex); + } + + @Override + public void onError(Throwable ex) { + errors.add(ex); + } + }; + + try (var httpClient = solrClient(null); + var concurrentClient = + errorHandlerConcurrentClient(unreachable, 10, 2, httpClient, handler)) { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField("id", "doc-1"); + concurrentClient.add("collection1", doc); + concurrentClient.blockUntilFinished(); + } + + assertFalse("expected a connection error to be reported", errors.isEmpty()); + Throwable reported = errors.get(0); + assertTrue( + "connection error should be wrapped as SolrServerException; got: " + reported, + reported instanceof SolrServerException); + assertNotNull("the original cause must be preserved", reported.getCause()); + } + /** * A naive lambda handler only registers for per-id failures; an error not tied to identifiable * documents is logged by the default general callback and does not stop the client.