diff --git a/docs/changelog/138553.yaml b/docs/changelog/138553.yaml
new file mode 100644
index 0000000000000..e1d6f2c392dbb
--- /dev/null
+++ b/docs/changelog/138553.yaml
@@ -0,0 +1,5 @@
+pr: 138553
+summary: Use common retry logic for GCS
+area: Snapshot/Restore
+type: enhancement
+issues: []
diff --git a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java
index de5f4569116db..b0f3999a86e2f 100644
--- a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java
+++ b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java
@@ -13,7 +13,6 @@
import fixture.gcs.GoogleCloudStorageHttpHandler;
import fixture.gcs.TestUtils;
-import com.google.api.gax.retrying.RetrySettings;
import com.google.cloud.http.HttpTransportOptions;
import com.google.cloud.storage.StorageOptions;
import com.google.cloud.storage.StorageRetryStrategy;
@@ -62,8 +61,10 @@
import static org.elasticsearch.common.bytes.BytesReferenceTestUtils.equalBytes;
import static org.elasticsearch.common.io.Streams.readFully;
import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomPurpose;
+import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomRetryingPurpose;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.CREDENTIALS_FILE_SETTING;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.ENDPOINT_SETTING;
+import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.MAX_RETRIES_SETTING;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageClientSettings.TOKEN_URI_SETTING;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BASE_PATH;
import static org.elasticsearch.repositories.gcs.GoogleCloudStorageRepository.BUCKET;
@@ -119,6 +120,7 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
settings.put(super.nodeSettings(nodeOrdinal, otherSettings));
settings.put(ENDPOINT_SETTING.getConcreteSettingForNamespace("test").getKey(), httpServerUrl());
settings.put(TOKEN_URI_SETTING.getConcreteSettingForNamespace("test").getKey(), httpServerUrl() + "/token");
+ settings.put(MAX_RETRIES_SETTING.getConcreteSettingForNamespace("test").getKey(), 6);
final MockSecureSettings secureSettings = new MockSecureSettings();
final byte[] serviceAccount = TestUtils.createServiceAccount(random());
@@ -196,7 +198,7 @@ public void testWriteReadLarge() throws IOException {
random().nextBytes(data);
writeBlob(container, "foobar", new BytesArray(data), false);
}
- try (InputStream stream = container.readBlob(randomPurpose(), "foobar")) {
+ try (InputStream stream = container.readBlob(randomRetryingPurpose(), "foobar")) {
BytesRefBuilder target = new BytesRefBuilder();
while (target.length() < data.length) {
byte[] buffer = new byte[scaledRandomIntBetween(1, data.length - target.length())];
@@ -219,7 +221,7 @@ public void testWriteFileMultipleOfChunkSize() throws IOException {
byte[] initialValue = randomByteArrayOfLength(uploadSize);
container.writeBlob(randomPurpose(), key, new BytesArray(initialValue), true);
- BytesReference reference = readFully(container.readBlob(randomPurpose(), key));
+ BytesReference reference = readFully(container.readBlob(randomRetryingPurpose(), key));
assertThat(reference, equalBytes(new BytesArray(initialValue)));
container.deleteBlobsIgnoringIfNotExists(randomPurpose(), Iterators.single(key));
@@ -243,24 +245,18 @@ protected GoogleCloudStorageService createStorageService(ClusterService clusterS
@Override
StorageOptions createStorageOptions(
final GoogleCloudStorageClientSettings gcsClientSettings,
- final HttpTransportOptions httpTransportOptions
+ final HttpTransportOptions httpTransportOptions,
+ final RetryBehaviour retryBehaviour
) {
- StorageOptions options = super.createStorageOptions(gcsClientSettings, httpTransportOptions);
+ StorageOptions options = super.createStorageOptions(gcsClientSettings, httpTransportOptions, retryBehaviour);
return options.toBuilder()
.setStorageRetryStrategy(StorageRetryStrategy.getLegacyStorageRetryStrategy())
- .setHost(options.getHost())
- .setCredentials(options.getCredentials())
.setRetrySettings(
- RetrySettings.newBuilder()
- .setTotalTimeout(options.getRetrySettings().getTotalTimeout())
+ options.getRetrySettings()
+ .toBuilder()
.setInitialRetryDelay(Duration.ofMillis(10L))
- .setRetryDelayMultiplier(options.getRetrySettings().getRetryDelayMultiplier())
.setMaxRetryDelay(Duration.ofSeconds(1L))
- .setMaxAttempts(0)
.setJittered(false)
- .setInitialRpcTimeout(options.getRetrySettings().getInitialRpcTimeout())
- .setRpcTimeoutMultiplier(options.getRetrySettings().getRpcTimeoutMultiplier())
- .setMaxRpcTimeout(options.getRetrySettings().getMaxRpcTimeout())
.build()
)
.build();
diff --git a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageThirdPartyTests.java b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageThirdPartyTests.java
index 520f922280eb0..7dfce9afeca0b 100644
--- a/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageThirdPartyTests.java
+++ b/modules/repository-gcs/src/internalClusterTest/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageThirdPartyTests.java
@@ -34,6 +34,7 @@
import static org.elasticsearch.common.io.Streams.readFully;
import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomPurpose;
+import static org.elasticsearch.repositories.blobstore.BlobStoreTestUtil.randomRetryingPurpose;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
@@ -115,7 +116,7 @@ public void testResumeAfterUpdate() {
executeOnBlobStore(repo, container -> {
container.writeBlob(randomPurpose(), blobKey, new BytesArray(initialValue), true);
- try (InputStream inputStream = container.readBlob(randomPurpose(), blobKey)) {
+ try (InputStream inputStream = container.readBlob(randomRetryingPurpose(), blobKey)) {
// Trigger the first request for the blob, partially read it
int read = inputStream.read();
assert read != -1;
diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java
index 2af940c19bbac..012cf450049bb 100644
--- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java
+++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageBlobStore.java
@@ -108,7 +108,7 @@ class GoogleCloudStorageBlobStore implements BlobStore {
@Nullable // for cluster level object store in MP
private final ProjectId projectId;
- private final String bucketName;
+ protected final String bucketName;
private final String clientName;
private final String repositoryName;
private final GoogleCloudStorageService storageService;
@@ -139,8 +139,32 @@ class GoogleCloudStorageBlobStore implements BlobStore {
this.casBackoffPolicy = casBackoffPolicy;
}
- private MeteredStorage client() throws IOException {
- return storageService.client(projectId, clientName, repositoryName, statsCollector);
+ /**
+ * Get a client that will retry according to its configured settings
+ *
+ * @return A client
+ */
+ MeteredStorage client() throws IOException {
+ return storageService.client(
+ projectId,
+ clientName,
+ repositoryName,
+ statsCollector,
+ GoogleCloudStorageService.RetryBehaviour.ClientConfigured
+ );
+ }
+
+ /**
+ * Get a client that will not retry on failure
+ *
+ * @return A client with max retries configured to zero
+ */
+ MeteredStorage clientNoRetries() throws IOException {
+ return storageService.client(projectId, clientName, repositoryName, statsCollector, GoogleCloudStorageService.RetryBehaviour.None);
+ }
+
+ int getMaxRetries() {
+ return storageService.clientSettings(projectId, clientName).getMaxRetries();
}
@Override
@@ -229,7 +253,7 @@ boolean blobExists(OperationPurpose purpose, String blobName) throws IOException
* @return the InputStream used to read the blob's content
*/
InputStream readBlob(OperationPurpose purpose, String blobName) throws IOException {
- return new GoogleCloudStorageRetryingInputStream(purpose, client(), BlobId.of(bucketName, blobName));
+ return new GoogleCloudStorageRetryingInputStream(this, purpose, BlobId.of(bucketName, blobName));
}
/**
@@ -252,8 +276,8 @@ InputStream readBlob(OperationPurpose purpose, String blobName, long position, l
return new ByteArrayInputStream(new byte[0]);
} else {
return new GoogleCloudStorageRetryingInputStream(
+ this,
purpose,
- client(),
BlobId.of(bucketName, blobName),
position,
Math.addExact(position, length - 1)
diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageClientSettings.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageClientSettings.java
index 582e0b48121fa..10d557ce2a8aa 100644
--- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageClientSettings.java
+++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageClientSettings.java
@@ -120,6 +120,17 @@ public class GoogleCloudStorageClientSettings {
() -> PROXY_HOST_SETTING
);
+ /**
+ * The maximum number of retries to use when a GCS request fails.
+ *
+ * Default to 5 to match {@link com.google.cloud.ServiceOptions#getDefaultRetrySettings()}
+ */
+ static final Setting.AffixSetting MAX_RETRIES_SETTING = Setting.affixKeySetting(
+ PREFIX,
+ "max_retries",
+ (key) -> Setting.intSetting(key, 5, 0, Setting.Property.NodeScope)
+ );
+
/** The credentials used by the client to connect to the Storage endpoint. */
private final ServiceAccountCredentials credential;
@@ -144,6 +155,8 @@ public class GoogleCloudStorageClientSettings {
@Nullable
private final Proxy proxy;
+ private final int maxRetries;
+
GoogleCloudStorageClientSettings(
final ServiceAccountCredentials credential,
final String endpoint,
@@ -152,7 +165,8 @@ public class GoogleCloudStorageClientSettings {
final TimeValue readTimeout,
final String applicationName,
final URI tokenUri,
- final Proxy proxy
+ final Proxy proxy,
+ final int maxRetries
) {
this.credential = credential;
this.endpoint = endpoint;
@@ -162,6 +176,7 @@ public class GoogleCloudStorageClientSettings {
this.applicationName = applicationName;
this.tokenUri = tokenUri;
this.proxy = proxy;
+ this.maxRetries = maxRetries;
}
public ServiceAccountCredentials getCredential() {
@@ -197,6 +212,10 @@ public Proxy getProxy() {
return proxy;
}
+ public int getMaxRetries() {
+ return maxRetries;
+ }
+
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
@@ -249,7 +268,8 @@ static GoogleCloudStorageClientSettings getClientSettings(final Settings setting
getConfigValue(settings, clientName, READ_TIMEOUT_SETTING),
getConfigValue(settings, clientName, APPLICATION_NAME_SETTING),
getConfigValue(settings, clientName, TOKEN_URI_SETTING),
- proxy
+ proxy,
+ getConfigValue(settings, clientName, MAX_RETRIES_SETTING)
);
}
diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java
index 2c35aac1a46e8..5b930337bc447 100644
--- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java
+++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStoragePlugin.java
@@ -93,7 +93,8 @@ public List> getSettings() {
GoogleCloudStorageClientSettings.TOKEN_URI_SETTING,
GoogleCloudStorageClientSettings.PROXY_TYPE_SETTING,
GoogleCloudStorageClientSettings.PROXY_HOST_SETTING,
- GoogleCloudStorageClientSettings.PROXY_PORT_SETTING
+ GoogleCloudStorageClientSettings.PROXY_PORT_SETTING,
+ GoogleCloudStorageClientSettings.MAX_RETRIES_SETTING
);
}
diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRetryingInputStream.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRetryingInputStream.java
index a74e86d8ee677..e233af493422d 100644
--- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRetryingInputStream.java
+++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRetryingInputStream.java
@@ -9,15 +9,15 @@
package org.elasticsearch.repositories.gcs;
import com.google.api.client.http.HttpResponse;
-import com.google.cloud.BaseService;
-import com.google.cloud.RetryHelper;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.StorageRetryStrategy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.blobstore.OperationPurpose;
-import org.elasticsearch.core.IOUtils;
+import org.elasticsearch.common.blobstore.RetryingInputStream;
+import org.elasticsearch.core.Nullable;
import org.elasticsearch.repositories.blobstore.RequestedRangeNotSatisfiedException;
import org.elasticsearch.rest.RestStatus;
@@ -25,129 +25,139 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.NoSuchFileException;
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.elasticsearch.core.Strings.format;
/**
* Wrapper around reads from GCS that will retry blob downloads that fail part-way through, resuming from where the failure occurred.
- * This should be handled by the SDK but it isn't today. This should be revisited in the future (e.g. before removing
- * the {@link org.elasticsearch.Version#V_7_0_0} version constant) and removed if the SDK handles retries itself in the future.
+ *
+ * We make use of the retry logic from {@link RetryingInputStream}, which is slightly more sophisticated and tailored to our needs than
+ * the retry logic the GCS SDK provides by default.
*/
-class GoogleCloudStorageRetryingInputStream extends InputStream {
+class GoogleCloudStorageRetryingInputStream extends RetryingInputStream {
private static final Logger logger = LogManager.getLogger(GoogleCloudStorageRetryingInputStream.class);
+ private static final StorageRetryStrategy STORAGE_RETRY_STRATEGY = GoogleCloudStorageService.createStorageRetryStrategy();
- static final int MAX_SUPPRESSED_EXCEPTIONS = 10;
-
- private final OperationPurpose purpose;
- private final MeteredStorage client;
- private final BlobId blobId;
- private final long start;
- private final long end;
- private final int maxAttempts;
- private InputStream currentStream;
- private int attempt = 1;
- private List failures = new ArrayList<>(MAX_SUPPRESSED_EXCEPTIONS);
- private long currentOffset;
- private boolean closed;
- private Long lastGeneration;
+ GoogleCloudStorageRetryingInputStream(GoogleCloudStorageBlobStore blobStore, OperationPurpose purpose, BlobId blobId)
+ throws IOException {
+ this(blobStore, purpose, blobId, 0, Long.MAX_VALUE - 1);
+ }
- // Used for testing only
- GoogleCloudStorageRetryingInputStream(OperationPurpose purpose, MeteredStorage client, BlobId blobId) throws IOException {
- this(purpose, client, blobId, 0, Long.MAX_VALUE - 1);
+ GoogleCloudStorageRetryingInputStream(
+ GoogleCloudStorageBlobStore blobStore,
+ OperationPurpose purpose,
+ BlobId blobId,
+ long start,
+ long end
+ ) throws IOException {
+ super(new GoogleCloudStorageBlobStoreServices(blobStore, purpose, blobId), purpose, start, end);
}
- // Used for testing only
- GoogleCloudStorageRetryingInputStream(OperationPurpose purpose, MeteredStorage client, BlobId blobId, long start, long end)
- throws IOException {
- if (start < 0L) {
- throw new IllegalArgumentException("start must be non-negative");
- }
- if (end < start || end == Long.MAX_VALUE) {
- throw new IllegalArgumentException("end must be >= start and not Long.MAX_VALUE");
+ private static class GoogleCloudStorageBlobStoreServices implements BlobStoreServices {
+
+ private final GoogleCloudStorageBlobStore blobStore;
+ private final OperationPurpose purpose;
+ private final BlobId blobId;
+
+ private GoogleCloudStorageBlobStoreServices(GoogleCloudStorageBlobStore blobStore, OperationPurpose purpose, BlobId blobId) {
+ this.blobStore = blobStore;
+ this.purpose = purpose;
+ this.blobId = blobId;
}
- this.purpose = purpose;
- this.client = client;
- this.blobId = blobId;
- this.start = start;
- this.end = end;
- this.maxAttempts = client.getOptions().getRetrySettings().getMaxAttempts();
- this.currentStream = openStream();
- }
- private InputStream openStream() throws IOException {
- try {
+ @Override
+ public SingleAttemptInputStream getInputStream(@Nullable Long lastGeneration, long start, long end) throws IOException {
+ final MeteredStorage client = blobStore.clientNoRetries();
try {
- return RetryHelper.runWithRetries(() -> {
- try {
- final var meteredGet = client.meteredObjectsGet(purpose, blobId.getBucket(), blobId.getName());
- meteredGet.setReturnRawInputStream(true);
- if (lastGeneration != null) {
- meteredGet.setGeneration(lastGeneration);
- }
+ try {
+ final var meteredGet = client.meteredObjectsGet(purpose, blobId.getBucket(), blobId.getName());
+ meteredGet.setReturnRawInputStream(true);
+ if (lastGeneration != null) {
+ meteredGet.setGeneration(lastGeneration);
+ }
- if (currentOffset > 0 || start > 0 || end < Long.MAX_VALUE - 1) {
- if (meteredGet.getRequestHeaders() != null) {
- meteredGet.getRequestHeaders().setRange("bytes=" + Math.addExact(start, currentOffset) + "-" + end);
- }
- }
- final HttpResponse resp = meteredGet.executeMedia();
- // Store the generation of the first response we received, so we can detect
- // if the file has changed if we need to resume
- if (lastGeneration == null) {
- lastGeneration = parseGenerationHeader(resp);
+ if (start > 0 || end < Long.MAX_VALUE - 1) {
+ if (meteredGet.getRequestHeaders() != null) {
+ meteredGet.getRequestHeaders().setRange("bytes=" + start + "-" + end);
}
+ }
+ final HttpResponse resp = meteredGet.executeMedia();
+ // Store the generation of the first response we received, so we can detect
+ // if the file has changed if we need to resume
+ if (lastGeneration == null) {
+ lastGeneration = parseGenerationHeader(resp);
+ }
- final Long contentLength = resp.getHeaders().getContentLength();
- InputStream content = resp.getContent();
- if (contentLength != null) {
- content = new ContentLengthValidatingInputStream(content, contentLength);
- }
- return content;
- } catch (IOException e) {
- throw StorageException.translate(e);
+ final Long contentLength = resp.getHeaders().getContentLength();
+ InputStream content = resp.getContent();
+ if (contentLength != null) {
+ content = new ContentLengthValidatingInputStream(content, contentLength);
}
- }, client.getOptions().getRetrySettings(), BaseService.EXCEPTION_HANDLER, client.getOptions().getClock());
- } catch (RetryHelper.RetryHelperException e) {
- throw StorageException.translateAndThrow(e);
- }
- } catch (StorageException storageException) {
- if (storageException.getCode() == RestStatus.NOT_FOUND.getStatus()) {
- if (lastGeneration != null) {
- throw addSuppressedExceptions(
- new NoSuchFileException(
+ return new SingleAttemptInputStream<>(content, start, lastGeneration);
+ } catch (IOException e) {
+ throw StorageException.translate(e);
+ }
+ } catch (StorageException storageException) {
+ if (storageException.getCode() == RestStatus.NOT_FOUND.getStatus()) {
+ if (lastGeneration != null) {
+ throw new NoSuchFileException(
"Blob object ["
+ blobId.getName()
+ "] generation ["
+ lastGeneration
+ "] unavailable on resume (contents changed, or object deleted): "
+ storageException.getMessage()
- )
- );
- } else {
- throw addSuppressedExceptions(
- new NoSuchFileException("Blob object [" + blobId.getName() + "] not found: " + storageException.getMessage())
- );
+ );
+ } else {
+ throw new NoSuchFileException("Blob object [" + blobId.getName() + "] not found: " + storageException.getMessage());
+ }
}
- }
- if (storageException.getCode() == RestStatus.REQUESTED_RANGE_NOT_SATISFIED.getStatus()) {
- long currentPosition = Math.addExact(start, currentOffset);
- throw addSuppressedExceptions(
- new RequestedRangeNotSatisfiedException(
+ if (storageException.getCode() == RestStatus.REQUESTED_RANGE_NOT_SATISFIED.getStatus()) {
+ throw new RequestedRangeNotSatisfiedException(
blobId.getName(),
- currentPosition,
- (end < Long.MAX_VALUE - 1) ? end - currentPosition + 1 : end,
+ start,
+ (end < Long.MAX_VALUE - 1) ? end - start + 1 : end,
storageException
- )
- );
+ );
+ }
+ throw storageException;
}
- throw addSuppressedExceptions(storageException);
+ }
+
+ @Override
+ public void onRetryStarted(StreamAction action) {
+ // No retry metrics for GCS
+ }
+
+ @Override
+ public void onRetrySucceeded(StreamAction action, long numberOfRetries) {
+ // No retry metrics for GCS
+ }
+
+ @Override
+ public long getMeaningfulProgressSize() {
+ return Math.max(1L, GoogleCloudStorageBlobStore.SDK_DEFAULT_CHUNK_SIZE / 100L);
+ }
+
+ @Override
+ public int getMaxRetries() {
+ return blobStore.getMaxRetries();
+ }
+
+ @Override
+ public String getBlobDescription() {
+ return blobId.toString();
+ }
+
+ @Override
+ public boolean isRetryableException(StreamAction action, Exception e) {
+ return switch (action) {
+ case OPEN -> STORAGE_RETRY_STRATEGY.getIdempotentHandler().shouldRetry(e, null);
+ case READ -> true;
+ };
}
}
- private Long parseGenerationHeader(HttpResponse response) {
+ private static Long parseGenerationHeader(HttpResponse response) {
final String generationHeader = response.getHeaders().getFirstHeaderStringValue("x-goog-generation");
if (generationHeader != null) {
try {
@@ -178,68 +188,49 @@ static final class ContentLengthValidatingInputStream extends FilterInputStream
}
@Override
- public int read(byte[] b, int off, int len) throws IOException {
- final int n = in.read(b, off, len);
- if (n == -1) {
- checkContentLengthOnEOF();
- } else {
- read += n;
+ public int read(byte[] b, int off, int len) {
+ try {
+ final int n = in.read(b, off, len);
+ if (n == -1) {
+ checkContentLengthOnEOF();
+ } else {
+ read += n;
+ }
+ return n;
+ } catch (IOException e) {
+ throw StorageException.translate(e);
}
- return n;
}
@Override
- public int read() throws IOException {
- final int n = in.read();
- if (n == -1) {
- checkContentLengthOnEOF();
- } else {
- read++;
+ public int read() {
+ try {
+ final int n = in.read();
+ if (n == -1) {
+ checkContentLengthOnEOF();
+ } else {
+ read++;
+ }
+ return n;
+ } catch (IOException e) {
+ throw StorageException.translate(e);
}
- return n;
}
@Override
- public long skip(long len) throws IOException {
- final long n = in.skip(len);
- read += n;
- return n;
- }
-
- private void checkContentLengthOnEOF() throws IOException {
- if (read < contentLength) {
- throw new IOException("Connection closed prematurely: read = " + read + ", Content-Length = " + contentLength);
- }
- }
- }
-
- @Override
- public int read() throws IOException {
- ensureOpen();
- while (true) {
+ public long skip(long len) {
try {
- final int result = currentStream.read();
- currentOffset += 1;
- return result;
+ final long n = in.skip(len);
+ read += n;
+ return n;
} catch (IOException e) {
- reopenStreamOrFail(StorageException.translate(e));
+ throw StorageException.translate(e);
}
}
- }
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- ensureOpen();
- while (true) {
- try {
- final int bytesRead = currentStream.read(b, off, len);
- if (bytesRead == -1) {
- return -1;
- }
- currentOffset += bytesRead;
- return bytesRead;
- } catch (IOException e) {
- reopenStreamOrFail(StorageException.translate(e));
+ private void checkContentLengthOnEOF() throws IOException {
+ if (read < contentLength) {
+ throw new IOException("Connection closed prematurely: read = " + read + ", Content-Length = " + contentLength);
}
}
}
@@ -251,52 +242,4 @@ public int read(byte[] b, int off, int len) throws IOException {
void closeCurrentStream() throws IOException {
currentStream.close();
}
-
- private void ensureOpen() {
- if (closed) {
- assert false : "using GoogleCloudStorageRetryingInputStream after close";
- throw new IllegalStateException("using GoogleCloudStorageRetryingInputStream after close");
- }
- }
-
- private void reopenStreamOrFail(StorageException e) throws IOException {
- if (attempt >= maxAttempts) {
- throw addSuppressedExceptions(e);
- }
- logger.debug(
- () -> format("failed reading [%s] at offset [%s], attempt [%s] of [%s], retrying", blobId, currentOffset, attempt, maxAttempts),
- e
- );
- attempt += 1;
- if (failures.size() < MAX_SUPPRESSED_EXCEPTIONS) {
- failures.add(e);
- }
- IOUtils.closeWhileHandlingException(currentStream);
- currentStream = openStream();
- }
-
- @Override
- public void close() throws IOException {
- currentStream.close();
- closed = true;
- }
-
- @Override
- public long skip(long n) throws IOException {
- // This could be optimized on a failure by re-opening stream directly to the preferred location. However, it is rarely called,
- // so for now we will rely on the default implementation which just discards bytes by reading.
- return super.skip(n);
- }
-
- @Override
- public void reset() {
- throw new UnsupportedOperationException("GoogleCloudStorageRetryingInputStream does not support seeking");
- }
-
- private T addSuppressedExceptions(T e) {
- for (StorageException failure : failures) {
- e.addSuppressed(failure);
- }
- return e;
- }
}
diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java
index cb6b8b334181e..c175a991e5fa9 100644
--- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java
+++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageService.java
@@ -15,6 +15,7 @@
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.SecurityUtils;
import com.google.api.gax.retrying.ResultRetryAlgorithm;
+import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.ServiceOptions;
@@ -94,6 +95,23 @@ public void refreshAndClearCache(Map c
clientsManager.refreshAndClearCacheForClusterClients(clientsSettings);
}
+ public enum RetryBehaviour {
+ ClientConfigured {
+ @Override
+ public RetrySettings createRetrySettings(GoogleCloudStorageClientSettings settings) {
+ return ServiceOptions.getDefaultRetrySettings().toBuilder().setMaxAttempts(settings.getMaxRetries() + 1).build();
+ }
+ },
+ None {
+ @Override
+ public RetrySettings createRetrySettings(GoogleCloudStorageClientSettings settings) {
+ return ServiceOptions.getNoRetrySettings();
+ }
+ };
+
+ public abstract RetrySettings createRetrySettings(GoogleCloudStorageClientSettings settings);
+ }
+
/**
* Attempts to retrieve a client from the cache. If the client does not exist it
* will be created from the latest settings and will populate the cache. The
@@ -111,9 +129,14 @@ public MeteredStorage client(
@Nullable final ProjectId projectId,
final String clientName,
final String repositoryName,
- final GcsRepositoryStatsCollector statsCollector
+ final GcsRepositoryStatsCollector statsCollector,
+ final RetryBehaviour retryBehaviour
) throws IOException {
- return clientsManager.client(projectId, clientName, repositoryName, statsCollector);
+ return clientsManager.client(projectId, clientName, repositoryName, statsCollector, retryBehaviour);
+ }
+
+ GoogleCloudStorageClientSettings clientSettings(@Nullable ProjectId projectId, final String clientName) {
+ return clientsManager.clientSettings(projectId, clientName);
}
/**
@@ -138,8 +161,11 @@ GoogleCloudStorageClientsManager getClientsManager() {
* @return a new client storage instance that can be used to manage objects
* (blobs)
*/
- private MeteredStorage createClient(GoogleCloudStorageClientSettings gcsClientSettings, GcsRepositoryStatsCollector statsCollector)
- throws IOException {
+ private MeteredStorage createClient(
+ GoogleCloudStorageClientSettings gcsClientSettings,
+ GcsRepositoryStatsCollector statsCollector,
+ RetryBehaviour retryBehaviour
+ ) throws IOException {
final NetHttpTransport.Builder builder = new NetHttpTransport.Builder();
// requires java.lang.RuntimePermission "setFactory"
@@ -183,22 +209,24 @@ public HttpRequestInitializer getHttpRequestInitializer(ServiceOptions, ?> ser
}
};
- final StorageOptions storageOptions = createStorageOptions(gcsClientSettings, httpTransportOptions);
+ final StorageOptions storageOptions = createStorageOptions(gcsClientSettings, httpTransportOptions, retryBehaviour);
return new MeteredStorage(storageOptions.getService(), statsCollector);
}
StorageOptions createStorageOptions(
final GoogleCloudStorageClientSettings gcsClientSettings,
- final HttpTransportOptions httpTransportOptions
+ final HttpTransportOptions httpTransportOptions,
+ final RetryBehaviour retryBehaviour
) {
final StorageOptions.Builder storageOptionsBuilder = StorageOptions.newBuilder()
- .setStorageRetryStrategy(getRetryStrategy())
+ .setStorageRetryStrategy(createStorageRetryStrategy())
.setTransportOptions(httpTransportOptions)
.setHeaderProvider(() -> {
return Strings.hasLength(gcsClientSettings.getApplicationName())
? Map.of("user-agent", gcsClientSettings.getApplicationName())
: Map.of();
- });
+ })
+ .setRetrySettings(retryBehaviour.createRetrySettings(gcsClientSettings));
if (Strings.hasLength(gcsClientSettings.getHost())) {
storageOptionsBuilder.setHost(gcsClientSettings.getHost());
}
@@ -247,7 +275,7 @@ StorageOptions createStorageOptions(
return storageOptionsBuilder.build();
}
- protected StorageRetryStrategy getRetryStrategy() {
+ static StorageRetryStrategy createStorageRetryStrategy() {
return ShouldRetryDecorator.decorate(
StorageRetryStrategy.getLegacyStorageRetryStrategy(),
(Throwable prevThrowable, Object prevResponse, ResultRetryAlgorithm