Skip to content

Commit dad7517

Browse files
authored
fix: fix RetryInfo algorithm and tests (#2041)
Gax already parses ErrorDetails from an error response and add the error details to ApiException. Fix the RetryInfoAlgorithm to handle this correctly and the test to send error responses with the correct format. Also fixed MutateRowsAttemptCallable to not use RetryInfoAlgorithm with the setting is disabled. Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) - [ ] Rollback plan is reviewed and LGTMed Fixes #<issue_number_goes_here> ☕️ If you write sample code, please follow the [samples format]( https://togithub.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
1 parent 2907038 commit dad7517

File tree

10 files changed

+119
-75
lines changed

10 files changed

+119
-75
lines changed

google-cloud-bigtable/clirr-ignored-differences.xml

+18
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,22 @@
156156
<className>com/google/cloud/bigtable/gaxx/retrying/ApiResultRetryAlgorithm</className>
157157
<field>*</field>
158158
</difference>
159+
<!-- InternalApi was updated -->
160+
<difference>
161+
<differenceType>7004</differenceType>
162+
<className>com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsRetryingCallable</className>
163+
<method>*</method>
164+
</difference>
165+
<!-- InternalApi was updated -->
166+
<difference>
167+
<differenceType>7004</differenceType>
168+
<className>com/google/cloud/bigtable/data/v2/models/MutateRowsException</className>
169+
<method>*</method>
170+
</difference>
171+
<!-- InternalApi was updated -->
172+
<difference>
173+
<differenceType>7009</differenceType>
174+
<className>com/google/cloud/bigtable/data/v2/models/MutateRowsException</className>
175+
<method>*</method>
176+
</difference>
159177
</differences>

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/MutateRowsException.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.api.core.InternalApi;
1919
import com.google.api.gax.rpc.ApiException;
20+
import com.google.api.gax.rpc.ErrorDetails;
2021
import com.google.api.gax.rpc.StatusCode;
2122
import com.google.auto.value.AutoValue;
2223
import com.google.bigtable.v2.MutateRowsRequest;
@@ -53,16 +54,36 @@ public Object getTransportCode() {
5354
* applications.
5455
*/
5556
@InternalApi
56-
public MutateRowsException(
57+
public static MutateRowsException create(
5758
@Nullable Throwable rpcError,
5859
@Nonnull List<FailedMutation> failedMutations,
5960
boolean retryable) {
60-
super("Some mutations failed to apply", rpcError, LOCAL_STATUS, retryable);
61+
ErrorDetails errorDetails = null;
62+
if (rpcError instanceof ApiException) {
63+
errorDetails = ((ApiException) rpcError).getErrorDetails();
64+
}
65+
66+
return new MutateRowsException(rpcError, failedMutations, retryable, errorDetails);
67+
}
68+
69+
private MutateRowsException(
70+
@Nullable Throwable rpcError,
71+
@Nonnull List<FailedMutation> failedMutations,
72+
boolean retryable,
73+
@Nullable ErrorDetails errorDetails) {
74+
super(rpcError, LOCAL_STATUS, retryable, errorDetails);
6175
Preconditions.checkNotNull(failedMutations);
6276
Preconditions.checkArgument(!failedMutations.isEmpty(), "failedMutations can't be empty");
6377
this.failedMutations = failedMutations;
6478
}
6579

80+
// TODO: remove this after we add a ctor in gax to pass in a Throwable, a message and error
81+
// details.
82+
@Override
83+
public String getMessage() {
84+
return "Some mutations failed to apply";
85+
}
86+
6687
/**
6788
* Retrieve all of the failed mutations. This list will contain failures for all of the mutations
6889
* that have failed across all of the retry attempts so far.

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,8 @@ public Map<String, String> extract(MutateRowsRequest mutateRowsRequest) {
784784
clientContext.getDefaultCallContext(),
785785
withBigtableTracer,
786786
retryingExecutor,
787-
settings.bulkMutateRowsSettings().getRetryableCodes());
787+
settings.bulkMutateRowsSettings().getRetryableCodes(),
788+
retryAlgorithm);
788789
}
789790

790791
/**

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallable.java

+16-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import com.google.api.core.ApiFuture;
2020
import com.google.api.core.ApiFutures;
2121
import com.google.api.gax.grpc.GrpcStatusCode;
22+
import com.google.api.gax.retrying.RetryAlgorithm;
2223
import com.google.api.gax.retrying.RetryingFuture;
24+
import com.google.api.gax.retrying.TimedAttemptSettings;
2325
import com.google.api.gax.rpc.ApiCallContext;
2426
import com.google.api.gax.rpc.ApiException;
2527
import com.google.api.gax.rpc.ApiExceptionFactory;
@@ -31,7 +33,6 @@
3133
import com.google.bigtable.v2.MutateRowsResponse.Entry;
3234
import com.google.cloud.bigtable.data.v2.models.MutateRowsException;
3335
import com.google.cloud.bigtable.data.v2.models.MutateRowsException.FailedMutation;
34-
import com.google.cloud.bigtable.gaxx.retrying.ApiExceptions;
3536
import com.google.cloud.bigtable.gaxx.retrying.NonCancellableFuture;
3637
import com.google.common.base.Preconditions;
3738
import com.google.common.collect.ImmutableList;
@@ -111,6 +112,8 @@ public Object getTransportCode() {
111112
@Nullable private List<Integer> originalIndexes;
112113
@Nonnull private final Set<StatusCode.Code> retryableCodes;
113114
@Nullable private final List<FailedMutation> permanentFailures;
115+
@Nonnull private final RetryAlgorithm<MutateRowsRequest> retryAlgorithm;
116+
@Nonnull private TimedAttemptSettings attemptSettings;
114117

115118
// Parent controller
116119
private RetryingFuture<Void> externalFuture;
@@ -138,11 +141,14 @@ public List<MutateRowsResponse> apply(Throwable throwable) {
138141
@Nonnull UnaryCallable<MutateRowsRequest, List<MutateRowsResponse>> innerCallable,
139142
@Nonnull MutateRowsRequest originalRequest,
140143
@Nonnull ApiCallContext callContext,
141-
@Nonnull Set<StatusCode.Code> retryableCodes) {
144+
@Nonnull Set<StatusCode.Code> retryableCodes,
145+
@Nonnull RetryAlgorithm<MutateRowsRequest> retryAlgorithm) {
142146
this.innerCallable = Preconditions.checkNotNull(innerCallable, "innerCallable");
143147
this.currentRequest = Preconditions.checkNotNull(originalRequest, "currentRequest");
144148
this.callContext = Preconditions.checkNotNull(callContext, "callContext");
145149
this.retryableCodes = Preconditions.checkNotNull(retryableCodes, "retryableCodes");
150+
this.retryAlgorithm = retryAlgorithm;
151+
this.attemptSettings = retryAlgorithm.createFirstAttempt();
146152

147153
permanentFailures = Lists.newArrayList();
148154
}
@@ -230,14 +236,15 @@ private void handleAttemptError(Throwable rpcError) {
230236
Builder builder = lastRequest.toBuilder().clearEntries();
231237
List<Integer> newOriginalIndexes = Lists.newArrayList();
232238

239+
attemptSettings = retryAlgorithm.createNextAttempt(null, entryError, null, attemptSettings);
240+
233241
for (int i = 0; i < currentRequest.getEntriesCount(); i++) {
234242
int origIndex = getOriginalIndex(i);
235243

236244
FailedMutation failedMutation = FailedMutation.create(origIndex, entryError);
237245
allFailures.add(failedMutation);
238246

239-
if (!ApiExceptions.isRetryable2(failedMutation.getError())
240-
&& !failedMutation.getError().isRetryable()) {
247+
if (!retryAlgorithm.shouldRetry(null, failedMutation.getError(), null, attemptSettings)) {
241248
permanentFailures.add(failedMutation);
242249
} else {
243250
// Schedule the mutation entry for the next RPC by adding it to the request builder and
@@ -250,15 +257,15 @@ private void handleAttemptError(Throwable rpcError) {
250257
currentRequest = builder.build();
251258
originalIndexes = newOriginalIndexes;
252259

253-
throw new MutateRowsException(rpcError, allFailures.build(), entryError.isRetryable());
260+
throw MutateRowsException.create(rpcError, allFailures.build(), builder.getEntriesCount() > 0);
254261
}
255262

256263
/**
257264
* Handle entry level failures. All new response entries are inspected for failure. If any
258265
* transient failures are found, their corresponding mutations are scheduled for the next RPC. The
259266
* caller is notified of both new found errors and pre-existing permanent errors in the thrown
260267
* {@link MutateRowsException}. If no errors exist, then the attempt future is successfully
261-
* completed.
268+
* completed. We don't currently handle RetryInfo on entry level failures.
262269
*/
263270
private void handleAttemptSuccess(List<MutateRowsResponse> responses) {
264271
List<FailedMutation> allFailures = Lists.newArrayList(permanentFailures);
@@ -319,7 +326,7 @@ private void handleAttemptSuccess(List<MutateRowsResponse> responses) {
319326

320327
if (!allFailures.isEmpty()) {
321328
boolean isRetryable = builder.getEntriesCount() > 0;
322-
throw new MutateRowsException(null, allFailures, isRetryable);
329+
throw MutateRowsException.create(null, allFailures, isRetryable);
323330
}
324331
}
325332

@@ -354,10 +361,10 @@ private static ApiException createSyntheticErrorForRpcFailure(Throwable overallR
354361
ApiException requestApiException = (ApiException) overallRequestError;
355362

356363
return ApiExceptionFactory.createException(
357-
"Didn't receive a result for this mutation entry",
358364
overallRequestError,
359365
requestApiException.getStatusCode(),
360-
requestApiException.isRetryable());
366+
requestApiException.isRetryable(),
367+
requestApiException.getErrorDetails());
361368
}
362369

363370
return ApiExceptionFactory.createException(

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsRetryingCallable.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.google.cloud.bigtable.data.v2.stub.mutaterows;
1717

1818
import com.google.api.core.InternalApi;
19+
import com.google.api.gax.retrying.RetryAlgorithm;
1920
import com.google.api.gax.retrying.RetryingExecutorWithContext;
2021
import com.google.api.gax.retrying.RetryingFuture;
2122
import com.google.api.gax.rpc.ApiCallContext;
@@ -44,23 +45,26 @@ public class MutateRowsRetryingCallable extends UnaryCallable<MutateRowsRequest,
4445
private final ServerStreamingCallable<MutateRowsRequest, MutateRowsResponse> callable;
4546
private final RetryingExecutorWithContext<Void> executor;
4647
private final ImmutableSet<Code> retryCodes;
48+
private final RetryAlgorithm retryAlgorithm;
4749

4850
public MutateRowsRetryingCallable(
4951
@Nonnull ApiCallContext callContextPrototype,
5052
@Nonnull ServerStreamingCallable<MutateRowsRequest, MutateRowsResponse> callable,
5153
@Nonnull RetryingExecutorWithContext<Void> executor,
52-
@Nonnull Set<StatusCode.Code> retryCodes) {
54+
@Nonnull Set<StatusCode.Code> retryCodes,
55+
@Nonnull RetryAlgorithm retryAlgorithm) {
5356
this.callContextPrototype = Preconditions.checkNotNull(callContextPrototype);
5457
this.callable = Preconditions.checkNotNull(callable);
5558
this.executor = Preconditions.checkNotNull(executor);
5659
this.retryCodes = ImmutableSet.copyOf(retryCodes);
60+
this.retryAlgorithm = retryAlgorithm;
5761
}
5862

5963
@Override
6064
public RetryingFuture<Void> futureCall(MutateRowsRequest request, ApiCallContext inputContext) {
6165
ApiCallContext context = callContextPrototype.nullToSelf(inputContext);
6266
MutateRowsAttemptCallable retryCallable =
63-
new MutateRowsAttemptCallable(callable.all(), request, context, retryCodes);
67+
new MutateRowsAttemptCallable(callable.all(), request, context, retryCodes, retryAlgorithm);
6468

6569
RetryingFuture<Void> retryingFuture = executor.createFuture(retryCallable, context);
6670
retryCallable.setExternalFuture(retryingFuture);

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/ApiExceptions.java

-34
This file was deleted.

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/retrying/RetryInfoRetryAlgorithm.java

+6-13
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@
2020
import com.google.api.gax.retrying.RetryingContext;
2121
import com.google.api.gax.retrying.TimedAttemptSettings;
2222
import com.google.api.gax.rpc.ApiException;
23-
import com.google.common.annotations.VisibleForTesting;
2423
import com.google.protobuf.util.Durations;
2524
import com.google.rpc.RetryInfo;
26-
import io.grpc.Metadata;
27-
import io.grpc.Status;
28-
import io.grpc.protobuf.ProtoUtils;
2925
import org.checkerframework.checker.nullness.qual.Nullable;
3026
import org.threeten.bp.Duration;
3127

@@ -37,10 +33,6 @@
3733
@InternalApi
3834
public class RetryInfoRetryAlgorithm<ResponseT> extends BasicResultRetryAlgorithm<ResponseT> {
3935

40-
@VisibleForTesting
41-
public static final Metadata.Key<RetryInfo> RETRY_INFO_KEY =
42-
ProtoUtils.keyForProto(RetryInfo.getDefaultInstance());
43-
4436
@Override
4537
public TimedAttemptSettings createNextAttempt(
4638
Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings prevSettings) {
@@ -50,6 +42,7 @@ public TimedAttemptSettings createNextAttempt(
5042
.toBuilder()
5143
.setRandomizedRetryDelay(retryDelay)
5244
.setAttemptCount(prevSettings.getAttemptCount() + 1)
45+
.setOverallAttemptCount(prevSettings.getAttemptCount() + 1)
5346
.build();
5447
}
5548
return null;
@@ -93,17 +86,17 @@ static Duration extractRetryDelay(@Nullable Throwable throwable) {
9386
if (throwable == null) {
9487
return null;
9588
}
96-
Metadata trailers = Status.trailersFromThrowable(throwable);
97-
if (trailers == null) {
89+
if (!(throwable instanceof ApiException)) {
9890
return null;
9991
}
100-
RetryInfo retryInfo = trailers.get(RETRY_INFO_KEY);
101-
if (retryInfo == null) {
92+
ApiException exception = (ApiException) throwable;
93+
if (exception.getErrorDetails() == null) {
10294
return null;
10395
}
104-
if (!retryInfo.hasRetryDelay()) {
96+
if (exception.getErrorDetails().getRetryInfo() == null) {
10597
return null;
10698
}
99+
RetryInfo retryInfo = exception.getErrorDetails().getRetryInfo();
107100
return Duration.ofMillis(Durations.toMillis(retryInfo.getRetryDelay()));
108101
}
109102
}

google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RetryInfoTest.java

+21-6
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
*/
1616
package com.google.cloud.bigtable.data.v2.stub;
1717

18-
import static com.google.cloud.bigtable.gaxx.retrying.RetryInfoRetryAlgorithm.RETRY_INFO_KEY;
1918
import static com.google.common.truth.Truth.assertThat;
2019

2120
import com.google.api.gax.core.NoCredentialsProvider;
2221
import com.google.api.gax.grpc.GrpcStatusCode;
2322
import com.google.api.gax.grpc.GrpcTransportChannel;
2423
import com.google.api.gax.rpc.ApiException;
24+
import com.google.api.gax.rpc.ErrorDetails;
2525
import com.google.api.gax.rpc.FixedTransportChannelProvider;
2626
import com.google.api.gax.rpc.InternalException;
2727
import com.google.api.gax.rpc.UnavailableException;
@@ -55,7 +55,9 @@
5555
import com.google.cloud.bigtable.data.v2.models.RowMutation;
5656
import com.google.cloud.bigtable.data.v2.models.RowMutationEntry;
5757
import com.google.common.base.Stopwatch;
58+
import com.google.common.collect.ImmutableList;
5859
import com.google.common.collect.Queues;
60+
import com.google.protobuf.Any;
5961
import com.google.rpc.RetryInfo;
6062
import io.grpc.Metadata;
6163
import io.grpc.Status;
@@ -77,13 +79,16 @@ public class RetryInfoTest {
7779

7880
@Rule public GrpcServerRule serverRule = new GrpcServerRule();
7981

82+
private static final Metadata.Key<byte[]> ERROR_DETAILS_KEY =
83+
Metadata.Key.of("grpc-status-details-bin", Metadata.BINARY_BYTE_MARSHALLER);
84+
8085
private FakeBigtableService service;
8186
private BigtableDataClient client;
8287
private BigtableDataSettings.Builder settings;
8388

8489
private AtomicInteger attemptCounter = new AtomicInteger();
8590
private com.google.protobuf.Duration delay =
86-
com.google.protobuf.Duration.newBuilder().setSeconds(1).setNanos(0).build();
91+
com.google.protobuf.Duration.newBuilder().setSeconds(2).setNanos(0).build();
8792

8893
@Before
8994
public void setUp() throws IOException {
@@ -366,27 +371,37 @@ private void verifyRetryInfoCanBeDisabled(Runnable runnable) {
366371
private void enqueueRetryableExceptionWithDelay(com.google.protobuf.Duration delay) {
367372
Metadata trailers = new Metadata();
368373
RetryInfo retryInfo = RetryInfo.newBuilder().setRetryDelay(delay).build();
369-
trailers.put(RETRY_INFO_KEY, retryInfo);
374+
ErrorDetails errorDetails =
375+
ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(retryInfo))).build();
376+
byte[] status =
377+
com.google.rpc.Status.newBuilder().addDetails(Any.pack(retryInfo)).build().toByteArray();
378+
trailers.put(ERROR_DETAILS_KEY, status);
370379

371380
ApiException exception =
372381
new UnavailableException(
373382
new StatusRuntimeException(Status.UNAVAILABLE, trailers),
374383
GrpcStatusCode.of(Status.Code.UNAVAILABLE),
375-
true);
384+
true,
385+
errorDetails);
376386

377387
service.expectations.add(exception);
378388
}
379389

380390
private ApiException enqueueNonRetryableExceptionWithDelay(com.google.protobuf.Duration delay) {
381391
Metadata trailers = new Metadata();
382392
RetryInfo retryInfo = RetryInfo.newBuilder().setRetryDelay(delay).build();
383-
trailers.put(RETRY_INFO_KEY, retryInfo);
393+
ErrorDetails errorDetails =
394+
ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(retryInfo))).build();
395+
byte[] status =
396+
com.google.rpc.Status.newBuilder().addDetails(Any.pack(retryInfo)).build().toByteArray();
397+
trailers.put(ERROR_DETAILS_KEY, status);
384398

385399
ApiException exception =
386400
new InternalException(
387401
new StatusRuntimeException(Status.INTERNAL, trailers),
388402
GrpcStatusCode.of(Status.Code.INTERNAL),
389-
false);
403+
false,
404+
errorDetails);
390405

391406
service.expectations.add(exception);
392407

0 commit comments

Comments
 (0)