From 01b447083bcf3bd2b4ca8e1c319475907324e523 Mon Sep 17 00:00:00 2001 From: weishao Date: Thu, 20 Aug 2026 17:13:26 +0800 Subject: [PATCH 1/3] YARN-11845. WebAppProxy add Connection close header to prevent CLOSE_WAIT socket buildup --- .../server/webproxy/WebAppProxyServlet.java | 5 +- .../webproxy/TestWebAppProxyServlet.java | 91 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java index 21e2570b9609dd..10938c6aa68cd3 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java @@ -310,7 +310,10 @@ private void proxyLink(final HttpServletRequest req, base.setHeader(name, value); } } - + // Tell the AM/history server to close the connection after the response + // so that the proxied connection is not left in CLOSE_WAIT state on the + // proxy side (SOHU-HADOOP-11). + base.setHeader("Connection", "close"); String user = req.getRemoteUser(); if (user != null && !user.isEmpty()) { base.setHeader("Cookie", diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java index b1349df26de2b9..0d8b8b2ef818f4 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java @@ -97,6 +97,10 @@ public class TestWebAppProxyServlet { private static int numberOfHeaders = 0; private static final String UNKNOWN_HEADER = "Unknown-Header"; private static boolean hasUnknownHeader = false; + // Value of the Connection header received by the proxied server (the + // embedded TestServlet backend). Used to verify the proxy sends + // 'Connection: close' (SOHU-HADOOP-11). + private static String proxiedConnectionHeader = null; Configuration configuration = new Configuration(); @@ -129,6 +133,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int numHeaders = 0; hasUnknownHeader = false; + proxiedConnectionHeader = req.getHeader("Connection"); @SuppressWarnings("unchecked") Enumeration names = req.getHeaderNames(); while(names.hasMoreElements()) { @@ -145,6 +150,23 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + proxiedConnectionHeader = req.getHeader("Connection"); + InputStream is = req.getInputStream(); + OutputStream os = resp.getOutputStream(); + int c = is.read(); + while (c > -1) { + os.write(c); + c = is.read(); + } + is.close(); + os.close(); + resp.setStatus(HttpServletResponse.SC_OK); + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + proxiedConnectionHeader = req.getHeader("Connection"); InputStream is = req.getInputStream(); OutputStream os = resp.getOutputStream(); int c = is.read(); @@ -481,6 +503,75 @@ void testWebAppProxyPassThroughHeaders() throws Exception { } } + /** + * Test that the proxy tells the proxied server (AM / history server) to + * close the connection after each response. The proxy builds a new + * HttpClient per request, so if the backend connection is kept alive the + * socket is left in CLOSE_WAIT state on the proxy host until GC reclaims + * it. Setting the 'Connection: close' request header makes the backend + * close the connection as soon as the response is sent (SOHU-HADOOP-11). + */ + @Test + @Timeout(5000) + void testWebAppProxyConnectionCloseHeader() throws Exception { + Configuration configuration = new Configuration(); + configuration.set(YarnConfiguration.PROXY_ADDRESS, "localhost:9093"); + configuration.setInt("hadoop.http.max.threads", 10); + WebAppProxyServerForTest proxy = new WebAppProxyServerForTest(); + proxy.init(configuration); + proxy.start(); + + int proxyPort = proxy.proxy.proxyServer.getConnectorAddress(0).getPort(); + proxy.proxy.appReportFetcher.answer = 0; + + try { + // GET: the proxied request must carry 'Connection: close' + URL url = new URL("http://localhost:" + proxyPort + + "/proxy/application_00_0"); + HttpURLConnection proxyConn = (HttpURLConnection) url.openConnection(); + proxyConn.setRequestProperty("Cookie", + "checked_application_0_0000=true"); + proxyConn.connect(); + assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode()); + assertNotNull(proxiedConnectionHeader, + "The proxied server did not receive a Connection header at all"); + assertEquals("close", proxiedConnectionHeader.trim().toLowerCase(), + "The proxy must send 'Connection: close' to the proxied server " + + "so the backend closes the connection instead of leaving it " + + "in CLOSE_WAIT state (SOHU-HADOOP-11)"); + + // even if the incoming client request asked for keep-alive, the proxy + // must still ask the backend to close the connection + proxyConn = (HttpURLConnection) url.openConnection(); + proxyConn.setRequestProperty("Cookie", + "checked_application_0_0000=true"); + proxyConn.setRequestProperty("Connection", "keep-alive"); + proxyConn.connect(); + assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode()); + assertEquals("close", proxiedConnectionHeader.trim().toLowerCase(), + "The proxy must override a client 'Connection: keep-alive' " + + "request header with 'close' (SOHU-HADOOP-11)"); + + // PUT: the header must be set on PUT requests as well + proxyConn = (HttpURLConnection) url.openConnection(); + proxyConn.setRequestMethod("PUT"); + proxyConn.setDoOutput(true); + proxyConn.setRequestProperty("Cookie", + "checked_application_0_0000=true"); + proxyConn.connect(); + byte[] body = "SOHU-HADOOP-11".getBytes(StandardCharsets.UTF_8); + try (OutputStream os = proxyConn.getOutputStream()) { + os.write(body); + } + assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode()); + assertEquals("close", proxiedConnectionHeader.trim().toLowerCase(), + "The proxy must send 'Connection: close' on PUT requests too " + + "(SOHU-HADOOP-11)"); + } finally { + proxy.close(); + } + } + /** * Test main method of WebAppProxyServer From a8e08557f66d9ab300f0c3b5600481cd52598a5d Mon Sep 17 00:00:00 2001 From: sohurdc Date: Fri, 21 Aug 2026 10:45:30 +0800 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java index 10938c6aa68cd3..f44050a3cc1596 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java @@ -312,7 +312,7 @@ private void proxyLink(final HttpServletRequest req, } // Tell the AM/history server to close the connection after the response // so that the proxied connection is not left in CLOSE_WAIT state on the - // proxy side (SOHU-HADOOP-11). + // proxy side (YARN-11845). base.setHeader("Connection", "close"); String user = req.getRemoteUser(); if (user != null && !user.isEmpty()) { From 9e39e8526dabfd1548ee2be9d5a748f240b52b5a Mon Sep 17 00:00:00 2001 From: sohurdc Date: Fri, 21 Aug 2026 11:18:00 +0800 Subject: [PATCH 3/3] YARN-11845 Test bug fix --- .../webproxy/TestWebAppProxyServlet.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java index 0d8b8b2ef818f4..f5143e8a70fbab 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java @@ -99,8 +99,8 @@ public class TestWebAppProxyServlet { private static boolean hasUnknownHeader = false; // Value of the Connection header received by the proxied server (the // embedded TestServlet backend). Used to verify the proxy sends - // 'Connection: close' (SOHU-HADOOP-11). - private static String proxiedConnectionHeader = null; + // 'Connection: close' (YARN-11845). + private static volatile String proxiedConnectionHeader = null; Configuration configuration = new Configuration(); @@ -509,7 +509,7 @@ void testWebAppProxyPassThroughHeaders() throws Exception { * HttpClient per request, so if the backend connection is kept alive the * socket is left in CLOSE_WAIT state on the proxy host until GC reclaims * it. Setting the 'Connection: close' request header makes the backend - * close the connection as soon as the response is sent (SOHU-HADOOP-11). + * close the connection as soon as the response is sent (YARN-11845). */ @Test @Timeout(5000) @@ -526,6 +526,7 @@ void testWebAppProxyConnectionCloseHeader() throws Exception { try { // GET: the proxied request must carry 'Connection: close' + proxiedConnectionHeader = null; URL url = new URL("http://localhost:" + proxyPort + "/proxy/application_00_0"); HttpURLConnection proxyConn = (HttpURLConnection) url.openConnection(); @@ -535,38 +536,44 @@ void testWebAppProxyConnectionCloseHeader() throws Exception { assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode()); assertNotNull(proxiedConnectionHeader, "The proxied server did not receive a Connection header at all"); - assertEquals("close", proxiedConnectionHeader.trim().toLowerCase(), + assertEquals("close", StringUtils.toLowerCase(proxiedConnectionHeader.trim()), "The proxy must send 'Connection: close' to the proxied server " + "so the backend closes the connection instead of leaving it " - + "in CLOSE_WAIT state (SOHU-HADOOP-11)"); + + "in CLOSE_WAIT state (YARN-11845)"); // even if the incoming client request asked for keep-alive, the proxy // must still ask the backend to close the connection + proxiedConnectionHeader = null; proxyConn = (HttpURLConnection) url.openConnection(); proxyConn.setRequestProperty("Cookie", "checked_application_0_0000=true"); proxyConn.setRequestProperty("Connection", "keep-alive"); proxyConn.connect(); assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode()); - assertEquals("close", proxiedConnectionHeader.trim().toLowerCase(), + assertNotNull(proxiedConnectionHeader, + "The proxied server did not receive a Connection header at all"); + assertEquals("close", StringUtils.toLowerCase(proxiedConnectionHeader.trim()), "The proxy must override a client 'Connection: keep-alive' " - + "request header with 'close' (SOHU-HADOOP-11)"); + + "request header with 'close' (YARN-11845)"); // PUT: the header must be set on PUT requests as well + proxiedConnectionHeader = null; proxyConn = (HttpURLConnection) url.openConnection(); proxyConn.setRequestMethod("PUT"); proxyConn.setDoOutput(true); proxyConn.setRequestProperty("Cookie", "checked_application_0_0000=true"); proxyConn.connect(); - byte[] body = "SOHU-HADOOP-11".getBytes(StandardCharsets.UTF_8); + byte[] body = "test-body".getBytes(StandardCharsets.UTF_8); try (OutputStream os = proxyConn.getOutputStream()) { os.write(body); } assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode()); - assertEquals("close", proxiedConnectionHeader.trim().toLowerCase(), + assertNotNull(proxiedConnectionHeader, + "The proxied server did not receive a Connection header at all"); + assertEquals("close", StringUtils.toLowerCase(proxiedConnectionHeader.trim()), "The proxy must send 'Connection: close' on PUT requests too " - + "(SOHU-HADOOP-11)"); + + "(YARN-11845)"); } finally { proxy.close(); }