This is an automated email from the ASF dual-hosted git repository.

hossman pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new fe7fe7966a6 SOLR-17744: Solr now enables Jetty's Graceful Shutdown 
features to prevent client connections from being abruptly terminated on 
orderly shutdown
fe7fe7966a6 is described below

commit fe7fe7966a654802f70f6853095ff427ee17d318
Author: Chris Hostetter <[email protected]>
AuthorDate: Mon Apr 28 09:32:32 2025 -0700

    SOLR-17744: Solr now enables Jetty's Graceful Shutdown features to prevent 
client connections from being abruptly terminated on orderly shutdown
    
    (cherry picked from commit d0d71a7b38f87f3c70551678b2eab62d925483ef)
---
 solr/CHANGES.txt                                   |   2 +
 solr/bin/solr                                      |   5 +
 .../test/org/apache/solr/cloud/SplitShardTest.java |   1 +
 .../solr/cloud/TestGracefulJettyShutdown.java      | 161 +++++++++++++++++++++
 solr/server/etc/jetty-graceful.xml                 |  22 +++
 solr/server/modules/graceful.mod                   |   8 +
 .../apache/solr/cloud/MiniSolrCloudCluster.java    |   2 +
 .../org/apache/solr/embedded/JettySolrRunner.java  |   7 +
 8 files changed, 208 insertions(+)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 3a6ad50b554..7fc58f71438 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -49,6 +49,8 @@ Improvements
 
 * SOLR-17732: Score-based return fields other than "score" can now be returned 
in distributed queries. (Houston Putman)
 
+* SOLR-17744: Solr now enables Jetty's Graceful Shutdown features to prevent 
client connections from being abruptly terminated on orderly shutdown (hossman)
+
 Optimizations
 ---------------------
 * SOLR-17578: Remove ZkController internal core supplier, for slightly faster 
reconnection after Zookeeper session loss. (Pierre Salagnac)
diff --git a/solr/bin/solr b/solr/bin/solr
index 0759d8ed7db..a49cc54e537 100755
--- a/solr/bin/solr
+++ b/solr/bin/solr
@@ -304,6 +304,11 @@ else
 fi
 export SOLR_URL_SCHEME
 
+# Gracefully wait for existing requests on shutdown
+if [ "${SOLR_JETTY_GRACEFUL:-true}" == "true" ]; then
+  SOLR_JETTY_CONFIG+=("--module=graceful")
+fi
+
 # Requestlog options
 if [ "${SOLR_REQUESTLOG_ENABLED:-true}" == "true" ]; then
   SOLR_JETTY_CONFIG+=("--module=requestlog")
diff --git a/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java 
b/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java
index 1210b5169ed..b8585de0c87 100644
--- a/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java
@@ -390,6 +390,7 @@ public class SplitShardTest extends SolrCloudTestCase {
         (s, replica) -> {
           if (replica.getNodeName().equals(jetty.getNodeName())
               && !replica.isLeader()
+              && replica.getState().equals(Replica.State.ACTIVE)
               && set.contains(replica.shard)) {
             set.remove(replica.shard);
           }
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/TestGracefulJettyShutdown.java 
b/solr/core/src/test/org/apache/solr/cloud/TestGracefulJettyShutdown.java
new file mode 100644
index 00000000000..44caa313c52
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cloud/TestGracefulJettyShutdown.java
@@ -0,0 +1,161 @@
+/*
+ * 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.cloud;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.embedded.JettyConfig;
+import org.apache.solr.embedded.JettySolrRunner;
+import org.apache.solr.handler.component.SearchHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TestGracefulJettyShutdown extends SolrTestCaseJ4 {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  public void testSingleShardInFlightRequestsDuringShutDown() throws Exception 
{
+    final String collection = getSaferTestName();
+    final String handler = "/foo";
+
+    final Semaphore handlerGate = new Semaphore(0);
+    final Semaphore handlerSignal = new Semaphore(0);
+
+    final ExecutorService exec = 
ExecutorUtil.newMDCAwareCachedThreadPool("client-requests");
+    final MiniSolrCloudCluster cluster =
+        new MiniSolrCloudCluster(1, createTempDir(), 
JettyConfig.builder().build());
+    try {
+      assertTrue(
+          CollectionAdminRequest.createCollection(collection, "_default", 1, 1)
+              .process(cluster.getSolrClient())
+              .isSuccess());
+
+      final JettySolrRunner nodeToStop = cluster.getJettySolrRunner(0);
+
+      // register our custom handler with "all" (one) of our SolrCores
+      for (String coreName : 
nodeToStop.getCoreContainer().getLoadedCoreNames()) {
+        try (SolrCore core = nodeToStop.getCoreContainer().getCore(coreName)) {
+          final BlockingSearchHandler h = new 
BlockingSearchHandler(handlerGate, handlerSignal);
+          h.inform(core);
+          core.registerRequestHandler(handler, h);
+        }
+      }
+
+      final CloudSolrClient cloudClient = cluster.getSolrClient();
+
+      // add a few docs...
+      cloudClient.add(collection, sdoc("id", "xxx", "foo_s", "aaa"));
+      cloudClient.add(collection, sdoc("id", "yyy", "foo_s", "bbb"));
+      cloudClient.add(collection, sdoc("id", "zzz", "foo_s", "aaa"));
+      cloudClient.commit(collection);
+
+      final List<Future<QueryResponse>> results = new ArrayList<>(13);
+
+      try (SolrClient jettyClient = nodeToStop.newClient()) {
+        final QueryRequest req = new QueryRequest(params("q", "foo_s:aaa"));
+        req.setPath(handler);
+
+        // check inflight requests using both clients...
+        for (SolrClient client : Arrays.asList(cloudClient, jettyClient)) {
+          results.add(
+              exec.submit(
+                  () -> {
+                    return req.process(client, collection);
+                  }));
+        }
+
+        // wait for our handlers to indicate they have recieved the requests 
and started processing
+        log.info("Waiting for signals from both requests");
+        assertTrue(handlerSignal.tryAcquire(2, 300, TimeUnit.SECONDS)); // 
safety valve
+
+        // stop our node (via executor so it doesn't block) and open the gate 
for our handlers
+        log.info("Stopping jetty node");
+        final Future<Boolean> stopped =
+            exec.submit(
+                () -> {
+                  nodeToStop.stop();
+                  return true;
+                });
+        log.info("Releasing gate for requests");
+        handlerGate.release(2);
+        log.info("Released gate for requests");
+
+        // confirm success of requests
+        assertEquals(2, results.size());
+        for (Future<QueryResponse> f : results) {
+          final QueryResponse rsp = f.get(300, TimeUnit.SECONDS); // safety 
valve
+          assertEquals(2, rsp.getResults().getNumFound());
+        }
+        assertTrue(stopped.get(300, TimeUnit.SECONDS)); // safety valve
+      }
+
+    } finally {
+      handlerGate.release(9999999); // safety valve
+      handlerSignal.release(9999999); // safety valve
+      cluster.shutdown();
+      ExecutorUtil.shutdownAndAwaitTermination(exec);
+    }
+  }
+
+  /**
+   * On every request, prior to handling, releases a ticket to it's signal 
Semaphre, and then blocks
+   * until it can acquire a ticket from it's gate semaphore. Has a safety 
valve that gives up on the
+   * gate after 300 seconds
+   */
+  private static final class BlockingSearchHandler extends SearchHandler {
+    private final Semaphore gate;
+    private final Semaphore signal;
+
+    public BlockingSearchHandler(final Semaphore gate, final Semaphore signal) 
{
+      super();
+      this.gate = gate;
+      this.signal = signal;
+      super.init(new NamedList<>());
+    }
+
+    @Override
+    public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
+      log.info("Starting request");
+      signal.release();
+      if (gate.tryAcquire(300, TimeUnit.SECONDS)) {
+        super.handleRequestBody(req, rsp);
+      } else {
+        log.error("Gate safety valve timeout");
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Gate 
timeout");
+      }
+      log.info("Finishing request");
+    }
+  }
+}
diff --git a/solr/server/etc/jetty-graceful.xml 
b/solr/server/etc/jetty-graceful.xml
new file mode 100644
index 00000000000..34ec9316445
--- /dev/null
+++ b/solr/server/etc/jetty-graceful.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" 
"https://www.eclipse.org/jetty/configure_10_0.dtd";>
+
+<!-- =============================================================== -->
+<!-- Configure Jetty to finish existing requests on STOP.            -->
+<!--                                                                 -->
+<!-- This is inspired by the "official" Jetty 12 graceful module,    -->
+<!-- adapted to use StatisticsHandler for Jetty 10 and 11.           -->
+<!--                                                                 -->
+<!-- IF THIS IS MODIFIED, JettySolrRunner MUST BE MODIFIED!          -->
+<!-- =============================================================== -->
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+  <Call name="insertHandler">
+    <Arg>
+      <New id="GracefulHandler" 
class="org.eclipse.jetty.server.handler.StatisticsHandler">
+        <Set name="gracefulShutdownWaitsForRequests">true</Set>
+      </New>
+    </Arg>
+  </Call>
+  <Set name="stopTimeout"><Property name="solr.jetty.stop.timeout" 
default="15000"/></Set>
+</Configure>
diff --git a/solr/server/modules/graceful.mod b/solr/server/modules/graceful.mod
new file mode 100644
index 00000000000..581ebca898f
--- /dev/null
+++ b/solr/server/modules/graceful.mod
@@ -0,0 +1,8 @@
+[description]
+Enables Graceful processing of requests on STOP.  Inspired by graceful.mod for 
Jetty-12, this works with Jetty 10 and 11.
+
+[depend]
+server
+
+[xml]
+etc/jetty-graceful.xml
diff --git 
a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java 
b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
index 5789b6b42f2..f17ec1d0fe5 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
@@ -853,9 +853,11 @@ public class MiniSolrCloudCluster {
       try {
         future.get();
       } catch (ExecutionException e) {
+        log.error(message, e);
         parsed.addSuppressed(e.getCause());
         ok = false;
       } catch (InterruptedException e) {
+        log.error(message, e);
         Thread.interrupted();
         throw e;
       }
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 92d8e4b2efc..76dd1add5f7 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
@@ -87,6 +87,7 @@ import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
 import org.eclipse.jetty.server.SslConnectionFactory;
 import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
 import org.eclipse.jetty.server.handler.gzip.GzipHandler;
 import org.eclipse.jetty.server.session.DefaultSessionIdManager;
 import org.eclipse.jetty.servlet.FilterHolder;
@@ -438,6 +439,12 @@ public class JettySolrRunner {
     gzipHandler.setIncludedMethods("GET");
 
     server.setHandler(gzipHandler);
+
+    // Mimic "graceful.mod"
+    final StatisticsHandler graceful = new StatisticsHandler();
+    graceful.setGracefulShutdownWaitsForRequests(true);
+    server.insertHandler(graceful);
+    server.setStopTimeout(15 * 1000);
   }
 
   /**

Reply via email to