18
18
19
19
import java .io .ByteArrayInputStream ;
20
20
import java .io .IOException ;
21
- import java .io .InputStream ;
22
21
import java .io .OutputStream ;
23
22
import java .nio .ByteBuffer ;
24
23
import java .util .ArrayList ;
25
24
import java .util .List ;
25
+ import java .util .concurrent .CompletableFuture ;
26
+ import java .util .concurrent .ConcurrentLinkedQueue ;
27
+ import java .util .concurrent .ExecutionException ;
28
+ import java .util .concurrent .atomic .AtomicInteger ;
26
29
27
30
import com .amazonaws .services .s3 .AmazonS3 ;
28
31
import com .amazonaws .services .s3 .model .AbortMultipartUploadRequest ;
31
34
import com .amazonaws .services .s3 .model .InitiateMultipartUploadResult ;
32
35
import com .amazonaws .services .s3 .model .PartETag ;
33
36
import com .amazonaws .services .s3 .model .UploadPartRequest ;
34
- import com .amazonaws .services .s3 .model .UploadPartResult ;
35
37
import org .slf4j .Logger ;
36
38
import org .slf4j .LoggerFactory ;
37
39
38
40
/**
39
41
* S3 multipart output stream.
40
42
* Enable uploads to S3 with unknown size by feeding input bytes to multiple parts and upload them on close.
41
43
*
44
+ * <p>OutputStream is used to write sequentially, but
45
+ * uploading parts happen asynchronously to reduce full upload latency.
46
+ * Concurrency happens within the output stream implementation and does not require changes on the callers.
47
+ *
42
48
* <p>Requires S3 client and starts a multipart transaction when instantiated. Do not reuse.
43
49
*
44
50
* <p>{@link S3MultiPartOutputStream} is not thread-safe.
@@ -54,8 +60,11 @@ public class S3MultiPartOutputStream extends OutputStream {
54
60
final int partSize ;
55
61
56
62
private final String uploadId ;
57
- private final List < PartETag > partETags = new ArrayList <>( );
63
+ private final AtomicInteger partNumber = new AtomicInteger ( 0 );
58
64
65
+ // holds async part upload operations building a list of partETags required when committing
66
+ private CompletableFuture <ConcurrentLinkedQueue <PartETag >> partUploads =
67
+ CompletableFuture .completedFuture (new ConcurrentLinkedQueue <>());
59
68
private boolean closed ;
60
69
61
70
public S3MultiPartOutputStream (final String bucketName ,
@@ -87,15 +96,23 @@ public void write(final byte[] b, final int off, final int len) throws IOExcepti
87
96
return ;
88
97
}
89
98
try {
90
- final ByteBuffer source = ByteBuffer .wrap (b , off , len );
91
- while (source .hasRemaining ()) {
92
- final int transferred = Math .min (partBuffer .remaining (), source .remaining ());
93
- final int offset = source .arrayOffset () + source .position ();
94
- // TODO: get rid of this array copying
95
- partBuffer .put (source .array (), offset , transferred );
96
- source .position (source .position () + transferred );
99
+ final ByteBuffer currentBatch = ByteBuffer .wrap (b , off , len );
100
+ while (currentBatch .hasRemaining ()) {
101
+ // copy batch to part buffer
102
+ final int toCopy = Math .min (partBuffer .remaining (), currentBatch .remaining ());
103
+ final int positionAfterCopying = currentBatch .position () + toCopy ;
104
+ currentBatch .limit (positionAfterCopying );
105
+ partBuffer .put (currentBatch .slice ());
106
+
107
+ // prepare current batch for next part
108
+ currentBatch .clear (); // reset limit
109
+ currentBatch .position (positionAfterCopying );
110
+
97
111
if (!partBuffer .hasRemaining ()) {
98
- flushBuffer (0 , partSize );
112
+ partBuffer .position (0 );
113
+ partBuffer .limit (partSize );
114
+ uploadPart (partBuffer .slice (), partSize );
115
+ partBuffer .clear ();
99
116
}
100
117
}
101
118
} catch (final RuntimeException e ) {
@@ -105,26 +122,48 @@ public void write(final byte[] b, final int off, final int len) throws IOExcepti
105
122
}
106
123
}
107
124
125
+ /**
126
+ * Completes pending part uploads
127
+ *
128
+ * @throws IOException if uploads fail and abort transaction
129
+ */
108
130
@ Override
109
- public void close () throws IOException {
110
- if (! isClosed ()) {
131
+ public void flush () throws IOException {
132
+ try {
111
133
if (partBuffer .position () > 0 ) {
112
- try {
113
- flushBuffer (partBuffer .arrayOffset (), partBuffer .position ());
114
- } catch (final RuntimeException e ) {
115
- log .error ("Failed to upload last part {}, aborting transaction" , uploadId , e );
116
- abortUpload ();
117
- throw new IOException (e );
118
- }
134
+ // flush missing bytes
135
+ final int actualPartSize = partBuffer .position ();
136
+ partBuffer .position (0 );
137
+ partBuffer .limit (actualPartSize );
138
+ uploadPart (partBuffer .slice (), actualPartSize );
139
+ partBuffer .clear ();
119
140
}
120
- if (!partETags .isEmpty ()) {
141
+
142
+ // wait for requests to be processed
143
+ partUploads .join ();
144
+ } catch (final RuntimeException e ) {
145
+ log .error ("Failed to upload parts {}, aborting transaction" , uploadId , e );
146
+ abortUpload ();
147
+ throw new IOException ("Failed to flush upload part operations" , e );
148
+ }
149
+ }
150
+
151
+ @ Override
152
+ public void close () throws IOException {
153
+ if (!isClosed ()) {
154
+ flush ();
155
+ if (partNumber .get () > 0 ) {
121
156
try {
122
- completeUpload ();
157
+ // wait for all uploads to complete successfully before committing
158
+ final ConcurrentLinkedQueue <PartETag > tagsQueue = partUploads .get (); // TODO: maybe set a timeout?
159
+ final ArrayList <PartETag > partETags = new ArrayList <>(tagsQueue );
160
+
161
+ completeUpload (partETags );
123
162
log .debug ("Completed multipart upload {}" , uploadId );
124
- } catch (final RuntimeException e ) {
163
+ } catch (final RuntimeException | InterruptedException | ExecutionException e ) {
125
164
log .error ("Failed to complete multipart upload {}, aborting transaction" , uploadId , e );
126
165
abortUpload ();
127
- throw new IOException (e );
166
+ throw new IOException ("Failed to complete upload transaction" , e );
128
167
}
129
168
} else {
130
169
abortUpload ();
@@ -136,7 +175,7 @@ public boolean isClosed() {
136
175
return closed ;
137
176
}
138
177
139
- private void completeUpload () {
178
+ private void completeUpload (final List < PartETag > partETags ) {
140
179
final var request = new CompleteMultipartUploadRequest (bucketName , key , uploadId , partETags );
141
180
client .completeMultipartUpload (request );
142
181
closed = true ;
@@ -148,24 +187,24 @@ private void abortUpload() {
148
187
closed = true ;
149
188
}
150
189
151
- private void flushBuffer (final int offset ,
152
- final int actualPartSize ) {
153
- final ByteArrayInputStream in = new ByteArrayInputStream ( partBuffer .array (), offset , actualPartSize );
154
- uploadPart ( in , actualPartSize );
155
- partBuffer . clear ();
156
- }
157
-
158
- private void uploadPart ( final InputStream in , final int actualPartSize ) {
159
- final int partNumber = partETags . size () + 1 ;
160
- final UploadPartRequest uploadPartRequest =
161
- new UploadPartRequest ()
162
- . withBucketName ( bucketName )
163
- . withKey ( key )
164
- . withUploadId ( uploadId )
165
- . withPartSize ( actualPartSize )
166
- . withPartNumber ( partNumber )
167
- . withInputStream ( in );
168
- final UploadPartResult uploadResult = client . uploadPart ( uploadPartRequest ) ;
169
- partETags . add ( uploadResult . getPartETag () );
190
+ private void uploadPart (final ByteBuffer partBuffer , final int actualPartSize ) {
191
+ final byte [] partContent = new byte [ actualPartSize ];
192
+ partBuffer .get ( partContent , 0 , actualPartSize );
193
+
194
+ final var uploadPartRequest = new UploadPartRequest ()
195
+ . withBucketName ( bucketName )
196
+ . withKey ( key )
197
+ . withUploadId ( uploadId )
198
+ . withPartSize ( actualPartSize )
199
+ . withPartNumber ( partNumber . incrementAndGet ())
200
+ . withInputStream ( new ByteArrayInputStream ( partContent ));
201
+
202
+ // Run request async
203
+ partUploads = partUploads . thenCombine (
204
+ CompletableFuture . supplyAsync (() -> client . uploadPart ( uploadPartRequest )),
205
+ ( partETags , result ) -> {
206
+ partETags . add ( result . getPartETag () );
207
+ return partETags ;
208
+ } );
170
209
}
171
210
}
0 commit comments