From b68b30eec648f27239dd5ce8af26ac63c88f2db6 Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Thu, 16 Jul 2026 11:18:38 +0300 Subject: [PATCH] SOLR-17764: retry CloudSolrClient requests failing with ClosedChannelException CloudSolrClient.wasCommError() classified only SocketException and UnknownHostException as communication errors worth retrying. A connection dropped mid-request surfaces as a ClosedChannelException (for example an HTTP/2 GOAWAY when a node shuts down), which was not retried even though maxRetries allowed it. Treat ClosedChannelException as a communication error so the request is retried on another node when one is available. Note: this does not by itself fix the graceful-shutdown failures tracked in SOLR-17764 (the underlying Jetty HTTP/2 client bug jetty#15368 discards an already-sent response, and a single-node cluster has no other node to retry on), but retrying a dropped connection is correct regardless. --- .../SOLR-17764-retry-closedchannel.yml | 9 +++ .../client/solrj/impl/CloudSolrClient.java | 5 +- .../impl/CloudSolrClientCommErrorTest.java | 64 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/SOLR-17764-retry-closedchannel.yml create mode 100644 solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCommErrorTest.java diff --git a/changelog/unreleased/SOLR-17764-retry-closedchannel.yml b/changelog/unreleased/SOLR-17764-retry-closedchannel.yml new file mode 100644 index 000000000000..d7655a26243c --- /dev/null +++ b/changelog/unreleased/SOLR-17764-retry-closedchannel.yml @@ -0,0 +1,9 @@ +title: > + CloudSolrClient now retries requests that fail with ClosedChannelException, treating a connection + dropped mid-request the same as other communication errors. +type: fixed +authors: + - name: Serhiy Bzhezytskyy +links: + - name: SOLR-17764 + url: https://issues.apache.org/jira/browse/SOLR-17764 diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java index 220f1a72f646..16274e79551c 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java @@ -23,6 +23,7 @@ import java.lang.invoke.MethodHandles; import java.net.SocketException; import java.net.UnknownHostException; +import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -208,7 +209,9 @@ public ClusterState getClusterState() { /** Is this a communication error? We will retry if so. */ protected boolean wasCommError(Throwable t) { - return t instanceof SocketException || t instanceof UnknownHostException; + return t instanceof SocketException + || t instanceof UnknownHostException + || t instanceof ClosedChannelException; } @Override diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCommErrorTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCommErrorTest.java new file mode 100644 index 000000000000..097571955480 --- /dev/null +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudSolrClientCommErrorTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.solr.client.solrj.impl; + +import java.io.IOException; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.channels.ClosedChannelException; +import org.apache.solr.SolrTestCaseJ4; + +/** Unit tests for {@link CloudSolrClient#wasCommError(Throwable)}, which drives request retries. */ +public class CloudSolrClientCommErrorTest extends SolrTestCaseJ4 { + + public void testCommErrorClassification() throws Exception { + try (ProbeClient client = new ProbeClient()) { + // connection-level failures: the target node is gone, so retrying on a fresh node makes sense + assertTrue(client.wasCommError(new SocketException("connection reset"))); + assertTrue(client.wasCommError(new UnknownHostException("no such host"))); + // a connection dropped mid-request (e.g. an HTTP/2 GOAWAY on server shutdown) surfaces as a + // ClosedChannelException; it is the same "node went away" case and should be retried too + assertTrue(client.wasCommError(new ClosedChannelException())); + + // not comm errors: these should not trigger a comm-error retry + assertFalse(client.wasCommError(new IOException("some other io problem"))); + assertFalse(client.wasCommError(new RuntimeException("boom"))); + } + } + + /** Minimal concrete subclass so the protected wasCommError can be exercised in isolation. */ + private static final class ProbeClient extends CloudSolrClient implements AutoCloseable { + ProbeClient() { + super(true, true, false); + } + + @Override + protected LBSolrClient getLbClient() { + return null; + } + + @Override + public ClusterStateProvider getClusterStateProvider() { + return null; + } + + @Override + public HttpSolrClient getHttpClient() { + return null; + } + } +}