jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more

Home Page:https://eclipse.dev/jetty

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Client hangs despite setting timeout

garydgregory opened this issue · comments

Jetty version(s)
12.0.10-SNAPSHOT

Jetty Environment
ee9

Java version/vendor (use: java -version)

openjdk version "17.0.11" 2024-04-16
OpenJDK Runtime Environment Temurin-17.0.11+9 (build 17.0.11+9)
OpenJDK 64-Bit Server VM Temurin-17.0.11+9 (build 17.0.11+9, mixed mode, sharing)

OS type/version
Microsoft Windows [Version 10.0.19045.4412]

Description
To even give the test below a chance to run, I must apply PR #11891 to avoid NPEs on the server side. For me, the test hangs after between 90 and 400 iterations. If it does not, you can stress your CPU to 92%+ with CPU Stres.

Based on some println()s here and there (logging can affect timings):
The hang is cause by CompletableFuture.get() never returning in listener.send().get() in HttpRequest.send():

    public ContentResponse send() throws InterruptedException, TimeoutException, ExecutionException
    {
        try
        {
            CompletableResponseListener listener = new CompletableResponseListener(this);
            return listener.send().get();
        }
    ...

get never returns because CompletableResponseListener.onComplete(Result) is never called,
because ResponseListeners.notifyComplete(Response.CompleteListener, Result) is never called,
because ResponseListeners.notifyComplete(Result) is never called.

How to reproduce?

diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java
index 571b223307..1d171ffa17 100644
--- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java
+++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServletTest.java
@@ -41,6 +41,7 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 import java.util.zip.ZipEntry;
@@ -289,6 +290,72 @@ public class AsyncMiddleManServletTest
         }
     }

+    /**
+     * Tests that we do not hang processing sequenced client requests that are fast on the server-side;
+     * requires testServletOnContinueNullPointerException() to pass as a prerequisite.
+     * <p>
+     * Commenting out client EXPECT processing allows the test not to hang.
+     * </p>
+     * <p>
+     * It does not always happen, it may be related to how busy the CPU is; using "CPU Stres" from
+     * https://learn.microsoft.com/en-us/sysinternals/downloads/cpustres helps make the failure happen more often when
+     * I peg the CPU at 92% busy or above.
+     * </p>
+     * @throws Exception on a test failure.
+     */
+    @Test
+    public void testServletExpect100Hanging() throws Exception
+    {
+        startServer(new HttpServlet()
+        {
+            private AtomicInteger count = new AtomicInteger();
+
+            @Override
+            protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
+            {
+                // Adding logging may slow things down enough to not hang
+                // System.out.printf("Origin servicing %d%n", count.incrementAndGet());
+                // Send the 100 Continue.
+                ServletInputStream input = request.getInputStream();
+                // Echo the content.
+                IO.copy(input, response.getOutputStream());
+                // Slowing down the origin allows the test to not hang sometimes
+//                try {
+//                    Thread.sleep(50);
+//                } catch (InterruptedException e) {
+//                    e.printStackTrace();
+//                }
+            }
+        });
+        startProxy(new AsyncMiddleManServlet() {
+            private AtomicInteger count = new AtomicInteger();
+
+            @Override
+            protected void service(HttpServletRequest clientRequest, HttpServletResponse proxyResponse) throws ServletException, IOException {
+                // Adding logging may slow things down enough to not hang
+                // System.out.printf("Middle servicing %d%n", count.incrementAndGet());
+                super.service(clientRequest, proxyResponse);
+            }
+        });
+        startClient();
+
+        // loop to attempt increase odds of hanging
+        for (int i = 1; i <= 500; i++)
+        {
+            long start = System.currentTimeMillis();
+            System.out.printf("Sending %d", i);
+            ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort())
+                    .timeout(5, TimeUnit.SECONDS)
+                    // Commenting out EXPECT processing allows the test not to hang.
+                    .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE))
+                    // Hangs with small payloads, 10k won't hang, so test smallest payload.
+                    .body(new BytesRequestContent("a"))
+                    .send();
+            assertEquals(200, response.getStatus());
+            System.out.printf(" %,d%n", System.currentTimeMillis() - start);
+        }
+    }
+
     @Test
     public void testClientRequestSmallContentKnownLengthGzipped() throws Exception
     {

@lorban any insight on this one?

We started investigating with @sbordet and it looks like there is a race condition in the proxy code. More work is needed to really understand what's going on and to come up with a fix.

Stay tuned.

@lorban
Thank you for the update! Looking forward to a solution.