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

Reply via email to