-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Storage - SeekableByteChannel Unnecessary Request Bugfix #49526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6a42a8e
7527c30
d3f9b98
62d4306
a98f5c8
c66aa40
2a2f21a
7a68071
cf27db8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,9 +35,11 @@ | |
| import java.util.stream.Stream; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.ArgumentMatchers.anyBoolean; | ||
| import static org.mockito.ArgumentMatchers.eq; | ||
| import static org.mockito.Mockito.never; | ||
| import static org.mockito.Mockito.times; | ||
| import static org.mockito.Mockito.verify; | ||
|
|
||
|
|
@@ -273,4 +275,110 @@ void readDetectsBlobGrowth() throws IOException { | |
| assertEquals(buffer.capacity(), buffer.position()); | ||
| TestUtils.assertArraysEqual(data, halfLength, buffer.array(), 0, data.length - halfLength); | ||
| } | ||
|
|
||
| /** | ||
| * When the request conditions lock the blob via an If-Match ETag, a read at or past the known end of the | ||
| * resource must short-circuit to EOF without issuing a service call (which the service would reject with an | ||
| * HTTP 416 response). | ||
| */ | ||
| @Test | ||
| public void readPastEndShortCircuitsWhenETagLocked() throws IOException { | ||
| BlobClientBase client = Mockito.mock(BlobClientBase.class); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think we could move this to the |
||
|
|
||
| long resourceLength = Constants.KB; | ||
| BlobRequestConditions conditions = new BlobRequestConditions().setIfMatch("0xETAG"); | ||
| StorageSeekableByteChannelBlobReadBehavior behavior = new StorageSeekableByteChannelBlobReadBehavior(client, | ||
| ByteBuffer.allocate(0), -1, resourceLength, conditions); | ||
|
|
||
| // when: "Reading at the known end of the resource" | ||
| int readAtEnd = behavior.read(ByteBuffer.allocate(Constants.KB), resourceLength); | ||
|
|
||
| // then: "EOF is signaled without any service call" | ||
| assertEquals(-1, readAtEnd); | ||
| verify(client, never()).downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any()); | ||
|
|
||
| // when: "Reading past the known end of the resource" | ||
| int readPastEnd = behavior.read(ByteBuffer.allocate(Constants.KB), resourceLength + Constants.KB); | ||
|
|
||
| // then: "EOF is still signaled without any service call" | ||
| assertEquals(-1, readPastEnd); | ||
| verify(client, never()).downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any()); | ||
|
|
||
| // If-Match="*" is an existence precondition (not a specific ETag lock), so growth-detection behavior should be | ||
| // preserved and a request should still be issued. | ||
| Mockito.when(client.downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any())) | ||
| .thenReturn(createMockDownloadResponse( | ||
| "bytes " + resourceLength + "-" + (resourceLength + Constants.KB - 1) + "/" + 2 * Constants.KB)); | ||
|
|
||
| BlobRequestConditions ifMatchStar = new BlobRequestConditions().setIfMatch("*"); | ||
| StorageSeekableByteChannelBlobReadBehavior starBehavior = new StorageSeekableByteChannelBlobReadBehavior(client, | ||
| ByteBuffer.allocate(0), -1, resourceLength, ifMatchStar); | ||
|
|
||
| starBehavior.read(ByteBuffer.allocate(Constants.KB), resourceLength); | ||
|
|
||
| verify(client, times(1)).downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any()); | ||
| } | ||
|
|
||
| /** | ||
| * When the blob is not ETag-locked, a read past the end must still issue a request so the behavior can | ||
| * detect blob growth (existing contract preserved). | ||
| */ | ||
| @Test | ||
| public void readPastEndIssuesRequestWhenNotETagLocked() throws IOException { | ||
| BlobClientBase client = Mockito.mock(BlobClientBase.class); | ||
| long resourceLength = Constants.KB; | ||
| Mockito.when(client.downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any())) | ||
| .thenReturn(createMockDownloadResponse( | ||
| "bytes " + resourceLength + "-" + (resourceLength + Constants.KB - 1) + "/" + 2 * Constants.KB)); | ||
|
|
||
| StorageSeekableByteChannelBlobReadBehavior behavior | ||
| = new StorageSeekableByteChannelBlobReadBehavior(client, ByteBuffer.allocate(0), -1, resourceLength, null); | ||
|
|
||
| // when: "Reading past the known end of the resource without an ETag lock" | ||
| behavior.read(ByteBuffer.allocate(Constants.KB), resourceLength); | ||
|
|
||
| // then: "A service call is issued (existing growth-detection behavior preserved)" | ||
| verify(client, times(1)).downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any()); | ||
| } | ||
|
|
||
| /** | ||
| * If the underlying download throws a non-{@code BlobStorageException} (for example, the connection is | ||
| * reset while streaming the body of a 416 response) and the caller is already at or past the known end of | ||
| * the resource, the behavior should swallow the error, log a warning, and signal EOF rather than throwing. | ||
| */ | ||
| @Test | ||
| public void readPastEndSwallowsTransportErrorAndSignalsEof() throws IOException { | ||
| BlobClientBase client = Mockito.mock(BlobClientBase.class); | ||
| long resourceLength = Constants.KB; | ||
| Mockito.when(client.downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any())) | ||
| .thenThrow(new RuntimeException("Connection reset by peer")); | ||
|
|
||
| StorageSeekableByteChannelBlobReadBehavior behavior | ||
| = new StorageSeekableByteChannelBlobReadBehavior(client, ByteBuffer.allocate(0), -1, resourceLength, null); | ||
|
|
||
| // when: "Reading past the known end and the download fails with a transport-level error" | ||
| int read = behavior.read(ByteBuffer.allocate(Constants.KB), resourceLength); | ||
|
|
||
| // then: "EOF is signaled instead of the exception propagating" | ||
| assertEquals(-1, read); | ||
| verify(client, times(1)).downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any()); | ||
| } | ||
|
|
||
| /** | ||
| * Non-{@code BlobStorageException} failures that occur while reading within the known bounds of the resource | ||
| * must continue to surface as exceptions, since some bytes the caller asked for could not be retrieved. | ||
| */ | ||
| @Test | ||
| public void readWithinResourcePropagatesTransportError() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be a clearer name: readBeforeEndOfBlobPropagatesTransportError |
||
| BlobClientBase client = Mockito.mock(BlobClientBase.class); | ||
| long resourceLength = Constants.KB; | ||
| Mockito.when(client.downloadStreamWithResponse(any(), any(), any(), any(), anyBoolean(), any(), any())) | ||
| .thenThrow(new RuntimeException("Connection reset by peer")); | ||
|
|
||
| StorageSeekableByteChannelBlobReadBehavior behavior | ||
| = new StorageSeekableByteChannelBlobReadBehavior(client, ByteBuffer.allocate(0), -1, resourceLength, null); | ||
|
|
||
| // Reading within the known resource range should still surface the error. | ||
| assertThrows(RuntimeException.class, () -> behavior.read(ByteBuffer.allocate(Constants.KB), 0)); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this is just hard to read because of the multiple negations. Is it possible to simplify this so that it is just easier to understand e.g. isIfMatchPopulated(ifMatch) && isIfMatchNotAWildcard(ifMatch)
idk, it just takes way more time to read it with all of the double negations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe something like this might be easier to understand: