Skip to content

Commit cb2e324

Browse files
committed
Exercise multipart copy randomly in testWriteMaybeCopyRead
1 parent 892e330 commit cb2e324

File tree

3 files changed

+71
-12
lines changed

3 files changed

+71
-12
lines changed

modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,12 @@ long getLargeBlobThresholdInBytes() {
622622
return ByteSizeUnit.MB.toBytes(1L);
623623
}
624624

625+
@Override
626+
long getLargeCopyThresholdInBytes() {
627+
// on my laptop 10K exercises this better but larger values should be fine for nightlies
628+
return ByteSizeUnit.MB.toBytes(1L);
629+
}
630+
625631
@Override
626632
void ensureMultiPartUploadSize(long blobSize) {}
627633
};

modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ long getLargeBlobThresholdInBytes() {
304304
return blobStore.bufferSizeInBytes();
305305
}
306306

307+
// package private for testing
308+
long getLargeCopyThresholdInBytes() {
309+
return MAX_FILE_SIZE.getBytes();
310+
}
311+
307312
@Override
308313
public void writeBlobAtomic(
309314
OperationPurpose purpose,
@@ -353,7 +358,7 @@ public void copyBlob(
353358
final var s3SourceBlobContainer = (S3BlobContainer) sourceBlobContainer;
354359

355360
try {
356-
if (blobSize > MAX_FILE_SIZE.getBytes()) {
361+
if (blobSize > getLargeCopyThresholdInBytes()) {
357362
executeMultipartCopy(purpose, s3SourceBlobContainer, sourceBlobName, blobName, blobSize);
358363
} else {
359364
// metadata is inherited from source, but not canned ACL or storage class

test/fixtures/s3-fixture/src/main/java/fixture/s3/S3HttpHandler.java

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,33 @@ public void handle(final HttpExchange exchange) throws IOException {
155155
if (upload == null) {
156156
exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1);
157157
} else {
158-
final Tuple<String, BytesReference> blob = parseRequestBody(exchange);
159-
upload.addPart(blob.v1(), blob.v2());
160-
exchange.getResponseHeaders().add("ETag", blob.v1());
161-
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), -1);
158+
// CopyPart is UploadPart with an x-amz-copy-source header
159+
final var sourceBlobName = exchange.getRequestHeaders().get("X-amz-copy-source");
160+
if (sourceBlobName != null) {
161+
var sourceBlob = blobs.get(sourceBlobName.getFirst());
162+
if (sourceBlob == null) {
163+
exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1);
164+
} else {
165+
var range = parsePartRange(exchange);
166+
// we'll assume for tests that the source object on the heap is under 2G
167+
var part = sourceBlob.slice(range.v1().intValue(), range.v2().intValue());
168+
var etag = UUIDs.randomBase64UUID();
169+
upload.addPart(etag, part);
170+
byte[] response = ("""
171+
<?xml version="1.0" encoding="UTF-8"?>
172+
<CopyPartResult>
173+
<ETag>%s</ETag>
174+
</CopyPartResult>""".formatted(etag)).getBytes(StandardCharsets.UTF_8);
175+
exchange.getResponseHeaders().add("Content-Type", "application/xml");
176+
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
177+
exchange.getResponseBody().write(response);
178+
}
179+
} else {
180+
final Tuple<String, BytesReference> blob = parseRequestBody(exchange);
181+
upload.addPart(blob.v1(), blob.v2());
182+
exchange.getResponseHeaders().add("ETag", blob.v1());
183+
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), -1);
184+
}
162185
}
163186

164187
} else if (request.isCompleteMultipartUploadRequest()) {
@@ -205,14 +228,18 @@ public void handle(final HttpExchange exchange) throws IOException {
205228
final var sourceBlobName = exchange.getRequestHeaders().get("X-amz-copy-source");
206229
if (sourceBlobName != null) {
207230
var sourceBlob = blobs.get(sourceBlobName.getFirst());
208-
blobs.put(request.path(), sourceBlob);
231+
if (sourceBlob == null) {
232+
exchange.sendResponseHeaders(RestStatus.NOT_FOUND.getStatus(), -1);
233+
} else {
234+
blobs.put(request.path(), sourceBlob);
209235

210-
byte[] response = ("""
211-
<?xml version="1.0" encoding="UTF-8"?>
212-
<CopyObjectResult></CopyObjectResult>""").getBytes(StandardCharsets.UTF_8);
213-
exchange.getResponseHeaders().add("Content-Type", "application/xml");
214-
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
215-
exchange.getResponseBody().write(response);
236+
byte[] response = ("""
237+
<?xml version="1.0" encoding="UTF-8"?>
238+
<CopyObjectResult></CopyObjectResult>""").getBytes(StandardCharsets.UTF_8);
239+
exchange.getResponseHeaders().add("Content-Type", "application/xml");
240+
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
241+
exchange.getResponseBody().write(response);
242+
}
216243
} else {
217244
final Tuple<String, BytesReference> blob = parseRequestBody(exchange);
218245
blobs.put(request.path(), blob.v2());
@@ -481,6 +508,27 @@ static List<String> extractPartEtags(BytesReference completeMultipartUploadBody)
481508
}
482509
}
483510

511+
private static final Pattern rangePattern = Pattern.compile("^bytes=([0-9]+)-([0-9]+)$");
512+
513+
private static Tuple<Long, Long> parsePartRange(final HttpExchange exchange) {
514+
final var sourceRangeHeaders = exchange.getRequestHeaders().get("X-amz-copy-source-range");
515+
if (sourceRangeHeaders == null) {
516+
throw new IllegalStateException("missing x-amz-copy-source-range header");
517+
}
518+
if (sourceRangeHeaders.size() != 1) {
519+
throw new IllegalStateException("expected 1 x-amz-copy-source-range header, found " + sourceRangeHeaders.size());
520+
}
521+
final var sourceRangeHeader = sourceRangeHeaders.getFirst();
522+
final Matcher matcher = rangePattern.matcher(sourceRangeHeader);
523+
if (matcher.find() == false) {
524+
throw new IllegalStateException("invalid x-amz-copy-source-range header [" + sourceRangeHeader + "]");
525+
}
526+
final var start = Long.parseLong(matcher.group(1));
527+
final var end = Long.parseLong(matcher.group(2));
528+
529+
return new Tuple<>(start, end - start + 1);
530+
}
531+
484532
MultipartUpload getUpload(String uploadId) {
485533
return uploads.get(uploadId);
486534
}

0 commit comments

Comments
 (0)