This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit df99db866d99b77db30578f3ae643b9f1eb250f9 Author: David Smiley <[email protected]> AuthorDate: Fri Jun 5 16:01:54 2026 -0400 SOLR-18188: Misc test fixes (#4496) Accumulated from several commits on the misc-test-fixes branch. PrepRecoveryOp: add class-level javadoc to explain its purpose. ShowFileRequestHandlerTest, TestPrometheusResponseWriter, TestCborDataFormat: close response streams to fix resource leaks flagged by the test framework. TestDistributedTracing: add retry logic around span assertions to reduce flakiness caused by timing-sensitive telemetry delivery. SocketProxy: call setSoTimeout(1000) on the server socket so the accept loop unblocks promptly when close() is called. Without a timeout the acceptor thread blocks indefinitely in accept() and leaks, causing test hangs or "thread leaked" warnings. JettySolrRunner: move proxy.close() to before server.stop() rather than leaving it in the finally block. If qtp.join() hangs (e.g. stuck PrepRecoveryOp handlers or H2C session threads) the finally block is never reached, leaving the SocketProxy acceptor thread leaked. Closing the proxy first ensures cleanup even when server shutdown hangs. HttpJettySolrClientCompatibilityTest: switch from @ClassRule to @Rule so the Jetty server lifecycle is per-test as intended. TestSolrJ: delete this file -- it contains no actual tests. ClientUtils: add Objects.requireNonNull() guard on serverRootUrl to surface null earlier with a clear error rather than a cryptic NPE downstream. AbstractFullDistribZkTestBase: wrap control-collection creation in RetryUtil.retryOnException() to tolerate transient ZooKeeper errors during setup. Also move coreClients.clear(), solrClientByCollection.clear(), and super.destroyServers() into a finally block after ExecutorUtil.shutdownAndAwaitTermination() so teardown always completes even if the executor shutdown throws. HttpPartitionTest: decrease leaderConflictResolveWait to 10sec so stale PrepRecoveryOp handlers time out within the Jetty graceful stop window Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> --- .../apache/solr/handler/admin/PrepRecoveryOp.java | 7 + .../org/apache/solr/cloud/HttpPartitionTest.java | 2 + .../handler/admin/ShowFileRequestHandlerTest.java | 11 ++ .../response/TestPrometheusResponseWriter.java | 2 + .../src/test/org/apache/solr/search/TestSolrJ.java | 188 --------------------- .../org/apache/solr/util/TestCborDataFormat.java | 6 +- .../solr/opentelemetry/TestDistributedTracing.java | 36 ++-- .../HttpJettySolrClientCompatibilityTest.java | 10 +- .../apache/solr/client/solrj/util/ClientUtils.java | 3 +- .../solr/cloud/AbstractFullDistribZkTestBase.java | 30 ++-- .../org/apache/solr/embedded/JettySolrRunner.java | 8 +- .../src/java/org/apache/solr/util/SocketProxy.java | 28 ++- 12 files changed, 105 insertions(+), 226 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/admin/PrepRecoveryOp.java b/solr/core/src/java/org/apache/solr/handler/admin/PrepRecoveryOp.java index 9f8bc79413e..8fdbd5aa72f 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/PrepRecoveryOp.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/PrepRecoveryOp.java @@ -37,6 +37,13 @@ import org.apache.solr.util.TestInjection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Long-polling request sent by a recovering replica to the shard leader. The leader holds the + * request open, waiting until it observes the replica reach the expected state (typically + * RECOVERING) in ZooKeeper before responding. This synchronization ensures the leader is ready to + * accept updates from the replica before recovery proceeds. The wait is bounded by {@code + * leaderConflictResolveWait} (default 180s). + */ class PrepRecoveryOp implements CoreAdminHandler.CoreAdminOp { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/test/org/apache/solr/cloud/HttpPartitionTest.java b/solr/core/src/test/org/apache/solr/cloud/HttpPartitionTest.java index 3f1e3c19c1b..c799ab5981e 100644 --- a/solr/core/src/test/org/apache/solr/cloud/HttpPartitionTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/HttpPartitionTest.java @@ -83,6 +83,8 @@ public class HttpPartitionTest extends AbstractFullDistribZkTestBase { System.setProperty("solr.httpclient.retries", "0"); System.setProperty("solr.retries.on.forward", "0"); System.setProperty("solr.retries.to.followers", "0"); + // Keep short so stale PrepRecoveryOp handlers time out within the Jetty graceful stop window + System.setProperty("leaderConflictResolveWait", "10000"); } public HttpPartitionTest() { diff --git a/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java index 69f3108ba8a..31b0f00b79c 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/ShowFileRequestHandlerTest.java @@ -142,6 +142,7 @@ public class ShowFileRequestHandlerTest extends SolrTestCaseJ4 { createShowFileRequest(params("file", "managed-schema", "contentType", "not/known")); request.setResponseParser(new InputStreamResponseParser("xml")); NamedList<Object> response = client.request(request); + closeResponseStream(response); assertEquals(response.get("responseStatus"), 404); } @@ -152,6 +153,7 @@ public class ShowFileRequestHandlerTest extends SolrTestCaseJ4 { params("file", "/etc/passwd", "contentType", "text/plain; charset=utf-8")); request.setResponseParser(new InputStreamResponseParser("xml")); NamedList<Object> response = client.request(request); + closeResponseStream(response); assertEquals(response.get("responseStatus"), 404); } @@ -162,6 +164,7 @@ public class ShowFileRequestHandlerTest extends SolrTestCaseJ4 { params("file", "../../solr.xml", "contentType", "application/xml; charset=utf-8")); request.setResponseParser(new InputStreamResponseParser("xml")); NamedList<Object> response = client.request(request); + closeResponseStream(response); assertEquals(response.get("responseStatus"), 400); } @@ -176,6 +179,7 @@ public class ShowFileRequestHandlerTest extends SolrTestCaseJ4 { "text/plain; charset=utf-8")); request.setResponseParser(new InputStreamResponseParser("xml")); NamedList<Object> response = client.request(request); + closeResponseStream(response); assertEquals(response.get("responseStatus"), 400); } @@ -202,4 +206,11 @@ public class ShowFileRequestHandlerTest extends SolrTestCaseJ4 { // Non-known content types are rejected with 400 error expectThrows(SolrException.class, () -> ShowFileRequestHandler.getSafeContentType("foo/bar")); } + + private void closeResponseStream(NamedList<Object> response) throws IOException { + InputStream stream = (InputStream) response.get("stream"); + if (stream != null) { + stream.close(); + } + } } diff --git a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java index f229b01ac67..610b7267cbe 100644 --- a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java +++ b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java @@ -193,6 +193,8 @@ public class TestPrometheusResponseWriter extends SolrTestCaseJ4 { try (SolrClient adminClient = getHttpSolrClient(solrTestRule.getBaseUrl())) { NamedList<Object> res = adminClient.request(req); + InputStream stream = (InputStream) res.get("stream"); + if (stream != null) stream.close(); // Unknown wt parameter should return a 400 error assertEquals(400, res.get("responseStatus")); } diff --git a/solr/core/src/test/org/apache/solr/search/TestSolrJ.java b/solr/core/src/test/org/apache/solr/search/TestSolrJ.java deleted file mode 100644 index 4d3c4a3c9d7..00000000000 --- a/solr/core/src/test/org/apache/solr/search/TestSolrJ.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.search; - -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Random; -import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.SolrServerException; -import org.apache.solr.client.solrj.apache.ConcurrentUpdateSolrClient; -import org.apache.solr.common.SolrInputDocument; -import org.apache.solr.util.RTimer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TestSolrJ extends SolrTestCaseJ4 { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - public void testSolrJ() { - // docs, producers, connections, sleep_time - // main(new String[] {"1000000","4", "1", "0"}); - - // doCommitPerf(); - } - - public static SolrClient client; - public static String idField = "id"; - public static Exception ex; - - public static void main(String[] args) throws Exception { - // String addr = "http://odin.local:80/solr"; - // String addr = "http://odin.local:8983/solr"; - String addr = "http://127.0.0.1:8983/solr"; - - int i = 0; - final int nDocs = Integer.parseInt(args[i++]); - final int nProducers = Integer.parseInt(args[i++]); - final int nConnections = Integer.parseInt(args[i++]); - final int maxSleep = Integer.parseInt(args[i++]); - - ConcurrentUpdateSolrClient concurrentClient = null; - - // server = concurrentClient = new ConcurrentUpdateSolrServer(addr,32,8); - client = - concurrentClient = - new ConcurrentUpdateSolrClient.Builder(addr) - .withQueueSize(64) - .withThreadCount(nConnections) - .build(); - - client.deleteByQuery("*:*"); - client.commit(); - - final RTimer timer = new RTimer(); - - final int docsPerThread = nDocs / nProducers; - - Thread[] threads = new Thread[nProducers]; - - for (int threadNum = 0; threadNum < nProducers; threadNum++) { - final int base = threadNum * docsPerThread; - - threads[threadNum] = - new Thread("add-thread" + i) { - @Override - public void run() { - try { - indexDocs(base, docsPerThread, maxSleep); - } catch (Exception e) { - log.error("###############################CAUGHT EXCEPTION", e); - ex = e; - } - } - }; - threads[threadNum].start(); - } - - // optional: wait for commit? - - for (int threadNum = 0; threadNum < nProducers; threadNum++) { - threads[threadNum].join(); - } - - if (concurrentClient != null) { - concurrentClient.blockUntilFinished(); - } - - double elapsed = timer.getTime(); - System.out.println( - "time=" + elapsed + " throughput=" + (nDocs * 1000 / elapsed) + " Exception=" + ex); - - // should server threads be marked as daemon? - // need a server.close()!!! - } - - @SuppressWarnings({"unchecked"}) - public static SolrInputDocument getDocument(int docnum) { - SolrInputDocument doc = new SolrInputDocument(); - doc.setField(idField, docnum); - doc.setField("cat", Integer.toString(docnum & 0x0f)); - doc.setField("name", "my name is " + Integer.toString(docnum & 0xff)); - doc.setField("foo_t", "now is the time for all good men to come to the aid of their country"); - doc.setField("foo_i", Integer.toString(docnum & 0x0f)); - doc.setField("foo_s", Integer.toString(docnum & 0xff)); - doc.setField("foo_b", Boolean.toString((docnum & 0x01) == 1)); - doc.setField("parent_s", Integer.toString(docnum - 1)); - doc.setField("price", Integer.toString(docnum >> 4)); - - int golden = (int) 2654435761L; - int h = docnum * golden; - int n = (h & 0xff) + 1; - @SuppressWarnings({"rawtypes"}) - List lst = new ArrayList(n); - for (int i = 0; i < n; i++) { - h = (h + i) * golden; - lst.add(h & 0xfff); - } - - doc.setField("num_is", lst); - return doc; - } - - public static void indexDocs(int base, int count, int maxSleep) - throws IOException, SolrServerException { - Random r = new Random(base); - - for (int i = base; i < count + base; i++) { - if ((i & 0xfffff) == 0) { - System.out.print("\n% " + new Date() + "\t" + i + "\t"); - System.out.flush(); - } - - if ((i & 0xffff) == 0) { - System.out.print("."); - System.out.flush(); - } - - SolrInputDocument doc = getDocument(i); - client.add(doc); - - if (maxSleep > 0) { - int sleep = r.nextInt(maxSleep); - try { - Thread.sleep(sleep); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("interrupted", e); - throw new RuntimeException(e); - } - } - } - } - - public void doCommitPerf() throws Exception { - - try (SolrClient client = getHttpSolrClient("http://127.0.0.1:8983/solr")) { - - final RTimer timer = new RTimer(); - - for (int i = 0; i < 10000; i++) { - SolrInputDocument doc = new SolrInputDocument(); - doc.addField("id", Integer.toString(i % 13)); - client.add(doc); - client.commit(true, true, true); - } - - System.out.println("TIME: " + timer.getTime()); - } - } -} diff --git a/solr/core/src/test/org/apache/solr/util/TestCborDataFormat.java b/solr/core/src/test/org/apache/solr/util/TestCborDataFormat.java index 056e9536529..c98ef9f01eb 100644 --- a/solr/core/src/test/org/apache/solr/util/TestCborDataFormat.java +++ b/solr/core/src/test/org/apache/solr/util/TestCborDataFormat.java @@ -135,8 +135,10 @@ public class TestCborDataFormat extends SolrCloudTestCase { request.setResponseParser(new InputStreamResponseParser(wt)); } result = client.request(request, testCollection); - InputStream inputStream = (InputStream) result.get("stream"); - byte[] b = inputStream.readAllBytes(); + byte[] b; + try (InputStream inputStream = (InputStream) result.get("stream")) { + b = inputStream.readAllBytes(); + } System.out.println(wt + "_time : " + timer.getTime()); System.out.println(wt + "_size : " + b.length); return b; diff --git a/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java b/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java index ad5b474b771..5433c11fd1d 100644 --- a/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java +++ b/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; @@ -41,6 +42,7 @@ import org.apache.solr.client.solrj.response.V2Response; import org.apache.solr.cloud.SolrCloudTestCase; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.RetryUtil; import org.apache.solr.util.stats.MetricUtils; import org.apache.solr.util.tracing.TraceUtils; import org.junit.AfterClass; @@ -92,7 +94,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { // Indexing cloudClient.add(COLLECTION, sdoc("id", "1")); - var finishedSpans = getAndClearSpans(); + var finishedSpans = getAndClearSpans(1); finishedSpans.removeIf( span -> span.getAttributes().get(TraceUtils.TAG_HTTP_URL) == null @@ -111,7 +113,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { // Searching cloudClient.query(COLLECTION, new SolrQuery("*:*")); - finishedSpans = getAndClearSpans(); + finishedSpans = getAndClearSpans(1); finishedSpans.removeIf( span -> span.getAttributes().get(TraceUtils.TAG_HTTP_URL) == null @@ -138,11 +140,11 @@ public class TestDistributedTracing extends SolrCloudTestCase { request.setResponseParser(new InputStreamResponseParser(MetricUtils.PROMETHEUS_METRICS_WT)); NamedList<Object> rsp = cloudClient.request(request); ((InputStream) rsp.get("stream")).close(); - var finishedSpans = getAndClearSpans(); + var finishedSpans = getAndClearSpans(1); assertEquals("get:/admin/metrics", finishedSpans.get(0).getName()); CollectionAdminRequest.listCollections(cloudClient); - finishedSpans = getAndClearSpans(); + finishedSpans = getAndClearSpans(1); assertEquals("list:/admin/collections", finishedSpans.get(0).getName()); } @@ -155,7 +157,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { .withPayload("{}") .build() .process(cloudClient); - var finishedSpans = getAndClearSpans(); + var finishedSpans = getAndClearSpans(1); assertEquals("post:/collections/{collection}/reload", finishedSpans.get(0).getName()); assertCollectionName(finishedSpans.get(0), COLLECTION); @@ -165,7 +167,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { .withParams(params("commit", "true")) .build() .process(cloudClient); - finishedSpans = getAndClearSpans(); + finishedSpans = getAndClearSpans(1); assertEquals("post:/c/{collection}/update/json", finishedSpans.get(0).getName()); assertCollectionName(finishedSpans.get(0), COLLECTION); @@ -175,7 +177,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { .withParams(params("q", "id:9")) .build() .process(cloudClient); - finishedSpans = getAndClearSpans(); + finishedSpans = getAndClearSpans(1); assertEquals("get:/c/{collection}/select", finishedSpans.get(0).getName()); assertCollectionName(finishedSpans.get(0), COLLECTION); assertEquals(1, ((SolrDocumentList) v2Response.getResponse().get("response")).getNumFound()); @@ -191,7 +193,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { CollectionAdminRequest.ColStatus a1 = CollectionAdminRequest.collectionStatus(COLLECTION); CollectionAdminResponse r1 = a1.process(cluster.getSolrClient()); assertEquals(0, r1.getStatus()); - var finishedSpans = getAndClearSpans(); + var finishedSpans = getAndClearSpans(1); var parentTraceId = getRootTraceId(finishedSpans); for (var span : finishedSpans) { if (isRootSpan(span)) { @@ -239,7 +241,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { // db.instance=testInternalCollectionApiCommands_shard2_replica_n4 // db.instance=testInternalCollectionApiCommands_shard1_replica_n2 - var finishedSpans = getAndClearSpans(); + var finishedSpans = getAndClearSpans(1); var s0 = finishedSpans.remove(0); assertCollectionName(s0, collection); assertEquals("create:/admin/collections", s0.getName()); @@ -280,7 +282,7 @@ public class TestDistributedTracing extends SolrCloudTestCase { // db.instance=testInternalCollectionApiCommands_shard2_replica_n4 // db.instance=testInternalCollectionApiCommands_shard1_replica_n6 - var finishedSpans = getAndClearSpans(); + var finishedSpans = getAndClearSpans(1); var s0 = finishedSpans.remove(0); assertCollectionName(s0, collection); assertEquals("delete:/admin/collections", s0.getName()); @@ -327,7 +329,21 @@ public class TestDistributedTracing extends SolrCloudTestCase { } static List<SpanData> getAndClearSpans() { + return getAndClearSpans(0); + } + + static List<SpanData> getAndClearSpans(int minExpected) { InMemorySpanExporter exporter = CustomTestOtelTracerConfigurator.getInMemorySpanExporter(); + try { + RetryUtil.retryUntil( + "Timed out waiting for " + minExpected + " span(s)", + 250, + 20, + TimeUnit.MILLISECONDS, + () -> exporter.getFinishedSpanItems().size() >= minExpected); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } List<SpanData> result = new ArrayList<>(exporter.getFinishedSpanItems()); Collections.reverse(result); // nicer to see spans chronologically exporter.reset(); diff --git a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java index 82b3fd8e83a..5badd40211a 100644 --- a/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java +++ b/solr/solrj-jetty/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java @@ -34,13 +34,13 @@ import org.apache.solr.util.SolrJettyTestRule; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; -import org.junit.ClassRule; +import org.junit.Rule; @LogLevel("org.eclipse.jetty.client=DEBUG;org.eclipse.jetty.util=DEBUG") @SolrTestCaseJ4.SuppressSSL public class HttpJettySolrClientCompatibilityTest extends SolrTestCaseJ4 { - @ClassRule public static SolrJettyTestRule solrTestRule = new SolrJettyTestRule(); + @Rule public SolrJettyTestRule solrTestRule = new SolrJettyTestRule(); public void testSystemPropertyFlag() { System.setProperty("solr.http1", "true"); @@ -74,8 +74,6 @@ public class HttpJettySolrClientCompatibilityTest extends SolrTestCaseJ4 { client.query(new SolrQuery("*:*"), SolrRequest.METHOD.GET); } catch (RemoteSolrException ignored) { } - } finally { - solrTestRule.reset(); } } @@ -100,8 +98,6 @@ public class HttpJettySolrClientCompatibilityTest extends SolrTestCaseJ4 { client.query(new SolrQuery("*:*"), SolrRequest.METHOD.GET); } catch (RemoteSolrException ignored) { } - } finally { - solrTestRule.reset(); } } @@ -133,8 +129,6 @@ public class HttpJettySolrClientCompatibilityTest extends SolrTestCaseJ4 { } catch (SolrServerException e) { // expected } - } finally { - solrTestRule.reset(); } } } diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java index 60005d50e5a..bf06c491d03 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java @@ -28,6 +28,7 @@ import java.util.Date; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrInputField; @@ -69,7 +70,7 @@ public class ClientUtils { SolrRequest<?> solrRequest, String serverRootUrl, String collection) throws MalformedURLException { - String basePath = serverRootUrl; + String basePath = Objects.requireNonNull(serverRootUrl); if (solrRequest.getApiVersion() == SolrRequest.ApiVersion.V2) { basePath = addNormalV2ApiRoot(basePath); } diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java b/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java index d4b81d99ea6..e2062527680 100644 --- a/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java +++ b/solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java @@ -90,6 +90,7 @@ import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.ExecutorUtil; import org.apache.solr.common.util.IOUtils; import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.RetryUtil; import org.apache.solr.common.util.SolrNamedThreadFactory; import org.apache.solr.common.util.StrUtils; import org.apache.solr.common.util.TimeSource; @@ -378,12 +379,17 @@ public abstract class AbstractFullDistribZkTestBase extends BaseDistributedSearc controlJettyDir, useJettyDataDir ? getDataDir(testDir + "/control/data") : null); controlJetty.start(); try (CloudSolrClient client = createCloudClient("control_collection")) { - assertEquals( - 0, - CollectionAdminRequest.createCollection("control_collection", "conf1", 1, 1) - .setCreateNodeSet(controlJetty.getNodeName()) - .process(client) - .getStatus()); + RetryUtil.retryOnException( + SolrException.class, + 30000, + 1000, + () -> + assertEquals( + 0, + CollectionAdminRequest.createCollection("control_collection", "conf1", 1, 1) + .setCreateNodeSet(controlJetty.getNodeName()) + .process(client) + .getStatus())); waitForActiveReplicaCount(client, "control_collection", 1); } @@ -2261,12 +2267,14 @@ public abstract class AbstractFullDistribZkTestBase extends BaseDistributedSearc customThreadPool.execute(() -> IOUtils.closeQuietly(cloudClient)); - ExecutorUtil.shutdownAndAwaitTermination(customThreadPool); - - coreClients.clear(); - solrClientByCollection.clear(); + try { + ExecutorUtil.shutdownAndAwaitTermination(customThreadPool); + } finally { + coreClients.clear(); + solrClientByCollection.clear(); - super.destroyServers(); + super.destroyServers(); + } } @Override diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java index f355b057aa4..10906c15e66 100644 --- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java +++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java @@ -579,6 +579,10 @@ public class JettySolrRunner implements SolrBackend { IOUtils.closeQuietly(jettySolrClient); jettySolrClient = null; + if (enableProxy) { + proxy.close(); + } + QueuedThreadPool qtp = (QueuedThreadPool) server.getThreadPool(); ReservedThreadExecutor rte = qtp.getBean(ReservedThreadExecutor.class); @@ -620,10 +624,6 @@ public class JettySolrRunner implements SolrBackend { } while (!server.isStopped()); } finally { - if (enableProxy) { - proxy.close(); - } - if (prevContext != null) { MDC.setContextMap(prevContext); } else { diff --git a/solr/test-framework/src/java/org/apache/solr/util/SocketProxy.java b/solr/test-framework/src/java/org/apache/solr/util/SocketProxy.java index 2e0c6ea388b..2ee70a4f9ad 100644 --- a/solr/test-framework/src/java/org/apache/solr/util/SocketProxy.java +++ b/solr/test-framework/src/java/org/apache/solr/util/SocketProxy.java @@ -37,8 +37,30 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Kindly borrowed the idea and base implementation from the ActiveMQ project; useful for blocking - * traffic on a specified port. + * A TCP-level proxy used in tests to simulate network partitions. It listens on its own port and + * forwards all traffic to the target Jetty server. Key operations: + * + * <ul> + * <li>{@link #close()} — drops all connections and stops accepting new ones (simulates connection + * refused) + * <li>{@link #pause()} / {@link #goOn()} — freezes all data flow while keeping sockets open + * (simulates a hung network) + * <li>{@link #reopen()} — restores connectivity on the same port after a {@link #close()} + * </ul> + * + * <p>The proxy sits <em>in front of</em> Jetty by design: the Jetty server and ZooKeeper heartbeats + * continue running normally while the proxy makes the node appear unreachable to clients. A Jetty + * Handler cannot achieve this because it requires a TCP connection to already exist. + * + * <p>Implementation note: each accepted connection creates a {@link Bridge} backed by two {@link + * Bridge.Pump} threads (one per direction), using blocking I/O borrowed from ActiveMQ. + * + * <p>TODO: Replace the blocking-I/O {@link Bridge.Pump} threads with a NIO {@code + * java.nio.channels.Selector}-based implementation. The current model spawns 2 OS threads per + * connection; under test load with many concurrent connections this is wasteful, and the {@link + * #PUMP_SOCKET_TIMEOUT_MS} timeout means threads linger for up to 100 s after connections are + * dropped. A single-selector loop would handle all connections with O(1) threads, as Jetty itself + * does internally. */ public class SocketProxy { @@ -81,6 +103,7 @@ public class SocketProxy { this.usesSSL = useSSL; serverSocket = createServerSocket(useSSL); serverSocket.setReuseAddress(true); + serverSocket.setSoTimeout(1000); if (receiveBufferSize > 0) { serverSocket.setReceiveBufferSize(receiveBufferSize); } @@ -188,6 +211,7 @@ public class SocketProxy { } serverSocket = createServerSocket(usesSSL); serverSocket.setReuseAddress(true); + serverSocket.setSoTimeout(1000); if (receiveBufferSize > 0) { serverSocket.setReceiveBufferSize(receiveBufferSize); }
