From 38e690de3c85226ec3d59d3256e10aa46e3f2c33 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Wed, 1 May 2024 18:37:54 -0400 Subject: [PATCH] fix: update StorageOptions to carry forward fields that aren't part of ServiceOptions --- .../clirr-ignored-differences.xml | 12 ++ .../storage/BidiBlobWriteSessionConfig.java | 18 ++ .../cloud/storage/BlobWriteSessionConfig.java | 6 + .../cloud/storage/BufferToDiskThenUpload.java | 20 +++ .../DefaultBlobWriteSessionConfig.java | 18 ++ .../storage/GrpcRetryAlgorithmManager.java | 26 ++- .../cloud/storage/GrpcStorageOptions.java | 30 +++- .../storage/HttpRetryAlgorithmManager.java | 2 +- .../cloud/storage/HttpStorageOptions.java | 19 +- .../JournalingBlobWriteSessionConfig.java | 20 +++ ...CompositeUploadBlobWriteSessionConfig.java | 167 ++++++++++++++++++ .../storage/StorageOptionsBuilderTest.java | 84 +++++++++ 12 files changed, 414 insertions(+), 8 deletions(-) create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index df4e590cc..d81c37bb2 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -84,4 +84,16 @@ com/google/cloud/storage/WriteFlushStrategy$DefaultBidiFlusher + + + 7013 + com/google/cloud/storage/BlobWriteSessionConfig + int hashCode() + + + 7013 + com/google/cloud/storage/BlobWriteSessionConfig + boolean equals(java.lang.Object) + + diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java index 390c5abbc..7a80fed49 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BidiBlobWriteSessionConfig.java @@ -28,6 +28,7 @@ import com.google.storage.v2.BidiWriteObjectResponse; import java.io.IOException; import java.time.Clock; +import java.util.Objects; import javax.annotation.concurrent.Immutable; /** @@ -64,6 +65,23 @@ public int getBufferSize() { return bufferSize; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BidiBlobWriteSessionConfig)) { + return false; + } + BidiBlobWriteSessionConfig that = (BidiBlobWriteSessionConfig) o; + return bufferSize == that.bufferSize; + } + + @Override + public int hashCode() { + return Objects.hashCode(bufferSize); + } + @Override WriterFactory createFactory(Clock clock) throws IOException { return new Factory(bufferSize); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java index 441f78ac7..7595822ce 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobWriteSessionConfig.java @@ -43,6 +43,12 @@ public abstract class BlobWriteSessionConfig implements Serializable { @InternalApi BlobWriteSessionConfig() {} + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); + @InternalApi abstract WriterFactory createFactory(Clock clock) throws IOException; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java index 58ce10293..bfd85c1d8 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BufferToDiskThenUpload.java @@ -41,6 +41,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collection; +import java.util.Objects; import java.util.stream.Collector; import javax.annotation.concurrent.Immutable; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -81,6 +82,25 @@ public final class BufferToDiskThenUpload extends BlobWriteSessionConfig this.includeLoggingSink = includeLoggingSink; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BufferToDiskThenUpload)) { + return false; + } + BufferToDiskThenUpload that = (BufferToDiskThenUpload) o; + return includeLoggingSink == that.includeLoggingSink + && Objects.equals(paths, that.paths) + && Objects.equals(absolutePaths, that.absolutePaths); + } + + @Override + public int hashCode() { + return Objects.hash(paths, includeLoggingSink, absolutePaths); + } + @VisibleForTesting @InternalApi BufferToDiskThenUpload withIncludeLoggingSink() throws IOException { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java index ffcbcfa8e..4b6c34ef6 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java @@ -37,6 +37,7 @@ import java.nio.channels.WritableByteChannel; import java.time.Clock; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; import javax.annotation.concurrent.Immutable; @@ -105,6 +106,23 @@ public DefaultBlobWriteSessionConfig withChunkSize(int chunkSize) { return new DefaultBlobWriteSessionConfig(chunkSize); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultBlobWriteSessionConfig)) { + return false; + } + DefaultBlobWriteSessionConfig that = (DefaultBlobWriteSessionConfig) o; + return chunkSize == that.chunkSize; + } + + @Override + public int hashCode() { + return Objects.hashCode(chunkSize); + } + @Override @InternalApi WriterFactory createFactory(Clock clock) { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java index 1178c7c94..9eb0a581c 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java @@ -17,6 +17,7 @@ package com.google.cloud.storage; import com.google.api.gax.retrying.ResultRetryAlgorithm; +import com.google.common.base.MoreObjects; import com.google.iam.v1.GetIamPolicyRequest; import com.google.iam.v1.SetIamPolicyRequest; import com.google.iam.v1.TestIamPermissionsRequest; @@ -50,11 +51,12 @@ import com.google.storage.v2.UpdateObjectRequest; import com.google.storage.v2.WriteObjectRequest; import java.io.Serializable; +import java.util.Objects; final class GrpcRetryAlgorithmManager implements Serializable { private static final long serialVersionUID = 3084833873820431477L; - private final StorageRetryStrategy retryStrategy; + final StorageRetryStrategy retryStrategy; GrpcRetryAlgorithmManager(StorageRetryStrategy retryStrategy) { this.retryStrategy = retryStrategy; @@ -217,4 +219,26 @@ public ResultRetryAlgorithm getFor(BidiWriteObjectRequest req) { ? retryStrategy.getIdempotentHandler() : retryStrategy.getNonidempotentHandler(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GrpcRetryAlgorithmManager)) { + return false; + } + GrpcRetryAlgorithmManager that = (GrpcRetryAlgorithmManager) o; + return Objects.equals(retryStrategy, that.retryStrategy); + } + + @Override + public int hashCode() { + return Objects.hashCode(retryStrategy); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("retryStrategy", retryStrategy).toString(); + } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index 438b5fd59..38acf8283 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -346,12 +346,30 @@ public GrpcStorageOptions.Builder toBuilder() { @Override public int hashCode() { - return baseHashCode(); + return Objects.hash( + retryAlgorithmManager, + terminationAwaitDuration, + attemptDirectPath, + grpcInterceptorProvider, + blobWriteSessionConfig, + baseHashCode()); } @Override - public boolean equals(Object obj) { - return obj instanceof GrpcStorageOptions && baseEquals((GrpcStorageOptions) obj); + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GrpcStorageOptions)) { + return false; + } + GrpcStorageOptions that = (GrpcStorageOptions) o; + return attemptDirectPath == that.attemptDirectPath + && Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager) + && Objects.equals(terminationAwaitDuration, that.terminationAwaitDuration) + && Objects.equals(grpcInterceptorProvider, that.grpcInterceptorProvider) + && Objects.equals(blobWriteSessionConfig, that.blobWriteSessionConfig) + && this.baseEquals(that); } /** @since 2.14.0 This new api is in preview and is subject to breaking changes. */ @@ -399,6 +417,12 @@ public static final class Builder extends StorageOptions.Builder { Builder(StorageOptions options) { super(options); + GrpcStorageOptions gso = (GrpcStorageOptions) options; + this.storageRetryStrategy = gso.getRetryAlgorithmManager().retryStrategy; + this.terminationAwaitDuration = gso.getTerminationAwaitDuration(); + this.attemptDirectPath = gso.attemptDirectPath; + this.grpcInterceptorProvider = gso.grpcInterceptorProvider; + this.blobWriteSessionConfig = gso.blobWriteSessionConfig; } /** diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java index c5163ad00..b2315cd82 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java @@ -35,7 +35,7 @@ final class HttpRetryAlgorithmManager implements Serializable { private static final long serialVersionUID = -3301856948991518651L; - private final StorageRetryStrategy retryStrategy; + final StorageRetryStrategy retryStrategy; HttpRetryAlgorithmManager(StorageRetryStrategy retryStrategy) { this.retryStrategy = retryStrategy; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java index 684f3f15b..77b8dfaa5 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java @@ -42,6 +42,7 @@ import java.io.ObjectInputStream; import java.io.Serializable; import java.time.Clock; +import java.util.Objects; import java.util.Set; import org.checkerframework.checker.nullness.qual.NonNull; @@ -93,12 +94,21 @@ public HttpStorageOptions.Builder toBuilder() { @Override public int hashCode() { - return baseHashCode(); + return Objects.hash(retryAlgorithmManager, blobWriteSessionConfig, baseHashCode()); } @Override - public boolean equals(Object obj) { - return obj instanceof HttpStorageOptions && baseEquals((HttpStorageOptions) obj); + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HttpStorageOptions)) { + return false; + } + HttpStorageOptions that = (HttpStorageOptions) o; + return Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager) + && Objects.equals(blobWriteSessionConfig, that.blobWriteSessionConfig) + && this.baseEquals(that); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { @@ -133,6 +143,9 @@ public static class Builder extends StorageOptions.Builder { Builder(StorageOptions options) { super(options); + HttpStorageOptions hso = (HttpStorageOptions) options; + this.storageRetryStrategy = hso.retryAlgorithmManager.retryStrategy; + this.blobWriteSessionConfig = hso.blobWriteSessionConfig; } @Override diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JournalingBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JournalingBlobWriteSessionConfig.java index 7978dd2b8..31784bfde 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JournalingBlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JournalingBlobWriteSessionConfig.java @@ -45,6 +45,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Objects; import java.util.stream.Collector; import javax.annotation.concurrent.Immutable; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -89,6 +90,25 @@ public final class JournalingBlobWriteSessionConfig extends BlobWriteSessionConf this.includeLoggingSink = includeLoggingSink; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof JournalingBlobWriteSessionConfig)) { + return false; + } + JournalingBlobWriteSessionConfig that = (JournalingBlobWriteSessionConfig) o; + return includeLoggingSink == that.includeLoggingSink + && Objects.equals(paths, that.paths) + && Objects.equals(absolutePaths, that.absolutePaths); + } + + @Override + public int hashCode() { + return Objects.hash(paths, includeLoggingSink, absolutePaths); + } + @VisibleForTesting @InternalApi JournalingBlobWriteSessionConfig withIncludeLoggingSink() { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java index 41f52c095..c37f3e1ab 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java @@ -46,6 +46,7 @@ import java.time.OffsetDateTime; import java.util.Base64; import java.util.Base64.Encoder; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; @@ -277,6 +278,35 @@ static ParallelCompositeUploadBlobWriteSessionConfig withDefaults() { PartMetadataFieldDecorator.noOp()); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParallelCompositeUploadBlobWriteSessionConfig)) { + return false; + } + ParallelCompositeUploadBlobWriteSessionConfig that = + (ParallelCompositeUploadBlobWriteSessionConfig) o; + return maxPartsPerCompose == that.maxPartsPerCompose + && Objects.equals(executorSupplier, that.executorSupplier) + && Objects.equals(bufferAllocationStrategy, that.bufferAllocationStrategy) + && Objects.equals(partNamingStrategy, that.partNamingStrategy) + && Objects.equals(partCleanupStrategy, that.partCleanupStrategy) + && Objects.equals(partMetadataFieldDecorator, that.partMetadataFieldDecorator); + } + + @Override + public int hashCode() { + return Objects.hash( + maxPartsPerCompose, + executorSupplier, + bufferAllocationStrategy, + partNamingStrategy, + partCleanupStrategy, + partMetadataFieldDecorator); + } + @InternalApi @Override WriterFactory createFactory(Clock clock) throws IOException { @@ -346,6 +376,23 @@ private SimpleBufferAllocationStrategy(int capacity) { BufferHandlePool get() { return BufferHandlePool.simple(capacity); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SimpleBufferAllocationStrategy)) { + return false; + } + SimpleBufferAllocationStrategy that = (SimpleBufferAllocationStrategy) o; + return capacity == that.capacity; + } + + @Override + public int hashCode() { + return Objects.hashCode(capacity); + } } private static class FixedPoolBufferAllocationStrategy extends BufferAllocationStrategy { @@ -363,6 +410,23 @@ private FixedPoolBufferAllocationStrategy(int bufferCount, int bufferCapacity) { BufferHandlePool get() { return BufferHandlePool.fixedPool(bufferCount, bufferCapacity); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FixedPoolBufferAllocationStrategy)) { + return false; + } + FixedPoolBufferAllocationStrategy that = (FixedPoolBufferAllocationStrategy) o; + return bufferCount == that.bufferCount && bufferCapacity == that.bufferCapacity; + } + + @Override + public int hashCode() { + return Objects.hash(bufferCount, bufferCapacity); + } } } @@ -441,6 +505,23 @@ Executor get() { return executor; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SuppliedExecutorSupplier)) { + return false; + } + SuppliedExecutorSupplier that = (SuppliedExecutorSupplier) o; + return Objects.equals(executor, that.executor); + } + + @Override + public int hashCode() { + return Objects.hashCode(executor); + } + private void writeObject(ObjectOutputStream out) throws IOException { throw new java.io.InvalidClassException(this.getClass().getName() + "; Not serializable"); } @@ -470,6 +551,23 @@ Executor get() { ThreadFactory threadFactory = newThreadFactory(); return Executors.newFixedThreadPool(poolSize, threadFactory); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FixedSupplier)) { + return false; + } + FixedSupplier that = (FixedSupplier) o; + return poolSize == that.poolSize; + } + + @Override + public int hashCode() { + return Objects.hashCode(poolSize); + } } } @@ -613,6 +711,23 @@ protected String fmtFields(String randomKey, String ultimateObjectName, String p + partRange + ".part"; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof WithPrefix)) { + return false; + } + WithPrefix that = (WithPrefix) o; + return Objects.equals(prefix, that.prefix); + } + + @Override + public int hashCode() { + return Objects.hashCode(prefix); + } } static final class WithObjectLevelPrefix extends PartNamingStrategy { @@ -642,6 +757,23 @@ protected String fmtFields(String randomKey, String ultimateObjectName, String p + partRange + ".part"; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof WithObjectLevelPrefix)) { + return false; + } + WithObjectLevelPrefix that = (WithObjectLevelPrefix) o; + return Objects.equals(prefix, that.prefix); + } + + @Override + public int hashCode() { + return Objects.hashCode(prefix); + } } static final class NoPrefix extends PartNamingStrategy { @@ -726,6 +858,23 @@ PartMetadataFieldDecoratorInstance newInstance(Clock clock) { return builder.setCustomTimeOffsetDateTime(futureTime); }; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomTimeInFuture)) { + return false; + } + CustomTimeInFuture that = (CustomTimeInFuture) o; + return Objects.equals(duration, that.duration); + } + + @Override + public int hashCode() { + return Objects.hashCode(duration); + } } private static final class NoOp extends PartMetadataFieldDecorator { @@ -771,6 +920,24 @@ boolean isDeleteAllOnError() { return deleteAllOnError; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PartCleanupStrategy)) { + return false; + } + PartCleanupStrategy that = (PartCleanupStrategy) o; + return deletePartsOnSuccess == that.deletePartsOnSuccess + && deleteAllOnError == that.deleteAllOnError; + } + + @Override + public int hashCode() { + return Objects.hash(deletePartsOnSuccess, deleteAllOnError); + } + /** * If an unrecoverable error is encountered, define whether to attempt to delete any objects * already uploaded. diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java new file mode 100644 index 000000000..4601a3b2e --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsBuilderTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.storage; + +import static com.google.cloud.storage.TestUtils.assertAll; +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.retrying.ResultRetryAlgorithm; +import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.BufferAllocationStrategy; +import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.ExecutorSupplier; +import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy; +import org.junit.Before; +import org.junit.Test; + +public final class StorageOptionsBuilderTest { + + private MyStorageRetryStrategy storageRetryStrategy; + private BlobWriteSessionConfig bwsc; + + @Before + public void setUp() throws Exception { + storageRetryStrategy = new MyStorageRetryStrategy(); + bwsc = + BlobWriteSessionConfigs.parallelCompositeUpload() + .withBufferAllocationStrategy(BufferAllocationStrategy.simple(256 * 1024)) + .withPartNamingStrategy(PartNamingStrategy.prefix("blahblahblah")) + .withExecutorSupplier(ExecutorSupplier.fixedPool(2)); + } + + @Test + public void http() throws Exception { + HttpStorageOptions base = + HttpStorageOptions.http() + .setStorageRetryStrategy(storageRetryStrategy) + .setBlobWriteSessionConfig(bwsc) + .build(); + + HttpStorageOptions rebuilt = base.toBuilder().build(); + assertAll( + () -> assertThat(rebuilt).isEqualTo(base), + () -> assertThat(rebuilt.hashCode()).isEqualTo(base.hashCode())); + } + + @Test + public void grpc() throws Exception { + GrpcStorageOptions base = + GrpcStorageOptions.grpc() + .setStorageRetryStrategy(storageRetryStrategy) + .setBlobWriteSessionConfig(bwsc) + .build(); + + GrpcStorageOptions rebuilt = base.toBuilder().build(); + assertAll( + () -> assertThat(rebuilt).isEqualTo(base), + () -> assertThat(rebuilt.hashCode()).isEqualTo(base.hashCode())); + } + + private static class MyStorageRetryStrategy implements StorageRetryStrategy { + + @Override + public ResultRetryAlgorithm getIdempotentHandler() { + return null; + } + + @Override + public ResultRetryAlgorithm getNonidempotentHandler() { + return null; + } + } +}