Skip to content

Commit d2bb50e

Browse files
[JENKINS-76354] - resolve indefinite hang in s3Upload caused by untimeout'd future join (#364)
Signed-off-by: prathamalwayscomeslast <way4hell6@gmail.com>
1 parent a43007f commit d2bb50e

File tree

3 files changed

+45
-10
lines changed

3 files changed

+45
-10
lines changed

src/main/java/hudson/plugins/s3/S3Profile.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.util.Map;
3030
import java.util.concurrent.Callable;
3131
import java.util.concurrent.TimeUnit;
32+
import java.util.logging.Level;
33+
import java.util.logging.Logger;
3234

3335
public class S3Profile {
3436
private final String name;
@@ -275,15 +277,21 @@ public FingerprintRecord call() throws IOException, InterruptedException {
275277

276278
private <T> T repeat(int maxRetries, int waitTime, Destination dest, Callable<T> func) throws IOException, InterruptedException {
277279
int retryCount = 0;
280+
Logger LOGGER = Logger.getLogger(S3Profile.class.getName());
278281

279282
while (true) {
280283
try {
281284
return func.call();
285+
} catch (InterruptedException e) {
286+
Thread.currentThread().interrupt();
287+
throw e;
282288
} catch (Exception e) {
283289
retryCount++;
284290
if(retryCount >= maxRetries){
285291
throw new IOException("Call fails for " + dest + ": " + e + ":: Failed after " + retryCount + " tries.", e);
286292
}
293+
LOGGER.log(Level.WARNING, "Upload attempt {0}/{1} failed for {2}: {3}. Retrying in {4}s",
294+
new Object[]{retryCount, maxRetries, dest, e.getMessage(), waitTime});
287295
Thread.sleep(TimeUnit.SECONDS.toMillis(waitTime));
288296
}
289297
}

src/main/java/hudson/plugins/s3/Uploads.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import software.amazon.awssdk.core.async.AsyncRequestBody;
55
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
66
import software.amazon.awssdk.transfer.s3.S3TransferManager;
7-
import software.amazon.awssdk.transfer.s3.model.FileUpload;
87
import software.amazon.awssdk.transfer.s3.model.Upload;
98
import software.amazon.awssdk.transfer.s3.model.UploadRequest;
109
import software.amazon.awssdk.transfer.s3.progress.TransferListener;
@@ -14,8 +13,14 @@
1413
import java.io.InputStream;
1514
import java.util.HashMap;
1615
import java.util.Map;
16+
import java.util.concurrent.ConcurrentHashMap;
17+
import java.util.concurrent.ExecutionException;
1718
import java.util.concurrent.ExecutorService;
1819
import java.util.concurrent.Executors;
20+
import java.util.concurrent.LinkedBlockingQueue;
21+
import java.util.concurrent.ThreadPoolExecutor;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.TimeoutException;
1924
import java.util.function.Consumer;
2025
import java.util.logging.Logger;
2126

@@ -25,9 +30,22 @@ private Uploads() {}
2530
public static final int MULTIPART_UPLOAD_THRESHOLD = 16*1024*1024; // 16 MB
2631

2732
private static transient volatile Uploads instance;
28-
private final transient HashMap<FilePath, Upload> startedUploads = new HashMap<>();
29-
private final ExecutorService executors = Executors.newScheduledThreadPool(1, new NamedThreadFactory(Executors.defaultThreadFactory(), Uploads.class.getName()));
30-
private final transient HashMap<FilePath, InputStream> openedStreams = new HashMap<>();
33+
34+
private final transient Map<FilePath, Upload> startedUploads = new ConcurrentHashMap<>();
35+
private final ExecutorService executors;
36+
// This creates a cached thread pool with an upper bound (5) on threads to be spawned on demand.
37+
{
38+
ThreadPoolExecutor pool = new ThreadPoolExecutor(
39+
5, 5,
40+
60L, TimeUnit.SECONDS,
41+
new LinkedBlockingQueue<>(),
42+
new NamedThreadFactory(Executors.defaultThreadFactory(), Uploads.class.getName())
43+
);
44+
pool.allowCoreThreadTimeOut(true);
45+
executors = pool;
46+
}
47+
48+
private final transient Map<FilePath, InputStream> openedStreams = new ConcurrentHashMap<>();
3149

3250
public Upload startUploading(S3TransferManager manager, FilePath file, InputStream inputStream, String bucketName, String objectName, Metadata metadata, TransferListener listener) {
3351
UploadRequest.Builder request = UploadRequest.builder();
@@ -43,16 +61,24 @@ public Upload startUploading(S3TransferManager manager, FilePath file, InputStre
4361
return upload;
4462
}
4563

46-
public void finishUploading(FilePath filePath) throws InterruptedException {
64+
public void finishUploading(FilePath filePath) throws InterruptedException, IOException {
4765
final Upload upload = startedUploads.remove(filePath);
4866
if (upload == null) {
4967
LOGGER.info("File: " + filePath.getName() + " already was uploaded");
5068
return;
5169
}
5270
try {
53-
upload.completionFuture().join();
54-
}
55-
finally {
71+
upload.completionFuture().get(1, TimeUnit.HOURS);
72+
} catch (InterruptedException e) {
73+
Thread.currentThread().interrupt();
74+
upload.completionFuture().cancel(true); // cancel the upload
75+
throw e;
76+
} catch (ExecutionException e) {
77+
throw new IOException("Upload failed for: " + filePath.getName(), e.getCause());
78+
} catch (TimeoutException e) {
79+
upload.completionFuture().cancel(true);
80+
throw new IOException("Upload timed out for: " + filePath.getName(), e);
81+
} finally {
5682
closeStream(filePath);
5783
}
5884
}

src/main/java/hudson/plugins/s3/callable/S3WaitUploadCallable.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
import org.jenkinsci.remoting.RoleChecker;
88

99
import java.io.File;
10+
import java.io.IOException;
1011

1112
public final class S3WaitUploadCallable implements MasterSlaveCallable<Void> {
1213
@Override
13-
public Void invoke(File f, VirtualChannel channel) throws InterruptedException {
14+
public Void invoke(File f, VirtualChannel channel) throws InterruptedException, IOException {
1415
invoke(new FilePath(f));
1516
return null;
1617
}
1718

1819
@Override
19-
public Void invoke(FilePath file) throws InterruptedException {
20+
public Void invoke(FilePath file) throws InterruptedException, IOException {
2021
Uploads.getInstance().finishUploading(file);
2122
return null;
2223
}

0 commit comments

Comments
 (0)