|  | @@ -9,6 +9,7 @@
 | 
	
		
			
				|  |  |  package org.elasticsearch.repositories.s3;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import com.amazonaws.services.s3.AmazonS3;
 | 
	
		
			
				|  |  | +import com.amazonaws.services.s3.model.AmazonS3Exception;
 | 
	
		
			
				|  |  |  import com.amazonaws.services.s3.model.GetObjectRequest;
 | 
	
		
			
				|  |  |  import com.amazonaws.services.s3.model.S3Object;
 | 
	
		
			
				|  |  |  import com.amazonaws.services.s3.model.S3ObjectInputStream;
 | 
	
	
		
			
				|  | @@ -16,6 +17,8 @@ import com.amazonaws.services.s3.model.S3ObjectInputStream;
 | 
	
		
			
				|  |  |  import org.apache.http.client.methods.HttpGet;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.io.Streams;
 | 
	
		
			
				|  |  |  import org.elasticsearch.core.Nullable;
 | 
	
		
			
				|  |  | +import org.elasticsearch.repositories.blobstore.RequestedRangeNotSatisfiedException;
 | 
	
		
			
				|  |  | +import org.elasticsearch.rest.RestStatus;
 | 
	
		
			
				|  |  |  import org.elasticsearch.test.ESTestCase;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import java.io.ByteArrayInputStream;
 | 
	
	
		
			
				|  | @@ -23,7 +26,9 @@ import java.io.IOException;
 | 
	
		
			
				|  |  |  import java.util.Arrays;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomPurpose;
 | 
	
		
			
				|  |  | +import static org.hamcrest.Matchers.equalTo;
 | 
	
		
			
				|  |  |  import static org.hamcrest.Matchers.is;
 | 
	
		
			
				|  |  | +import static org.hamcrest.Matchers.startsWith;
 | 
	
		
			
				|  |  |  import static org.mockito.ArgumentMatchers.any;
 | 
	
		
			
				|  |  |  import static org.mockito.Mockito.mock;
 | 
	
		
			
				|  |  |  import static org.mockito.Mockito.when;
 | 
	
	
		
			
				|  | @@ -81,24 +86,61 @@ public class S3RetryingInputStreamTests extends ESTestCase {
 | 
	
		
			
				|  |  |          assertThat(stream.isAborted(), is(true));
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    public void testReadAfterBlobLengthThrowsRequestedRangeNotSatisfiedException() throws IOException {
 | 
	
		
			
				|  |  | +        final byte[] bytes = randomByteArrayOfLength(randomIntBetween(1, 512));
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            final int position = bytes.length + randomIntBetween(0, 100);
 | 
	
		
			
				|  |  | +            final int length = randomIntBetween(1, 100);
 | 
	
		
			
				|  |  | +            var exception = expectThrows(RequestedRangeNotSatisfiedException.class, () -> {
 | 
	
		
			
				|  |  | +                try (var ignored = createInputStream(bytes, position, length)) {
 | 
	
		
			
				|  |  | +                    fail();
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            });
 | 
	
		
			
				|  |  | +            assertThat(exception.getResource(), equalTo("_blob"));
 | 
	
		
			
				|  |  | +            assertThat(exception.getPosition(), equalTo((long) position));
 | 
	
		
			
				|  |  | +            assertThat(exception.getLength(), equalTo((long) length));
 | 
	
		
			
				|  |  | +            assertThat(
 | 
	
		
			
				|  |  | +                exception.getMessage(),
 | 
	
		
			
				|  |  | +                startsWith("Requested range [position=" + position + ", length=" + length + "] cannot be satisfied for [_blob]")
 | 
	
		
			
				|  |  | +            );
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            int position = randomIntBetween(0, Math.max(0, bytes.length - 1));
 | 
	
		
			
				|  |  | +            int maxLength = bytes.length - position;
 | 
	
		
			
				|  |  | +            int length = randomIntBetween(maxLength + 1, Integer.MAX_VALUE - 1);
 | 
	
		
			
				|  |  | +            try (var stream = createInputStream(bytes, position, length)) {
 | 
	
		
			
				|  |  | +                assertThat(Streams.consumeFully(stream), equalTo((long) maxLength));
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private S3RetryingInputStream createInputStream(final byte[] data, @Nullable final Integer position, @Nullable final Integer length)
 | 
	
		
			
				|  |  |          throws IOException {
 | 
	
		
			
				|  |  | -        final S3Object s3Object = new S3Object();
 | 
	
		
			
				|  |  |          final AmazonS3 client = mock(AmazonS3.class);
 | 
	
		
			
				|  |  | -        when(client.getObject(any(GetObjectRequest.class))).thenReturn(s3Object);
 | 
	
		
			
				|  |  |          final AmazonS3Reference clientReference = mock(AmazonS3Reference.class);
 | 
	
		
			
				|  |  |          when(clientReference.client()).thenReturn(client);
 | 
	
		
			
				|  |  |          final S3BlobStore blobStore = mock(S3BlobStore.class);
 | 
	
		
			
				|  |  |          when(blobStore.clientReference()).thenReturn(clientReference);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          if (position != null && length != null) {
 | 
	
		
			
				|  |  | +            if (data.length <= position) {
 | 
	
		
			
				|  |  | +                var amazonS3Exception = new AmazonS3Exception("test");
 | 
	
		
			
				|  |  | +                amazonS3Exception.setStatusCode(RestStatus.REQUESTED_RANGE_NOT_SATISFIED.getStatus());
 | 
	
		
			
				|  |  | +                when(client.getObject(any(GetObjectRequest.class))).thenThrow(amazonS3Exception);
 | 
	
		
			
				|  |  | +                return new S3RetryingInputStream(randomPurpose(), blobStore, "_blob", position, Math.addExact(position, length - 1));
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            final S3Object s3Object = new S3Object();
 | 
	
		
			
				|  |  |              s3Object.getObjectMetadata().setContentLength(length);
 | 
	
		
			
				|  |  |              s3Object.setObjectContent(new S3ObjectInputStream(new ByteArrayInputStream(data, position, length), new HttpGet()));
 | 
	
		
			
				|  |  | +            when(client.getObject(any(GetObjectRequest.class))).thenReturn(s3Object);
 | 
	
		
			
				|  |  |              return new S3RetryingInputStream(randomPurpose(), blobStore, "_blob", position, Math.addExact(position, length - 1));
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | -            s3Object.getObjectMetadata().setContentLength(data.length);
 | 
	
		
			
				|  |  | -            s3Object.setObjectContent(new S3ObjectInputStream(new ByteArrayInputStream(data), new HttpGet()));
 | 
	
		
			
				|  |  | -            return new S3RetryingInputStream(randomPurpose(), blobStore, "_blob");
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        final S3Object s3Object = new S3Object();
 | 
	
		
			
				|  |  | +        s3Object.getObjectMetadata().setContentLength(data.length);
 | 
	
		
			
				|  |  | +        s3Object.setObjectContent(new S3ObjectInputStream(new ByteArrayInputStream(data), new HttpGet()));
 | 
	
		
			
				|  |  | +        when(client.getObject(any(GetObjectRequest.class))).thenReturn(s3Object);
 | 
	
		
			
				|  |  | +        return new S3RetryingInputStream(randomPurpose(), blobStore, "_blob");
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  }
 |