Browse Source

YARN-10720. YARN WebAppProxyServlet should support connection timeout to prevent proxy server from hanging. Contributed by Qi Zhu. (#4103)

(cherry picked from commit a0deda1a777d8967fb8c08ac976543cda895773d)

 Conflicts:
	hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java
	hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml
	hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java
	hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java

(cherry picked from commit dbeb41b46a2cef9a48983add9c3b191915bd6de5)

 Conflicts:
	hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java

Co-authored-by: Peter Bacsko <pbacsko@cloudera.com>
Akira Ajisaka 3 years ago
parent
commit
37a7b9f934

+ 13 - 0
hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/java/org/apache/hadoop/yarn/conf/YarnConfiguration.java

@@ -1882,6 +1882,19 @@ public class YarnConfiguration extends Configuration {
   public static final long DEFAULT_RM_APPLICATION_MONITOR_INTERVAL_MS =
   public static final long DEFAULT_RM_APPLICATION_MONITOR_INTERVAL_MS =
       3000;
       3000;
 
 
+  // If the proxy connection time enabled.
+  public static final String RM_PROXY_TIMEOUT_ENABLED =
+      RM_PREFIX + "proxy.timeout.enabled";
+
+  public static final boolean DEFALUT_RM_PROXY_TIMEOUT_ENABLED =
+      true;
+
+  public static final String RM_PROXY_CONNECTION_TIMEOUT =
+      RM_PREFIX + "proxy.connection.timeout";
+
+  public static final int DEFAULT_RM_PROXY_CONNECTION_TIMEOUT =
+      60000;
+
   /**
   /**
    * Interval of time the linux container executor should try cleaning up
    * Interval of time the linux container executor should try cleaning up
    * cgroups entry when cleaning up a container. This is required due to what 
    * cgroups entry when cleaning up a container. This is required due to what 

+ 12 - 0
hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/resources/yarn-default.xml

@@ -2024,6 +2024,18 @@
      <value/>
      <value/>
   </property>
   </property>
 
 
+  <property>
+    <description>Enable the web proxy connection timeout, default is enabled.</description>
+    <name>yarn.resourcemanager.proxy.timeout.enabled</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <description>The web proxy connection timeout.</description>
+    <name>yarn.resourcemanager.proxy.connection.timeout</name>
+    <value>60000</value>
+  </property>
+
   <!-- Applications' Configuration -->
   <!-- Applications' Configuration -->
 
 
   <property>
   <property>

+ 31 - 14
hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java

@@ -64,15 +64,15 @@ import org.apache.hadoop.yarn.webapp.util.WebAppUtils;
 import org.apache.http.Header;
 import org.apache.http.Header;
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpResponse;
 import org.apache.http.NameValuePair;
 import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.methods.HttpRequestBase;
 import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.client.params.ClientPNames;
-import org.apache.http.client.params.CookiePolicy;
 import org.apache.http.client.utils.URLEncodedUtils;
 import org.apache.http.client.utils.URLEncodedUtils;
-import org.apache.http.conn.params.ConnRoutePNames;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
@@ -123,6 +123,9 @@ public class WebAppProxyServlet extends HttpServlet {
     }
     }
   }
   }
 
 
+  protected void setConf(YarnConfiguration conf){
+    this.conf = conf;
+  }
   /**
   /**
    * Default constructor
    * Default constructor
    */
    */
@@ -189,15 +192,18 @@ public class WebAppProxyServlet extends HttpServlet {
    * @param method the http method
    * @param method the http method
    * @throws IOException on any error.
    * @throws IOException on any error.
    */
    */
-  private static void proxyLink(final HttpServletRequest req,
+  private void proxyLink(final HttpServletRequest req,
       final HttpServletResponse resp, final URI link, final Cookie c,
       final HttpServletResponse resp, final URI link, final Cookie c,
       final String proxyHost, final HTTP method) throws IOException {
       final String proxyHost, final HTTP method) throws IOException {
-    DefaultHttpClient client = new DefaultHttpClient();
-    client
-        .getParams()
-        .setParameter(ClientPNames.COOKIE_POLICY,
-            CookiePolicy.BROWSER_COMPATIBILITY)
-        .setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
+    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
+
+    boolean connectionTimeoutEnabled =
+        conf.getBoolean(YarnConfiguration.RM_PROXY_TIMEOUT_ENABLED,
+            YarnConfiguration.DEFALUT_RM_PROXY_TIMEOUT_ENABLED);
+    int connectionTimeout =
+        conf.getInt(YarnConfiguration.RM_PROXY_CONNECTION_TIMEOUT,
+            YarnConfiguration.DEFAULT_RM_PROXY_CONNECTION_TIMEOUT);
+
     // Make sure we send the request from the proxy address in the config
     // Make sure we send the request from the proxy address in the config
     // since that is what the AM filter checks against. IP aliasing or
     // since that is what the AM filter checks against. IP aliasing or
     // similar could cause issues otherwise.
     // similar could cause issues otherwise.
@@ -205,8 +211,19 @@ public class WebAppProxyServlet extends HttpServlet {
     if (LOG.isDebugEnabled()) {
     if (LOG.isDebugEnabled()) {
       LOG.debug("local InetAddress for proxy host: {}", localAddress);
       LOG.debug("local InetAddress for proxy host: {}", localAddress);
     }
     }
-    client.getParams()
-        .setParameter(ConnRoutePNames.LOCAL_ADDRESS, localAddress);
+    httpClientBuilder.setDefaultRequestConfig(
+        connectionTimeoutEnabled ?
+            RequestConfig.custom()
+                .setCircularRedirectsAllowed(true)
+                .setLocalAddress(localAddress)
+                .setConnectionRequestTimeout(connectionTimeout)
+                .setSocketTimeout(connectionTimeout)
+                .setConnectTimeout(connectionTimeout)
+                .build() :
+            RequestConfig.custom()
+                .setCircularRedirectsAllowed(true)
+                .setLocalAddress(localAddress)
+                .build());
 
 
     HttpRequestBase base = null;
     HttpRequestBase base = null;
     if (method.equals(HTTP.GET)) {
     if (method.equals(HTTP.GET)) {
@@ -248,6 +265,7 @@ public class WebAppProxyServlet extends HttpServlet {
           PROXY_USER_COOKIE_NAME + "=" + URLEncoder.encode(user, "ASCII"));
           PROXY_USER_COOKIE_NAME + "=" + URLEncoder.encode(user, "ASCII"));
     }
     }
     OutputStream out = resp.getOutputStream();
     OutputStream out = resp.getOutputStream();
+    HttpClient client = httpClientBuilder.build();
     try {
     try {
       HttpResponse httpResp = client.execute(base);
       HttpResponse httpResp = client.execute(base);
       resp.setStatus(httpResp.getStatusLine().getStatusCode());
       resp.setStatus(httpResp.getStatusLine().getStatusCode());
@@ -571,7 +589,6 @@ public class WebAppProxyServlet extends HttpServlet {
    * again... If this method returns true, there was a redirect, and
    * again... If this method returns true, there was a redirect, and
    * it was handled by redirecting the current request to an error page.
    * it was handled by redirecting the current request to an error page.
    *
    *
-   * @param path the part of the request path after the app id
    * @param id the app id
    * @param id the app id
    * @param req the request object
    * @param req the request object
    * @param resp the response object
    * @param resp the response object

+ 80 - 4
hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java

@@ -22,6 +22,8 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 
 import java.io.ByteArrayOutputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.IOException;
@@ -32,10 +34,14 @@ import java.net.HttpCookie;
 import java.net.HttpURLConnection;
 import java.net.HttpURLConnection;
 import java.net.URI;
 import java.net.URI;
 import java.net.URL;
 import java.net.URL;
+import java.net.SocketTimeoutException;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
@@ -90,6 +96,7 @@ public class TestWebAppProxyServlet {
     context.setContextPath("/foo");
     context.setContextPath("/foo");
     server.setHandler(context);
     server.setHandler(context);
     context.addServlet(new ServletHolder(TestServlet.class), "/bar");
     context.addServlet(new ServletHolder(TestServlet.class), "/bar");
+    context.addServlet(new ServletHolder(TimeOutTestServlet.class), "/timeout");
     server.getConnectors()[0].setHost("localhost");
     server.getConnectors()[0].setHost("localhost");
     server.start();
     server.start();
     originalPort = server.getConnectors()[0].getLocalPort();
     originalPort = server.getConnectors()[0].getLocalPort();
@@ -137,6 +144,29 @@ public class TestWebAppProxyServlet {
     }
     }
   }
   }
 
 
+  @SuppressWarnings("serial")
+  public static class TimeOutTestServlet extends HttpServlet {
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+        throws ServletException, IOException {
+      try {
+        Thread.sleep(10 * 1000);
+      } catch (InterruptedException e) {
+        LOG.warn("doGet() interrupted", e);
+        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        return;
+      }
+      resp.setStatus(HttpServletResponse.SC_OK);
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+        throws ServletException, IOException {
+      resp.setStatus(HttpServletResponse.SC_OK);
+    }
+  }
+
   @Test(timeout=5000)
   @Test(timeout=5000)
   public void testWebAppProxyServlet() throws Exception {
   public void testWebAppProxyServlet() throws Exception {
     configuration.set(YarnConfiguration.PROXY_ADDRESS, "localhost:9090");
     configuration.set(YarnConfiguration.PROXY_ADDRESS, "localhost:9090");
@@ -248,6 +278,45 @@ public class TestWebAppProxyServlet {
     }
     }
   }
   }
 
 
+  @Test(expected = SocketTimeoutException.class)
+  public void testWebAppProxyConnectionTimeout()
+      throws IOException, ServletException{
+    HttpServletRequest request = mock(HttpServletRequest.class);
+    when(request.getMethod()).thenReturn("GET");
+    when(request.getRemoteUser()).thenReturn("dr.who");
+    when(request.getPathInfo()).thenReturn("/application_00_0");
+    when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
+
+    HttpServletResponse response = mock(HttpServletResponse.class);
+    when(response.getOutputStream()).thenReturn(null);
+
+    WebAppProxyServlet servlet = new WebAppProxyServlet();
+    YarnConfiguration conf = new YarnConfiguration();
+    conf.setBoolean(YarnConfiguration.RM_PROXY_TIMEOUT_ENABLED,
+        true);
+    conf.setInt(YarnConfiguration.RM_PROXY_CONNECTION_TIMEOUT,
+        1000);
+
+    servlet.setConf(conf);
+
+    ServletConfig config = mock(ServletConfig.class);
+    ServletContext context = mock(ServletContext.class);
+    when(config.getServletContext()).thenReturn(context);
+
+    AppReportFetcherForTest appReportFetcher =
+        new AppReportFetcherForTest(new YarnConfiguration());
+
+    when(config.getServletContext()
+        .getAttribute(WebAppProxy.FETCHER_ATTRIBUTE))
+        .thenReturn(appReportFetcher);
+
+    appReportFetcher.answer = 7;
+
+    servlet.init(config);
+    servlet.doGet(request, response);
+
+  }
+
   @Test(timeout=5000)
   @Test(timeout=5000)
   public void testAppReportForEmptyTrackingUrl() throws Exception {
   public void testAppReportForEmptyTrackingUrl() throws Exception {
     configuration.set(YarnConfiguration.PROXY_ADDRESS, "localhost:9090");
     configuration.set(YarnConfiguration.PROXY_ADDRESS, "localhost:9090");
@@ -330,12 +399,13 @@ public class TestWebAppProxyServlet {
       assertEquals(proxyConn.getRequestProperties().size(), 4);
       assertEquals(proxyConn.getRequestProperties().size(), 4);
       proxyConn.connect();
       proxyConn.connect();
       assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode());
       assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode());
-      // Verify if number of headers received by end server is 8.
-      // Eight headers include Accept, Host, Connection, User-Agent, Cookie,
-      // Origin, Access-Control-Request-Method and
+      // Verify if number of headers received by end server is 9.
+      // This should match WebAppProxyServlet#PASS_THROUGH_HEADERS.
+      // Nine headers include Accept, Host, Connection, User-Agent, Cookie,
+      // Origin, Access-Control-Request-Method, Accept-Encoding, and
       // Access-Control-Request-Headers. Pls note that Unknown-Header is dropped
       // Access-Control-Request-Headers. Pls note that Unknown-Header is dropped
       // by proxy as it is not in the list of allowed headers.
       // by proxy as it is not in the list of allowed headers.
-      assertEquals(numberOfHeaders, 8);
+      assertEquals(numberOfHeaders, 9);
       assertFalse(hasUnknownHeader);
       assertFalse(hasUnknownHeader);
     } finally {
     } finally {
       proxy.close();
       proxy.close();
@@ -553,6 +623,12 @@ public class TestWebAppProxyServlet {
         return result;
         return result;
       } else if (answer == 6) {
       } else if (answer == 6) {
         return getDefaultApplicationReport(appId, false);
         return getDefaultApplicationReport(appId, false);
+      } else if (answer == 7) {
+        // test connection timeout
+        FetchedAppReport result = getDefaultApplicationReport(appId);
+        result.getApplicationReport().setOriginalTrackingUrl("localhost:"
+            + originalPort + "/foo/timeout?a=b#main");
+        return result;
       }
       }
       return null;
       return null;
     }
     }