Skip to content
This repository was archived by the owner on Jul 19, 2024. It is now read-only.

Commit 06fa772

Browse files
authored
Merge pull request #387 from rickle-msft/dev
Dev
2 parents 9a28f04 + 42e015f commit 06fa772

File tree

12 files changed

+190
-92
lines changed

12 files changed

+190
-92
lines changed

CONTRIBUTING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ The Azure Storage development team uses Intellij. However, any preferred IDE or
2020
## Tests
2121

2222
### Configuration
23-
The only step to configure testing is to set the appropriate environment variable. Create environment variables named "ACCOUNT_NAME", "ACCOUNT_KEY", "SECONDARY_ACCOUNT_NAME", and "SECONDARY_ACCOUNT_KEY". The first two will be used for most requests. The second two will only be used for tests requiring two accounts.
23+
The only step to configure testing is to set the appropriate environment variables. Create environment variables named "ACCOUNT_NAME" and "ACCOUNT_KEY", holding your Azure storage account name and key respectively. This will satisfy most tests.
24+
To run any tests requiring two accounts (generally those testing copy-related apis), set environment variables "SECONDARY_ACCOUNT_NAME", and "SECONDARY_ACCOUNT_KEY".
25+
To run any tests related to setting blob tiers on block blobs, set environment variables "BLOB_STORAGE_ACCOUNT_NAME" and "BLOB_STORAGE_ACCOUNT_KEY". Note that a GPV2 account is also sufficient here.
26+
To run any tests related to setting blob tiers on page blobs, set environment variables "PREMIUM_ACCOUNT_NAME" and "PREMIUM_ACCOUNT_KEY".
27+
It is valid to use a single account for multiple scenarios; a GPV2 account would work for both the primary account and the blob storage account, for instance. The only restriction is that the primary and secondary accounts must be distinct.
2428

2529
### Running
2630
To actually run tests, right click on the test class in the Package Explorer or the individual test in the Outline and select Run As->GroovyTest. Alternatively, run mvn test from the command line.

ChangeLog.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ XXXX.XX.XX Version XX.X.X
44
* Added CopyFromURL, which will do a synchronous server-side copy, meaning the service will not return an HTTP response until it has completed the copy.
55
* Added support for IProgressReceiver in TransferManager operations. This parameter was previously ignored but is now supported.
66
* Removed internal dependency on javafx to be compatible with openjdk.
7+
* Fixed a bug that would cause downloading large files with the TransferManager to fail and a bug in BlobURL.download() logic for setting up reliable download.
8+
* Fixed a bug that would cause downloading large files with the TransferManager to fail.
9+
* Fixed a bug in BlobURL.download() logic for setting up reliable download. This had the potential to download the wrong range when a download stream was retried.
710

811
2018.09.11 Version 10.1.0
912
* Interfaces for helper types updated to be more consistent throughout the library. All types, with the exception of the options for pipeline factories, use a fluent pattern.

src/main/java/com/microsoft/azure/storage/blob/BlobURL.java

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -322,26 +322,6 @@ public Single<DownloadResponse> download() {
322322
return this.download(null, null, false, null);
323323
}
324324

325-
/**
326-
* Reads a range of bytes from a blob. The response also includes the blob's properties and metadata. For more
327-
* information, see the <a href="https://docs.microsoft.com/rest/api/storageservices/get-blob">Azure Docs</a>.
328-
* <p>
329-
* Note that the response body has reliable download functionality built in, meaning that a failed download stream
330-
* will be automatically retried. This behavior may be configured with {@link ReliableDownloadOptions}.
331-
*
332-
* @param range
333-
* {@link BlobRange}
334-
*
335-
* @return Emits the successful response.
336-
*
337-
* @apiNote ## Sample Code \n
338-
* [!code-java[Sample_Code](../azure-storage-java/src/test/java/com/microsoft/azure/storage/Samples.java?name=upload_download "Sample code for BlobURL.download")] \n
339-
* For more samples, please see the [Samples file](%https://github.com/Azure/azure-storage-java/blob/master/src/test/java/com/microsoft/azure/storage/Samples.java)
340-
*/
341-
public Single<DownloadResponse> download(BlobRange range) {
342-
return this.download(range, null, false, null);
343-
}
344-
345325
/**
346326
* Reads a range of bytes from a blob. The response also includes the blob's properties and metadata. For more
347327
* information, see the <a href="https://docs.microsoft.com/rest/api/storageservices/get-blob">Azure Docs</a>.
@@ -374,7 +354,7 @@ public Single<DownloadResponse> download(BlobRange range, BlobAccessConditions a
374354
range = range == null ? BlobRange.DEFAULT : range;
375355
accessConditions = accessConditions == null ? BlobAccessConditions.NONE : accessConditions;
376356
HTTPGetterInfo info = new HTTPGetterInfo()
377-
.withCount(range.offset())
357+
.withOffset(range.offset())
378358
.withCount(range.count())
379359
.withETag(accessConditions.modifiedAccessConditions().ifMatch());
380360

src/main/java/com/microsoft/azure/storage/blob/DownloadResponse.java

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,55 +73,60 @@ public Flowable<ByteBuffer> body(ReliableDownloadOptions options) {
7373
return this.rawResponse.body();
7474
}
7575

76-
return this.rawResponse.body()
77-
/*
78-
Update how much data we have received in case we need to retry and propagate to the user the data we
79-
have received.
80-
*/
81-
.doOnNext(buffer -> {
82-
this.info.withOffset(this.info.offset() + buffer.remaining());
83-
if (info.count() != null) {
84-
this.info.withCount(this.info.count() - buffer.remaining());
85-
}
86-
})
87-
.onErrorResumeNext(throwable -> {
88-
// So far we have tried once but retried zero times.
89-
return tryContinueFlowable(throwable, 0, optionsReal);
90-
});
76+
/*
77+
We pass -1 for currentRetryCount because we want tryContinueFlowable to receive a value of 0 for number of
78+
retries as we have not actually retried yet, only made the initial try. Because applyReliableDownload() will
79+
add 1 before calling into tryContinueFlowable, we set the initial value to -1.
80+
*/
81+
return this.applyReliableDownload(this.rawResponse.body(), -1, optionsReal);
9182
}
9283

9384
private Flowable<ByteBuffer> tryContinueFlowable(Throwable t, int retryCount, ReliableDownloadOptions options) {
9485
// If all the errors are exhausted, return this error to the user.
9586
if (retryCount > options.maxRetryRequests() || !(t instanceof IOException)) {
9687
return Flowable.error(t);
9788
} else {
89+
/*
90+
We wrap this in a try catch because we don't know the behavior of the getter. Most errors would probably
91+
come from an unsuccessful request, which would be propagated through the onError methods. However, it is
92+
possible the method call that returns a Single is what throws (like how our apis throw some exceptions at
93+
call time rather than at subscription time.
94+
*/
9895
try {
9996
// Get a new response and try reading from it.
10097
return getter.apply(this.info)
101-
.flatMapPublisher(response -> {
102-
// Do not compound the number of retries; just get the raw body.
103-
ReliableDownloadOptions newOptions = new ReliableDownloadOptions()
104-
.withMaxRetryRequests(0);
105-
106-
return response.body(newOptions)
107-
.doOnNext(buffer -> {
108-
this.info.withOffset(this.info.offset() + buffer.remaining());
109-
if (info.count() != null) {
110-
this.info.withCount(this.info.count() - buffer.remaining());
111-
}
112-
})
113-
.onErrorResumeNext(t2 -> {
114-
// Increment the retry count and try again with the new exception.
115-
return tryContinueFlowable(t2, retryCount + 1, options);
116-
});
117-
});
98+
.flatMapPublisher(response ->
99+
/*
100+
Do not compound the number of retries by passing in another set of downloadOptions; just get
101+
the raw body.
102+
*/
103+
this.applyReliableDownload(this.rawResponse.body(), retryCount, options));
118104
} catch (Exception e) {
119105
// If the getter fails, return the getter failure to the user.
120106
return Flowable.error(e);
121107
}
122108
}
123109
}
124110

111+
private Flowable<ByteBuffer> applyReliableDownload(Flowable<ByteBuffer> data,
112+
int currentRetryCount, ReliableDownloadOptions options) {
113+
return data
114+
.doOnNext(buffer -> {
115+
/*
116+
Update how much data we have received in case we need to retry and propagate to the user the data we
117+
have received.
118+
*/
119+
this.info.withOffset(this.info.offset() + buffer.remaining());
120+
if (this.info.count() != null) {
121+
this.info.withCount(this.info.count() - buffer.remaining());
122+
}
123+
})
124+
.onErrorResumeNext(t2 -> {
125+
// Increment the retry count and try again with the new exception.
126+
return tryContinueFlowable(t2, currentRetryCount + 1, options);
127+
});
128+
}
129+
125130
public int statusCode() {
126131
return this.rawResponse.statusCode();
127132
}

src/main/java/com/microsoft/azure/storage/blob/HTTPGetterInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public Long count() {
5555
*/
5656
public HTTPGetterInfo withCount(Long count) {
5757
if (count != null) {
58-
Utility.assertInBounds("count", count, 0, Integer.MAX_VALUE);
58+
Utility.assertInBounds("count", count, 0, Long.MAX_VALUE);
5959
}
6060
this.count = count;
6161
return this;

src/main/java/com/microsoft/azure/storage/blob/RequestRetryOptions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public RequestRetryOptions(RetryPolicyType retryPolicyType, Integer maxTries, In
8484
}
8585

8686
if (tryTimeout != null) {
87-
Utility.assertInBounds("tryTimeoutInMs", tryTimeout, 1, Long.MAX_VALUE);
87+
Utility.assertInBounds("tryTimeout", tryTimeout, 1, Integer.MAX_VALUE);
8888
this.tryTimeout = tryTimeout;
8989
} else {
9090
this.tryTimeout = 60;

src/main/java/com/microsoft/azure/storage/blob/TransferManager.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ public static Single<CommonRestResponse> uploadFileToBlockBlob(
106106
the list of Ids later. Eager ensures parallelism but may require some internal buffering.
107107
*/
108108
.concatMapEager(i -> {
109-
int count = Math.min(blockLength, (int) (file.size() - i * blockLength));
110-
Flowable<ByteBuffer> data = FlowableUtil.readFile(file, i * blockLength, count);
109+
// The max number of bytes for a block is currently 100MB, so the final result must be an int.
110+
int count = (int) Math.min((long)blockLength, (file.size() - i * (long)blockLength));
111+
// i * blockLength could be a long, so we need a cast to prevent overflow.
112+
Flowable<ByteBuffer> data = FlowableUtil.readFile(file, i * (long)blockLength, count);
111113

112114
// Report progress as necessary.
113115
data = ProgressReporter.addParallelProgressReporting(data, optionsReal.progressReceiver(),

src/test/java/com/microsoft/azure/storage/APISpec.groovy

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,20 @@ class APISpec extends Specification {
229229
}
230230
}
231231

232-
static ByteBuffer getRandomData(long size) {
232+
/*
233+
Size must be an int because ByteBuffer sizes can only be an int. Long is not supported.
234+
*/
235+
static ByteBuffer getRandomData(int size) {
233236
Random rand = new Random(getRandomSeed())
234237
byte[] data = new byte[size]
235238
rand.nextBytes(data)
236239
return ByteBuffer.wrap(data)
237240
}
238241

239-
static File getRandomFile(long size) {
242+
/*
243+
We only allow int because anything larger than 2GB (which would require a long) is left to stress/perf.
244+
*/
245+
static File getRandomFile(int size) {
240246
File file = File.createTempFile(UUID.randomUUID().toString(), ".txt")
241247
file.deleteOnExit()
242248
FileOutputStream fos = new FileOutputStream(file)
@@ -431,7 +437,6 @@ class APISpec extends Specification {
431437
to play too nicely with mocked objects and the complex reflection stuff on both ends made it more difficult to work
432438
with than was worth it.
433439
*/
434-
435440
def getStubResponse(int code, Class responseHeadersType) {
436441
return new HttpResponse() {
437442

@@ -477,6 +482,58 @@ class APISpec extends Specification {
477482
}
478483
}
479484

485+
/*
486+
This is for stubbing responses that will actually go through the pipeline and autorest code. Autorest does not seem
487+
to play too nicely with mocked objects and the complex reflection stuff on both ends made it more difficult to work
488+
with than was worth it. Because this type is just for BlobDownload, we don't need to accept a header type.
489+
*/
490+
def getStubResponseForBlobDownload(int code, Flowable<ByteBuffer> body, String etag) {
491+
return new HttpResponse() {
492+
493+
@Override
494+
int statusCode() {
495+
return code
496+
}
497+
498+
@Override
499+
String headerValue(String s) {
500+
return null
501+
}
502+
503+
@Override
504+
HttpHeaders headers() {
505+
return new HttpHeaders()
506+
}
507+
508+
@Override
509+
Flowable<ByteBuffer> body() {
510+
return body
511+
}
512+
513+
@Override
514+
Single<byte[]> bodyAsByteArray() {
515+
return null
516+
}
517+
518+
@Override
519+
Single<String> bodyAsString() {
520+
return null
521+
}
522+
523+
@Override
524+
Object deserializedHeaders() {
525+
def headers = new BlobDownloadHeaders()
526+
headers.withETag(etag)
527+
return headers
528+
}
529+
530+
@Override
531+
boolean isDecoded() {
532+
return true
533+
}
534+
}
535+
}
536+
480537
def getContextStubPolicy(int successCode, Class responseHeadersType) {
481538
return Mock(RequestPolicy) {
482539
sendAsync(_) >> { HttpRequest request ->

src/test/java/com/microsoft/azure/storage/BlobAPITest.groovy

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ package com.microsoft.azure.storage
1818
import com.microsoft.azure.storage.blob.*
1919
import com.microsoft.azure.storage.blob.models.*
2020
import com.microsoft.rest.v2.http.HttpPipeline
21+
import com.microsoft.rest.v2.http.HttpRequest
22+
import com.microsoft.rest.v2.policy.RequestPolicy
2123
import com.microsoft.rest.v2.util.FlowableUtil
2224
import io.reactivex.Flowable
25+
import io.reactivex.Single
2326
import spock.lang.Unroll
2427

2528
import java.nio.ByteBuffer
@@ -70,6 +73,48 @@ class BlobAPITest extends APISpec {
7073
headers.blobContentMD5() != null
7174
}
7275

76+
/*
77+
This is to test the appropriate integration of DownloadResponse, including setting the correct range values on
78+
HTTPGetterInfo.
79+
*/
80+
def "Download with retry range"() {
81+
/*
82+
We are going to make a request for some range on a blob. The Flowable returned will throw an exception, forcing
83+
a retry per the ReliableDownloadOptions. The next request should have the same range header, which was generated
84+
from the count and offset values in HTTPGetterInfo that was constructed on the initial call to download. We
85+
don't need to check the data here, but we want to ensure that the correct range is set each time. This will
86+
test the correction of a bug that was found which caused HTTPGetterInfo to have an incorrect offset when it was
87+
constructed in BlobURL.download().
88+
*/
89+
setup:
90+
def mockPolicy = Mock(RequestPolicy) {
91+
sendAsync(_) >> { HttpRequest request ->
92+
if (request.headers().value("x-ms-range") != "bytes=2-6") {
93+
return Single.error(new IllegalArgumentException("The range header was not set correctly on retry."))
94+
}
95+
else {
96+
// ETag can be a dummy value. It's not validated, but DownloadResponse requires one
97+
return Single.just(getStubResponseForBlobDownload(206, Flowable.error(new IOException()), "etag"))
98+
}
99+
}
100+
}
101+
def pipeline = HttpPipeline.build(getStubFactory(mockPolicy))
102+
bu = bu.withPipeline(pipeline)
103+
104+
when:
105+
def range = new BlobRange().withOffset(2).withCount(5)
106+
bu.download(range, null, false, null).blockingGet().body(new ReliableDownloadOptions().withMaxRetryRequests(3))
107+
.blockingSubscribe()
108+
109+
then:
110+
/*
111+
Because the dummy Flowable always throws an error. This will also validate that an IllegalArgumentException is
112+
NOT thrown because the types would not match.
113+
*/
114+
def e = thrown(RuntimeException)
115+
e.getCause() instanceof IOException
116+
}
117+
73118
def "Download min"() {
74119
expect:
75120
FlowableUtil.collectBytesInBuffer(bu.download().blockingGet().body(null)).blockingGet() == defaultData

src/test/java/com/microsoft/azure/storage/TransferManagerTest.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,9 @@ class TransferManagerTest extends APISpec {
424424
file | _
425425
getRandomFile(20) | _ // small file
426426
getRandomFile(16 * 1024 * 1024) | _ // medium file in several chunks
427-
getRandomFile(8L * 1026 * 1024 + 10) | _ // medium file not aligned to block
427+
getRandomFile(8 * 1026 * 1024 + 10) | _ // medium file not aligned to block
428428
getRandomFile(0) | _ // empty file
429-
getRandomFile(5L * 1024 * 1024 * 1024) | _ // file size exceeds max int
429+
// Files larger than 2GB to test no integer overflow are left to stress/perf tests to keep test passes short.
430430
}
431431

432432
def compareFiles(AsynchronousFileChannel channel1, long offset, long count, AsynchronousFileChannel channel2) {

0 commit comments

Comments
 (0)