From b1dd92881d50f0210cb989ee511273e87f7e9341 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Jul 2016 10:33:39 +0100 Subject: [PATCH] Retry class file upload to remote application that fails to connect Closes gh-6339 --- .../client/ClassPathChangeUploader.java | 46 ++++++++++++----- .../client/ClassPathChangeUploaderTests.java | 49 ++++++++++++++----- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java index f746b562cc1..cbd5c674991 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java @@ -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.remote.client; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; +import java.net.ConnectException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -51,6 +52,7 @@ import org.springframework.util.FileCopyUtils; * Listens and pushes any classpath updates to a remote endpoint. * * @author Phillip Webb + * @author Andy Wilkinson * @since 1.3.0 */ public class ClassPathChangeUploader @@ -91,23 +93,45 @@ public class ClassPathChangeUploader public void onApplicationEvent(ClassPathChangedEvent event) { try { ClassLoaderFiles classLoaderFiles = getClassLoaderFiles(event); - ClientHttpRequest request = this.requestFactory.createRequest(this.uri, - HttpMethod.POST); byte[] bytes = serialize(classLoaderFiles); - HttpHeaders headers = request.getHeaders(); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - headers.setContentLength(bytes.length); - FileCopyUtils.copy(bytes, request.getBody()); - logUpload(classLoaderFiles); - ClientHttpResponse response = request.execute(); - Assert.state(response.getStatusCode() == HttpStatus.OK, "Unexpected " - + response.getStatusCode() + " response uploading class files"); + performUpload(classLoaderFiles, bytes); } catch (IOException ex) { throw new IllegalStateException(ex); } } + private void performUpload(ClassLoaderFiles classLoaderFiles, byte[] bytes) + throws IOException { + try { + while (true) { + try { + ClientHttpRequest request = this.requestFactory + .createRequest(this.uri, HttpMethod.POST); + HttpHeaders headers = request.getHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentLength(bytes.length); + FileCopyUtils.copy(bytes, request.getBody()); + ClientHttpResponse response = request.execute(); + Assert.state(response.getStatusCode() == HttpStatus.OK, + "Unexpected " + response.getStatusCode() + + " response uploading class files"); + logUpload(classLoaderFiles); + return; + } + catch (ConnectException ex) { + logger.warn("Failed to connect when uploading to " + this.uri + + ". Upload will be retried in 2 seconds"); + Thread.sleep(2000); + } + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + } + private void logUpload(ClassLoaderFiles classLoaderFiles) { int size = classLoaderFiles.size(); logger.info( diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploaderTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploaderTests.java index cb06ea1433d..3943f776a34 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploaderTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploaderTests.java @@ -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. @@ -20,6 +20,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; +import java.net.ConnectException; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; @@ -45,12 +46,14 @@ import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.util.FileCopyUtils; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; /** * Tests for {@link ClassPathChangeUploader}. * * @author Phillip Webb + * @author Andy Wilkinson */ public class ClassPathChangeUploaderTests { @@ -102,19 +105,28 @@ public class ClassPathChangeUploaderTests { @Test public void sendsClassLoaderFiles() throws Exception { File sourceFolder = this.temp.newFolder(); - Set files = new LinkedHashSet(); - File file1 = createFile(sourceFolder, "File1"); - File file2 = createFile(sourceFolder, "File2"); - File file3 = createFile(sourceFolder, "File3"); - files.add(new ChangedFile(sourceFolder, file1, Type.ADD)); - files.add(new ChangedFile(sourceFolder, file2, Type.MODIFY)); - files.add(new ChangedFile(sourceFolder, file3, Type.DELETE)); - Set changeSet = new LinkedHashSet(); - changeSet.add(new ChangedFiles(sourceFolder, files)); - ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false); + ClassPathChangedEvent event = createClassPathChangedEvent(sourceFolder); this.requestFactory.willRespond(HttpStatus.OK); this.uploader.onApplicationEvent(event); + assertThat(this.requestFactory.getExecutedRequests().size(), is(1)); MockClientHttpRequest request = this.requestFactory.getExecutedRequests().get(0); + verifyUploadRequest(sourceFolder, request); + } + + @Test + public void retriesOnConnectException() throws Exception { + File sourceFolder = this.temp.newFolder(); + ClassPathChangedEvent event = createClassPathChangedEvent(sourceFolder); + this.requestFactory.willRespond(new ConnectException()); + this.requestFactory.willRespond(HttpStatus.OK); + this.uploader.onApplicationEvent(event); + assertThat(this.requestFactory.getExecutedRequests().size(), is(2)); + verifyUploadRequest(sourceFolder, + this.requestFactory.getExecutedRequests().get(1)); + } + + private void verifyUploadRequest(File sourceFolder, MockClientHttpRequest request) + throws IOException, ClassNotFoundException { ClassLoaderFiles classLoaderFiles = deserialize(request.getBodyAsBytes()); Collection sourceFolders = classLoaderFiles.getSourceFolders(); assertThat(sourceFolders.size(), equalTo(1)); @@ -133,6 +145,21 @@ public class ClassPathChangeUploaderTests { assertThat(file.getKind(), equalTo(kind)); } + private ClassPathChangedEvent createClassPathChangedEvent(File sourceFolder) + throws IOException { + Set files = new LinkedHashSet(); + File file1 = createFile(sourceFolder, "File1"); + File file2 = createFile(sourceFolder, "File2"); + File file3 = createFile(sourceFolder, "File3"); + files.add(new ChangedFile(sourceFolder, file1, Type.ADD)); + files.add(new ChangedFile(sourceFolder, file2, Type.MODIFY)); + files.add(new ChangedFile(sourceFolder, file3, Type.DELETE)); + Set changeSet = new LinkedHashSet(); + changeSet.add(new ChangedFiles(sourceFolder, files)); + ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false); + return event; + } + private File createFile(File sourceFolder, String name) throws IOException { File file = new File(sourceFolder, name); FileCopyUtils.copy(name.getBytes(), file);