Skip to content

Commit 527cac4

Browse files
committed
fix: fix RetryInfo algorithm and tests
1 parent f1b7fc7 commit 527cac4

File tree

4 files changed

+122
-21
lines changed

4 files changed

+122
-21
lines changed

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

Lines changed: 20 additions & 2 deletions
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;
@@ -56,13 +57,30 @@ public Object getTransportCode() {
5657
public MutateRowsException(
5758
@Nullable Throwable rpcError,
5859
@Nonnull List<FailedMutation> failedMutations,
59-
boolean retryable) {
60-
super("Some mutations failed to apply", rpcError, LOCAL_STATUS, retryable);
60+
boolean retryable,
61+
@Nullable ErrorDetails errorDetails) {
62+
super(
63+
new Throwable("Some mutations failed to apply", rpcError),
64+
LOCAL_STATUS,
65+
retryable,
66+
errorDetails);
6167
Preconditions.checkNotNull(failedMutations);
6268
Preconditions.checkArgument(!failedMutations.isEmpty(), "failedMutations can't be empty");
6369
this.failedMutations = failedMutations;
6470
}
6571

72+
/**
73+
* This constructor is considered an internal implementation detail and not meant to be used by
74+
* applications.
75+
*/
76+
@InternalApi
77+
public MutateRowsException(
78+
@Nullable Throwable rpcError,
79+
@Nonnull List<FailedMutation> failedMutations,
80+
boolean retryable) {
81+
this(rpcError, failedMutations, retryable, null);
82+
}
83+
6684
/**
6785
* Retrieve all of the failed mutations. This list will contain failures for all of the mutations
6886
* that have failed across all of the retry attempts so far.

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.api.gax.rpc.ApiCallContext;
2424
import com.google.api.gax.rpc.ApiException;
2525
import com.google.api.gax.rpc.ApiExceptionFactory;
26+
import com.google.api.gax.rpc.ErrorDetails;
2627
import com.google.api.gax.rpc.StatusCode;
2728
import com.google.api.gax.rpc.UnaryCallable;
2829
import com.google.bigtable.v2.MutateRowsRequest;
@@ -250,7 +251,12 @@ private void handleAttemptError(Throwable rpcError) {
250251
currentRequest = builder.build();
251252
originalIndexes = newOriginalIndexes;
252253

253-
throw new MutateRowsException(rpcError, allFailures.build(), entryError.isRetryable());
254+
ErrorDetails errorDetails = null;
255+
if (rpcError instanceof ApiException) {
256+
errorDetails = ((ApiException) rpcError).getErrorDetails();
257+
}
258+
throw new MutateRowsException(
259+
rpcError, allFailures.build(), entryError.isRetryable(), errorDetails);
254260
}
255261

256262
/**
@@ -268,6 +274,8 @@ private void handleAttemptSuccess(List<MutateRowsResponse> responses) {
268274
List<Integer> newOriginalIndexes = Lists.newArrayList();
269275
boolean[] seenIndices = new boolean[currentRequest.getEntriesCount()];
270276

277+
ErrorDetails errorDetails = null;
278+
271279
for (MutateRowsResponse response : responses) {
272280
for (Entry entry : response.getEntriesList()) {
273281
seenIndices[Ints.checkedCast(entry.getIndex())] = true;
@@ -283,13 +291,15 @@ private void handleAttemptSuccess(List<MutateRowsResponse> responses) {
283291

284292
allFailures.add(failedMutation);
285293

286-
if (!failedMutation.getError().isRetryable()) {
294+
if (!ApiExceptions.isRetryable2(failedMutation.getError())
295+
&& !failedMutation.getError().isRetryable()) {
287296
permanentFailures.add(failedMutation);
288297
} else {
289298
// Schedule the mutation entry for the next RPC by adding it to the request builder and
290299
// recording it's original index
291300
newOriginalIndexes.add(origIndex);
292301
builder.addEntries(lastRequest.getEntries((int) entry.getIndex()));
302+
errorDetails = failedMutation.getError().getErrorDetails();
293303
}
294304
}
295305
}
@@ -319,7 +329,7 @@ private void handleAttemptSuccess(List<MutateRowsResponse> responses) {
319329

320330
if (!allFailures.isEmpty()) {
321331
boolean isRetryable = builder.getEntriesCount() > 0;
322-
throw new MutateRowsException(null, allFailures, isRetryable);
332+
throw new MutateRowsException(null, allFailures, isRetryable, errorDetails);
323333
}
324334
}
325335

@@ -338,10 +348,14 @@ private ApiException createEntryError(com.google.rpc.Status protoStatus) {
338348

339349
StatusCode gaxStatusCode = GrpcStatusCode.of(grpcStatus.getCode());
340350

351+
ErrorDetails errorDetails =
352+
ErrorDetails.builder().setRawErrorMessages(protoStatus.getDetailsList()).build();
353+
341354
return ApiExceptionFactory.createException(
342355
grpcStatus.asRuntimeException(),
343356
gaxStatusCode,
344-
retryableCodes.contains(gaxStatusCode.getCode()));
357+
retryableCodes.contains(gaxStatusCode.getCode()),
358+
errorDetails);
345359
}
346360

347361
/**
@@ -354,10 +368,10 @@ private static ApiException createSyntheticErrorForRpcFailure(Throwable overallR
354368
ApiException requestApiException = (ApiException) overallRequestError;
355369

356370
return ApiExceptionFactory.createException(
357-
"Didn't receive a result for this mutation entry",
358371
overallRequestError,
359372
requestApiException.getStatusCode(),
360-
requestApiException.isRetryable());
373+
requestApiException.isRetryable(),
374+
requestApiException.getErrorDetails());
361375
}
362376

363377
return ApiExceptionFactory.createException(

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.google.protobuf.util.Durations;
2525
import com.google.rpc.RetryInfo;
2626
import io.grpc.Metadata;
27-
import io.grpc.Status;
2827
import io.grpc.protobuf.ProtoUtils;
2928
import org.checkerframework.checker.nullness.qual.Nullable;
3029
import org.threeten.bp.Duration;
@@ -93,17 +92,17 @@ static Duration extractRetryDelay(@Nullable Throwable throwable) {
9392
if (throwable == null) {
9493
return null;
9594
}
96-
Metadata trailers = Status.trailersFromThrowable(throwable);
97-
if (trailers == null) {
95+
if (!(throwable instanceof ApiException)) {
9896
return null;
9997
}
100-
RetryInfo retryInfo = trailers.get(RETRY_INFO_KEY);
101-
if (retryInfo == null) {
98+
ApiException exception = (ApiException) throwable;
99+
if (exception.getErrorDetails() == null) {
102100
return null;
103101
}
104-
if (!retryInfo.hasRetryDelay()) {
102+
if (exception.getErrorDetails().getRetryInfo() == null) {
105103
return null;
106104
}
105+
RetryInfo retryInfo = exception.getErrorDetails().getRetryInfo();
107106
return Duration.ofMillis(Durations.toMillis(retryInfo.getRetryDelay()));
108107
}
109108
}

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

Lines changed: 77 additions & 7 deletions
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,6 +79,9 @@ 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;
@@ -167,6 +172,42 @@ public void testMutateRowsNonRetryableErrorWithRetryInfo() {
167172
false);
168173
}
169174

175+
@Test
176+
public void testMutateRowsPartialFailure() {
177+
service.partial = true;
178+
179+
verifyRetryInfoIsUsed(
180+
() ->
181+
client.bulkMutateRows(
182+
BulkMutation.create("fake-table")
183+
.add(RowMutationEntry.create("row-key-1").setCell("cf", "q", "v"))),
184+
true);
185+
}
186+
187+
@Test
188+
public void testMutateRowsPartialFailureNonRetryableError() {
189+
service.partial = true;
190+
191+
verifyRetryInfoIsUsed(
192+
() ->
193+
client.bulkMutateRows(
194+
BulkMutation.create("fake-table")
195+
.add(RowMutationEntry.create("row-key-1").setCell("cf", "q", "v"))),
196+
false);
197+
}
198+
199+
// TODO: add this test back
200+
// @Test
201+
public void testMutateRowsPartialFailureCanBeDisabled() {
202+
service.partial = true;
203+
204+
verifyRetryInfoCanBeDisabled(
205+
() ->
206+
client.bulkMutateRows(
207+
BulkMutation.create("fake-table")
208+
.add(RowMutationEntry.create("row-key-1").setCell("cf", "q", "v"))));
209+
}
210+
170211
@Test
171212
public void testMutateRowsDisableRetryInfo() throws IOException {
172213
settings.stubSettings().setEnableRetryInfo(false);
@@ -366,27 +407,37 @@ private void verifyRetryInfoCanBeDisabled(Runnable runnable) {
366407
private void enqueueRetryableExceptionWithDelay(com.google.protobuf.Duration delay) {
367408
Metadata trailers = new Metadata();
368409
RetryInfo retryInfo = RetryInfo.newBuilder().setRetryDelay(delay).build();
369-
trailers.put(RETRY_INFO_KEY, retryInfo);
410+
ErrorDetails errorDetails =
411+
ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(retryInfo))).build();
412+
byte[] status =
413+
com.google.rpc.Status.newBuilder().addDetails(Any.pack(retryInfo)).build().toByteArray();
414+
trailers.put(ERROR_DETAILS_KEY, status);
370415

371416
ApiException exception =
372417
new UnavailableException(
373418
new StatusRuntimeException(Status.UNAVAILABLE, trailers),
374419
GrpcStatusCode.of(Status.Code.UNAVAILABLE),
375-
true);
420+
true,
421+
errorDetails);
376422

377423
service.expectations.add(exception);
378424
}
379425

380426
private ApiException enqueueNonRetryableExceptionWithDelay(com.google.protobuf.Duration delay) {
381427
Metadata trailers = new Metadata();
382428
RetryInfo retryInfo = RetryInfo.newBuilder().setRetryDelay(delay).build();
383-
trailers.put(RETRY_INFO_KEY, retryInfo);
429+
ErrorDetails errorDetails =
430+
ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(retryInfo))).build();
431+
byte[] status =
432+
com.google.rpc.Status.newBuilder().addDetails(Any.pack(retryInfo)).build().toByteArray();
433+
trailers.put(ERROR_DETAILS_KEY, status);
384434

385435
ApiException exception =
386436
new InternalException(
387437
new StatusRuntimeException(Status.INTERNAL, trailers),
388438
GrpcStatusCode.of(Status.Code.INTERNAL),
389-
false);
439+
false,
440+
errorDetails);
390441

391442
service.expectations.add(exception);
392443

@@ -395,6 +446,7 @@ private ApiException enqueueNonRetryableExceptionWithDelay(com.google.protobuf.D
395446

396447
private class FakeBigtableService extends BigtableGrpc.BigtableImplBase {
397448
Queue<Exception> expectations = Queues.newArrayDeque();
449+
boolean partial = false;
398450

399451
@Override
400452
public void readRows(
@@ -434,8 +486,26 @@ public void mutateRows(
434486
responseObserver.onNext(builder.build());
435487
responseObserver.onCompleted();
436488
} else {
437-
Exception expectedRpc = expectations.poll();
438-
responseObserver.onError(expectedRpc);
489+
if (partial) {
490+
ApiException expectedRpc = (ApiException) expectations.poll();
491+
MutateRowsResponse.Builder builder = MutateRowsResponse.newBuilder();
492+
builder.addEntries(
493+
0,
494+
MutateRowsResponse.Entry.newBuilder()
495+
.setStatus(
496+
com.google.rpc.Status.newBuilder()
497+
.setCode(expectedRpc.getStatusCode().getCode().getHttpStatusCode())
498+
.addDetails(Any.pack(expectedRpc.getErrorDetails().getRetryInfo())))
499+
.build());
500+
for (int i = 1; i < request.getEntriesCount(); i++) {
501+
builder.addEntriesBuilder().setIndex(i);
502+
}
503+
responseObserver.onNext(builder.build());
504+
responseObserver.onCompleted();
505+
} else {
506+
Exception expectedRpc = expectations.poll();
507+
responseObserver.onError(expectedRpc);
508+
}
439509
}
440510
}
441511

0 commit comments

Comments
 (0)