Improve handling of connection failures in remote debug tunnel

Previously, if an application had been started without remote
debugging enabled, an attempt to connect to it via
RemoteSpringApplication and the HTTP tunnel would result in the
application being hammered by connection attempts for 30 seconds.

This commit updates the tunnel server to respond with Service
Unavailable (503) when a connection attempt is made and the JVM
does not have remote debugging enabled. When the client receives a
503 response, it now logs a warning message describing the possible
problem before closing the connection.

The client has also been updated to provide improved diagnostics when
a connection to the tunnel server cannot be established, for example
because the remote URL is incorrect, or the remote application isn't
running.

Lastly, the client has been updated so that it continues to accept
connections when a connection to the server is closed. This allows
the user to correct a problem with the remote application, such as
restarting it with remote debugging enabled, without having to also
restart the process that's running RemoteSpringApplication.

Closes gh-5021
This commit is contained in:
Andy Wilkinson
2016-02-12 11:52:41 +00:00
parent 607dba97f8
commit 1c170b35ea
7 changed files with 105 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,12 +37,13 @@ import org.springframework.mock.http.client.MockClientHttpResponse;
* Mock {@link ClientHttpRequestFactory}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class MockClientHttpRequestFactory implements ClientHttpRequestFactory {
private AtomicLong seq = new AtomicLong();
private Deque<Response> responses = new ArrayDeque<Response>();
private Deque<Object> responses = new ArrayDeque<Object>();
private List<MockClientHttpRequest> executedRequests = new ArrayList<MockClientHttpRequest>();
@@ -58,6 +59,12 @@ public class MockClientHttpRequestFactory implements ClientHttpRequestFactory {
}
}
public void willRespond(IOException... response) {
for (IOException exception : response) {
this.responses.addLast(exception);
}
}
public void willRespond(String... response) {
for (String payload : response) {
this.responses.add(new Response(0, payload.getBytes(), HttpStatus.OK));
@@ -81,11 +88,15 @@ public class MockClientHttpRequestFactory implements ClientHttpRequestFactory {
@Override
protected ClientHttpResponse executeInternal() throws IOException {
MockClientHttpRequestFactory.this.executedRequests.add(this);
Response response = MockClientHttpRequestFactory.this.responses.pollFirst();
Object response = MockClientHttpRequestFactory.this.responses.pollFirst();
if (response instanceof IOException) {
throw (IOException) response;
}
if (response == null) {
response = new Response(0, null, HttpStatus.GONE);
}
return response.asHttpResponse(MockClientHttpRequestFactory.this.seq);
return ((Response) response)
.asHttpResponse(MockClientHttpRequestFactory.this.seq);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.boot.devtools.tunnel.client;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.net.ConnectException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
@@ -33,11 +34,14 @@ import org.mockito.MockitoAnnotations;
import org.springframework.boot.devtools.test.MockClientHttpRequestFactory;
import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection.TunnelChannel;
import org.springframework.boot.test.OutputCapture;
import org.springframework.http.HttpStatus;
import org.springframework.util.SocketUtils;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -48,12 +52,16 @@ import static org.mockito.Mockito.verify;
*
* @author Phillip Webb
* @author Rob Winch
* @author Andy Wilkinson
*/
public class HttpTunnelConnectionTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public OutputCapture outputCapture = new OutputCapture();
private int port = SocketUtils.findAvailableTcpPort();
private String url;
@@ -144,6 +152,25 @@ public class HttpTunnelConnectionTests {
assertThat(this.requestFactory.getExecutedRequests().size(), greaterThan(10));
}
@Test
public void serviceUnavailableResponseLogsWarningAndClosesTunnel() throws Exception {
this.requestFactory.willRespond(HttpStatus.SERVICE_UNAVAILABLE);
TunnelChannel tunnel = openTunnel(true);
assertThat(tunnel.isOpen(), is(false));
this.outputCapture.expect(containsString(
"Did you forget to start it with remote debugging enabled?"));
}
@Test
public void connectFailureLogsWarning() throws Exception {
this.requestFactory.willRespond(new ConnectException());
TunnelChannel tunnel = openTunnel(true);
assertThat(tunnel.isOpen(), is(false));
this.outputCapture.expect(containsString(
"Failed to connect to remote application at http://localhost:"
+ this.port));
}
private void write(TunnelChannel channel, String string) throws IOException {
channel.write(ByteBuffer.wrap(string.getBytes()));
}