|
@@ -0,0 +1,322 @@
|
|
|
|
+/**
|
|
|
|
+ * 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.hadoop.hdfs.web;
|
|
|
|
+
|
|
|
|
+import static org.junit.Assert.*;
|
|
|
|
+
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
+import java.io.IOException;
|
|
|
|
+import java.io.InputStream;
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
|
+import java.io.OutputStream;
|
|
|
|
+import java.net.InetSocketAddress;
|
|
|
|
+import java.net.ServerSocket;
|
|
|
|
+import java.net.Socket;
|
|
|
|
+import java.net.SocketTimeoutException;
|
|
|
|
+import java.nio.channels.SocketChannel;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.List;
|
|
|
|
+
|
|
|
|
+import org.junit.After;
|
|
|
|
+import org.junit.Before;
|
|
|
|
+import org.junit.Test;
|
|
|
|
+
|
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
|
+import org.apache.hadoop.conf.Configuration;
|
|
|
|
+import org.apache.hadoop.fs.Path;
|
|
|
|
+import org.apache.hadoop.hdfs.server.namenode.NameNode;
|
|
|
|
+import org.apache.hadoop.io.IOUtils;
|
|
|
|
+import org.apache.hadoop.net.NetUtils;
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * This test suite checks that WebHdfsFileSystem sets connection timeouts and
|
|
|
|
+ * read timeouts on its sockets, thus preventing threads from hanging
|
|
|
|
+ * indefinitely on an undefined/infinite timeout. The tests work by starting a
|
|
|
|
+ * bogus server on the namenode HTTP port, which is rigged to not accept new
|
|
|
|
+ * connections or to accept connections but not send responses.
|
|
|
|
+ */
|
|
|
|
+public class TestWebHdfsTimeouts {
|
|
|
|
+
|
|
|
|
+ private static final Log LOG = LogFactory.getLog(TestWebHdfsTimeouts.class);
|
|
|
|
+
|
|
|
|
+ private static final int CLIENTS_TO_CONSUME_BACKLOG = 100;
|
|
|
|
+ private static final int CONNECTION_BACKLOG = 1;
|
|
|
|
+ private static final int INITIAL_SOCKET_TIMEOUT = URLUtils.SOCKET_TIMEOUT;
|
|
|
|
+ private static final int SHORT_SOCKET_TIMEOUT = 5;
|
|
|
|
+ private static final int TEST_TIMEOUT = 10000;
|
|
|
|
+
|
|
|
|
+ private List<SocketChannel> clients;
|
|
|
|
+ private WebHdfsFileSystem fs;
|
|
|
|
+ private InetSocketAddress nnHttpAddress;
|
|
|
|
+ private ServerSocket serverSocket;
|
|
|
|
+ private Thread serverThread;
|
|
|
|
+
|
|
|
|
+ @Before
|
|
|
|
+ public void setUp() throws Exception {
|
|
|
|
+ URLUtils.SOCKET_TIMEOUT = SHORT_SOCKET_TIMEOUT;
|
|
|
|
+ Configuration conf = WebHdfsTestUtil.createConf();
|
|
|
|
+ nnHttpAddress = NameNode.getHttpAddress(conf);
|
|
|
|
+ serverSocket = new ServerSocket(nnHttpAddress.getPort(), CONNECTION_BACKLOG);
|
|
|
|
+ fs = WebHdfsTestUtil.getWebHdfsFileSystem(conf);
|
|
|
|
+ clients = new ArrayList<SocketChannel>();
|
|
|
|
+ serverThread = null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @After
|
|
|
|
+ public void tearDown() throws Exception {
|
|
|
|
+ IOUtils.cleanup(LOG, clients.toArray(new SocketChannel[clients.size()]));
|
|
|
|
+ IOUtils.cleanup(LOG, fs);
|
|
|
|
+ if (serverSocket != null) {
|
|
|
|
+ try {
|
|
|
|
+ serverSocket.close();
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
+ LOG.debug("Exception in closing " + serverSocket, e);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (serverThread != null) {
|
|
|
|
+ serverThread.join();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Expect connect timeout, because the connection backlog is consumed.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testConnectTimeout() throws Exception {
|
|
|
|
+ consumeConnectionBacklog();
|
|
|
|
+ try {
|
|
|
|
+ fs.listFiles(new Path("/"), false);
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("connect timed out", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Expect read timeout, because the bogus server never sends a reply.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testReadTimeout() throws Exception {
|
|
|
|
+ try {
|
|
|
|
+ fs.listFiles(new Path("/"), false);
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("Read timed out", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Expect connect timeout on a URL that requires auth, because the connection
|
|
|
|
+ * backlog is consumed.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testAuthUrlConnectTimeout() throws Exception {
|
|
|
|
+ consumeConnectionBacklog();
|
|
|
|
+ try {
|
|
|
|
+ fs.getDelegationToken("renewer");
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("connect timed out", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Expect read timeout on a URL that requires auth, because the bogus server
|
|
|
|
+ * never sends a reply.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testAuthUrlReadTimeout() throws Exception {
|
|
|
|
+ try {
|
|
|
|
+ fs.getDelegationToken("renewer");
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("Read timed out", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * After a redirect, expect connect timeout accessing the redirect location,
|
|
|
|
+ * because the connection backlog is consumed.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testRedirectConnectTimeout() throws Exception {
|
|
|
|
+ startSingleTemporaryRedirectResponseThread(true);
|
|
|
|
+ try {
|
|
|
|
+ fs.getFileChecksum(new Path("/file"));
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("connect timed out", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * After a redirect, expect read timeout accessing the redirect location,
|
|
|
|
+ * because the bogus server never sends a reply.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testRedirectReadTimeout() throws Exception {
|
|
|
|
+ startSingleTemporaryRedirectResponseThread(false);
|
|
|
|
+ try {
|
|
|
|
+ fs.getFileChecksum(new Path("/file"));
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("Read timed out", e.getMessage());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * On the second step of two-step write, expect connect timeout accessing the
|
|
|
|
+ * redirect location, because the connection backlog is consumed.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testTwoStepWriteConnectTimeout() throws Exception {
|
|
|
|
+ startSingleTemporaryRedirectResponseThread(true);
|
|
|
|
+ OutputStream os = null;
|
|
|
|
+ try {
|
|
|
|
+ os = fs.create(new Path("/file"));
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("connect timed out", e.getMessage());
|
|
|
|
+ } finally {
|
|
|
|
+ IOUtils.cleanup(LOG, os);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * On the second step of two-step write, expect read timeout accessing the
|
|
|
|
+ * redirect location, because the bogus server never sends a reply.
|
|
|
|
+ */
|
|
|
|
+ @Test(timeout=TEST_TIMEOUT)
|
|
|
|
+ public void testTwoStepWriteReadTimeout() throws Exception {
|
|
|
|
+ startSingleTemporaryRedirectResponseThread(false);
|
|
|
|
+ OutputStream os = null;
|
|
|
|
+ try {
|
|
|
|
+ os = fs.create(new Path("/file"));
|
|
|
|
+ os.close(); // must close stream to force reading the HTTP response
|
|
|
|
+ os = null;
|
|
|
|
+ fail("expected timeout");
|
|
|
|
+ } catch (SocketTimeoutException e) {
|
|
|
|
+ assertEquals("Read timed out", e.getMessage());
|
|
|
|
+ } finally {
|
|
|
|
+ IOUtils.cleanup(LOG, os);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Starts a background thread that accepts one and only one client connection
|
|
|
|
+ * on the server socket, sends an HTTP 307 Temporary Redirect response, and
|
|
|
|
+ * then exits. This is useful for testing timeouts on the second step of
|
|
|
|
+ * methods that issue 2 HTTP requests (request 1, redirect, request 2).
|
|
|
|
+ *
|
|
|
|
+ * For handling the first request, this method sets socket timeout to use the
|
|
|
|
+ * initial values defined in URLUtils. Afterwards, it guarantees that the
|
|
|
|
+ * second request will use a very short timeout.
|
|
|
|
+ *
|
|
|
|
+ * Optionally, the thread may consume the connection backlog immediately after
|
|
|
|
+ * receiving its one and only client connection. This is useful for forcing a
|
|
|
|
+ * connection timeout on the second request.
|
|
|
|
+ *
|
|
|
|
+ * On tearDown, open client connections are closed, and the thread is joined.
|
|
|
|
+ *
|
|
|
|
+ * @param consumeConnectionBacklog boolean whether or not to consume connection
|
|
|
|
+ * backlog and thus force a connection timeout on the second request
|
|
|
|
+ */
|
|
|
|
+ private void startSingleTemporaryRedirectResponseThread(
|
|
|
|
+ final boolean consumeConnectionBacklog) {
|
|
|
|
+ URLUtils.SOCKET_TIMEOUT = INITIAL_SOCKET_TIMEOUT;
|
|
|
|
+ serverThread = new Thread() {
|
|
|
|
+ @Override
|
|
|
|
+ public void run() {
|
|
|
|
+ Socket clientSocket = null;
|
|
|
|
+ OutputStream out = null;
|
|
|
|
+ InputStream in = null;
|
|
|
|
+ InputStreamReader isr = null;
|
|
|
|
+ BufferedReader br = null;
|
|
|
|
+ try {
|
|
|
|
+ // Accept one and only one client connection.
|
|
|
|
+ clientSocket = serverSocket.accept();
|
|
|
|
+
|
|
|
|
+ // Immediately setup conditions for subsequent connections.
|
|
|
|
+ URLUtils.SOCKET_TIMEOUT = SHORT_SOCKET_TIMEOUT;
|
|
|
|
+ if (consumeConnectionBacklog) {
|
|
|
|
+ consumeConnectionBacklog();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Consume client's HTTP request by reading until EOF or empty line.
|
|
|
|
+ in = clientSocket.getInputStream();
|
|
|
|
+ isr = new InputStreamReader(in);
|
|
|
|
+ br = new BufferedReader(isr);
|
|
|
|
+ for (;;) {
|
|
|
|
+ String line = br.readLine();
|
|
|
|
+ if (line == null || line.isEmpty()) {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Write response.
|
|
|
|
+ out = clientSocket.getOutputStream();
|
|
|
|
+ out.write(temporaryRedirect().getBytes("UTF-8"));
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
+ // Fail the test on any I/O error in the server thread.
|
|
|
|
+ LOG.error("unexpected IOException in server thread", e);
|
|
|
|
+ fail("unexpected IOException in server thread: " + e);
|
|
|
|
+ } finally {
|
|
|
|
+ // Clean it all up.
|
|
|
|
+ IOUtils.cleanup(LOG, br, isr, in, out);
|
|
|
|
+ IOUtils.closeSocket(clientSocket);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+ serverThread.start();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Consumes the test server's connection backlog by spamming non-blocking
|
|
|
|
+ * SocketChannel client connections. We never do anything with these sockets
|
|
|
|
+ * beyond just initiaing the connections. The method saves a reference to each
|
|
|
|
+ * new SocketChannel so that it can be closed during tearDown. We define a
|
|
|
|
+ * very small connection backlog, but the OS may silently enforce a larger
|
|
|
|
+ * minimum backlog than requested. To work around this, we create far more
|
|
|
|
+ * client connections than our defined backlog.
|
|
|
|
+ *
|
|
|
|
+ * @throws IOException thrown for any I/O error
|
|
|
|
+ */
|
|
|
|
+ private void consumeConnectionBacklog() throws IOException {
|
|
|
|
+ for (int i = 0; i < CLIENTS_TO_CONSUME_BACKLOG; ++i) {
|
|
|
|
+ SocketChannel client = SocketChannel.open();
|
|
|
|
+ client.configureBlocking(false);
|
|
|
|
+ client.connect(nnHttpAddress);
|
|
|
|
+ clients.add(client);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Creates an HTTP 307 response with the redirect location set back to the
|
|
|
|
+ * test server's address. HTTP is supposed to terminate newlines with CRLF, so
|
|
|
|
+ * we hard-code that instead of using the line separator property.
|
|
|
|
+ *
|
|
|
|
+ * @return String HTTP 307 response
|
|
|
|
+ */
|
|
|
|
+ private String temporaryRedirect() {
|
|
|
|
+ return "HTTP/1.1 307 Temporary Redirect\r\n" +
|
|
|
|
+ "Location: http://" + NetUtils.getHostPortString(nnHttpAddress) + "\r\n" +
|
|
|
|
+ "\r\n";
|
|
|
|
+ }
|
|
|
|
+}
|