Skip to content

Commit 1c170b3

Browse files
committed
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
1 parent 607dba9 commit 1c170b3

File tree

7 files changed

+105
-12
lines changed

7 files changed

+105
-12
lines changed

spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnection.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.io.Closeable;
2020
import java.io.IOException;
21+
import java.net.ConnectException;
2122
import java.net.MalformedURLException;
2223
import java.net.URI;
2324
import java.net.URISyntaxException;
@@ -46,6 +47,7 @@
4647
*
4748
* @author Phillip Webb
4849
* @author Rob Winch
50+
* @author Andy Wilkinson
4951
* @since 1.3.0
5052
* @see TunnelClient
5153
* @see org.springframework.boot.devtools.tunnel.server.HttpTunnelServer
@@ -157,7 +159,13 @@ public void run() {
157159
sendAndReceive(payload);
158160
}
159161
catch (IOException ex) {
160-
logger.trace("Unexpected connection error", ex);
162+
if (ex instanceof ConnectException) {
163+
logger.warn("Failed to connect to remote application at "
164+
+ HttpTunnelConnection.this.uri);
165+
}
166+
else {
167+
logger.trace("Unexpected connection error", ex);
168+
}
161169
closeQuietly();
162170
}
163171
}
@@ -188,6 +196,12 @@ private void handleResponse(ClientHttpResponse response) throws IOException {
188196
close();
189197
return;
190198
}
199+
if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE) {
200+
logger.warn("Remote application responded with service unavailable. Did "
201+
+ "you forget to start it with remote debugging enabled?");
202+
close();
203+
return;
204+
}
191205
if (response.getStatusCode() == HttpStatus.OK) {
192206
HttpTunnelPayload payload = HttpTunnelPayload.get(response);
193207
if (payload != null) {

spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import java.net.InetSocketAddress;
2222
import java.net.ServerSocket;
2323
import java.nio.ByteBuffer;
24+
import java.nio.channels.AsynchronousCloseException;
2425
import java.nio.channels.ServerSocketChannel;
2526
import java.nio.channels.SocketChannel;
2627
import java.nio.channels.WritableByteChannel;
@@ -36,6 +37,7 @@
3637
* specified port for local clients to connect to.
3738
*
3839
* @author Phillip Webb
40+
* @author Andy Wilkinson
3941
* @since 1.3.0
4042
*/
4143
public class TunnelClient implements SmartInitializingSingleton {
@@ -143,6 +145,9 @@ public void run() {
143145
try {
144146
handleConnection(socket);
145147
}
148+
catch (AsynchronousCloseException ex) {
149+
// Connection has been closed. Keep the server running
150+
}
146151
finally {
147152
socket.close();
148153
}

spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,6 +88,10 @@
8888
* <td>410 (Gone)</td>
8989
* <td>The target server has disconnected.</td>
9090
* </tr>
91+
* <tr>
92+
* <td>503 (Service Unavailable)</td>
93+
* <td>The target server is unavailable</td>
94+
* </tr>
9195
* </table>
9296
* <p>
9397
* Requests and responses that contain payloads include a {@code x-seq} header that
@@ -96,6 +100,7 @@
96100
* {@code 1}.
97101
*
98102
* @author Phillip Webb
103+
* @author Andy Wilkinson
99104
* @since 1.3.0
100105
* @see org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection
101106
*/
@@ -153,6 +158,9 @@ protected void handle(HttpConnection httpConnection) throws IOException {
153158
catch (ConnectException ex) {
154159
httpConnection.respond(HttpStatus.GONE);
155160
}
161+
catch (RemoteDebugNotRunningException ex) {
162+
httpConnection.respond(HttpStatus.SERVICE_UNAVAILABLE);
163+
}
156164
}
157165

158166
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.devtools.tunnel.server;
18+
19+
/**
20+
* Exception thrown to indicate that remote debug is not running.
21+
*
22+
* @author Andy Wilkinson
23+
*/
24+
class RemoteDebugNotRunningException extends RuntimeException {
25+
26+
}

spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/RemoteDebugPortProvider.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,12 +20,12 @@
2020
import org.apache.commons.logging.LogFactory;
2121

2222
import org.springframework.boot.lang.UsesUnsafeJava;
23-
import org.springframework.util.Assert;
2423

2524
/**
2625
* {@link PortProvider} that provides the port being used by the Java remote debugging.
2726
*
2827
* @author Phillip Webb
28+
* @author Andy Wilkinson
2929
*/
3030
public class RemoteDebugPortProvider implements PortProvider {
3131

@@ -35,7 +35,9 @@ public class RemoteDebugPortProvider implements PortProvider {
3535

3636
@Override
3737
public int getPort() {
38-
Assert.state(isRemoteDebugRunning(), "Remote debug is not running");
38+
if (!isRemoteDebugRunning()) {
39+
throw new RemoteDebugNotRunningException();
40+
}
3941
return getRemoteDebugPort();
4042
}
4143

spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/MockClientHttpRequestFactory.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,12 +37,13 @@
3737
* Mock {@link ClientHttpRequestFactory}.
3838
*
3939
* @author Phillip Webb
40+
* @author Andy Wilkinson
4041
*/
4142
public class MockClientHttpRequestFactory implements ClientHttpRequestFactory {
4243

4344
private AtomicLong seq = new AtomicLong();
4445

45-
private Deque<Response> responses = new ArrayDeque<Response>();
46+
private Deque<Object> responses = new ArrayDeque<Object>();
4647

4748
private List<MockClientHttpRequest> executedRequests = new ArrayList<MockClientHttpRequest>();
4849

@@ -58,6 +59,12 @@ public void willRespond(HttpStatus... response) {
5859
}
5960
}
6061

62+
public void willRespond(IOException... response) {
63+
for (IOException exception : response) {
64+
this.responses.addLast(exception);
65+
}
66+
}
67+
6168
public void willRespond(String... response) {
6269
for (String payload : response) {
6370
this.responses.add(new Response(0, payload.getBytes(), HttpStatus.OK));
@@ -81,11 +88,15 @@ private class MockRequest extends MockClientHttpRequest {
8188
@Override
8289
protected ClientHttpResponse executeInternal() throws IOException {
8390
MockClientHttpRequestFactory.this.executedRequests.add(this);
84-
Response response = MockClientHttpRequestFactory.this.responses.pollFirst();
91+
Object response = MockClientHttpRequestFactory.this.responses.pollFirst();
92+
if (response instanceof IOException) {
93+
throw (IOException) response;
94+
}
8595
if (response == null) {
8696
response = new Response(0, null, HttpStatus.GONE);
8797
}
88-
return response.asHttpResponse(MockClientHttpRequestFactory.this.seq);
98+
return ((Response) response)
99+
.asHttpResponse(MockClientHttpRequestFactory.this.seq);
89100
}
90101

91102
}

spring-boot-devtools/src/test/java/org/springframework/boot/devtools/tunnel/client/HttpTunnelConnectionTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.io.ByteArrayOutputStream;
2020
import java.io.Closeable;
2121
import java.io.IOException;
22+
import java.net.ConnectException;
2223
import java.nio.ByteBuffer;
2324
import java.nio.channels.Channels;
2425
import java.nio.channels.WritableByteChannel;
@@ -33,11 +34,14 @@
3334

3435
import org.springframework.boot.devtools.test.MockClientHttpRequestFactory;
3536
import org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection.TunnelChannel;
37+
import org.springframework.boot.test.OutputCapture;
3638
import org.springframework.http.HttpStatus;
3739
import org.springframework.util.SocketUtils;
3840

41+
import static org.hamcrest.Matchers.containsString;
3942
import static org.hamcrest.Matchers.equalTo;
4043
import static org.hamcrest.Matchers.greaterThan;
44+
import static org.hamcrest.Matchers.is;
4145
import static org.junit.Assert.assertThat;
4246
import static org.mockito.Mockito.never;
4347
import static org.mockito.Mockito.times;
@@ -48,12 +52,16 @@
4852
*
4953
* @author Phillip Webb
5054
* @author Rob Winch
55+
* @author Andy Wilkinson
5156
*/
5257
public class HttpTunnelConnectionTests {
5358

5459
@Rule
5560
public ExpectedException thrown = ExpectedException.none();
5661

62+
@Rule
63+
public OutputCapture outputCapture = new OutputCapture();
64+
5765
private int port = SocketUtils.findAvailableTcpPort();
5866

5967
private String url;
@@ -144,6 +152,25 @@ public void trafficWithLongPollTimeouts() throws Exception {
144152
assertThat(this.requestFactory.getExecutedRequests().size(), greaterThan(10));
145153
}
146154

155+
@Test
156+
public void serviceUnavailableResponseLogsWarningAndClosesTunnel() throws Exception {
157+
this.requestFactory.willRespond(HttpStatus.SERVICE_UNAVAILABLE);
158+
TunnelChannel tunnel = openTunnel(true);
159+
assertThat(tunnel.isOpen(), is(false));
160+
this.outputCapture.expect(containsString(
161+
"Did you forget to start it with remote debugging enabled?"));
162+
}
163+
164+
@Test
165+
public void connectFailureLogsWarning() throws Exception {
166+
this.requestFactory.willRespond(new ConnectException());
167+
TunnelChannel tunnel = openTunnel(true);
168+
assertThat(tunnel.isOpen(), is(false));
169+
this.outputCapture.expect(containsString(
170+
"Failed to connect to remote application at http://localhost:"
171+
+ this.port));
172+
}
173+
147174
private void write(TunnelChannel channel, String string) throws IOException {
148175
channel.write(ByteBuffer.wrap(string.getBytes()));
149176
}

0 commit comments

Comments
 (0)