Repository navigation
feat(bigtable): plumb per-operation timeouts through CbtTestProxy #14997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,10 @@ | |
| import com.google.api.gax.core.CredentialsProvider; | ||
| import com.google.api.gax.core.FixedCredentialsProvider; | ||
| import com.google.api.gax.core.NoCredentialsProvider; | ||
| import com.google.api.gax.grpc.GrpcCallContext; | ||
| import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; | ||
| import com.google.api.gax.retrying.RetrySettings; | ||
| import com.google.api.gax.rpc.ApiCallContext; | ||
| import com.google.api.gax.rpc.ApiException; | ||
| import com.google.api.gax.rpc.ServerStream; | ||
| import com.google.auth.oauth2.AccessToken; | ||
|
|
@@ -43,6 +45,7 @@ | |
| import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; | ||
| import com.google.cloud.bigtable.data.v2.models.RowCell; | ||
| import com.google.cloud.bigtable.data.v2.models.RowMutation; | ||
| import com.google.cloud.bigtable.data.v2.models.TableId; | ||
| import com.google.cloud.bigtable.data.v2.models.sql.PreparedStatement; | ||
| import com.google.cloud.bigtable.data.v2.models.sql.ResultSet; | ||
| import com.google.cloud.bigtable.data.v2.models.sql.SqlType; | ||
|
|
@@ -53,6 +56,7 @@ | |
| import com.google.protobuf.ByteString; | ||
| import com.google.protobuf.util.Durations; | ||
| import com.google.rpc.Code; | ||
| import io.grpc.Context; | ||
| import io.grpc.ManagedChannelBuilder; | ||
| import io.grpc.Status; | ||
| import io.grpc.StatusException; | ||
|
|
@@ -73,6 +77,7 @@ | |
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.logging.Logger; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
@@ -138,16 +143,37 @@ private static BigtableDataSettings.Builder overrideTimeoutSetting( | |
|
|
||
| private static void updateTimeout(RetrySettings.Builder settings, Duration newTimeout) { | ||
| Duration rpcTimeout = settings.getInitialRpcTimeoutDuration(); | ||
| Duration maxRpcTimeout = settings.getMaxRpcTimeoutDuration(); | ||
| Duration oldTotalTimeout = settings.getTotalTimeoutDuration(); | ||
|
|
||
| // TODO: this should happen in gax | ||
| // Clamp the rpcTimeout to the overall timeout | ||
| if (rpcTimeout != null && rpcTimeout.compareTo(newTimeout) > 0) { | ||
| settings.setInitialRpcTimeoutDuration(newTimeout).setMaxRpcTimeoutDuration(newTimeout); | ||
| // Clamp the rpcTimeout to the overall timeout, or raise it if it was previously clamped to the | ||
| // old total timeout. | ||
| if (rpcTimeout != null | ||
| && (rpcTimeout.compareTo(newTimeout) > 0 || rpcTimeout.equals(oldTotalTimeout))) { | ||
| settings.setInitialRpcTimeoutDuration(newTimeout); | ||
| } | ||
| if (maxRpcTimeout != null | ||
| && (maxRpcTimeout.compareTo(newTimeout) > 0 || maxRpcTimeout.equals(oldTotalTimeout))) { | ||
| settings.setMaxRpcTimeoutDuration(newTimeout); | ||
| } | ||
|
|
||
| settings.setTotalTimeoutDuration(newTimeout); | ||
| } | ||
|
|
||
| private static boolean hasPositiveTimeout( | ||
| boolean hasTimeout, com.google.protobuf.Duration timeout) { | ||
| return hasTimeout && (timeout.getSeconds() > 0 || timeout.getNanos() > 0); | ||
| } | ||
|
|
||
| private static ApiCallContext createCallContextWithTimeout( | ||
| RetrySettings baseRetrySettings, com.google.protobuf.Duration protoTimeout) { | ||
| Duration timeout = Duration.ofSeconds(protoTimeout.getSeconds(), protoTimeout.getNanos()); | ||
| RetrySettings.Builder builder = baseRetrySettings.toBuilder(); | ||
| updateTimeout(builder, timeout); | ||
| return GrpcCallContext.createDefault().withRetrySettings(builder.build()); | ||
| } | ||
|
|
||
| /** Helper method to get a client object by its id. */ | ||
| private CbtClient getClient(String id) throws StatusException { | ||
| CbtClient client = idClientMap.get(id); | ||
|
|
@@ -288,7 +314,16 @@ public void mutateRow( | |
| RowMutation mutation = RowMutation.fromProto(request.getRequest()); | ||
| try { | ||
| // This response is empty. | ||
| client.dataClient().mutateRow(mutation); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client.settings().getStubSettings().mutateRowSettings().getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| client.dataClient().mutateRowCallable().call(mutation, callContext); | ||
| } else { | ||
| client.dataClient().mutateRow(mutation); | ||
| } | ||
| } catch (ApiException e) { | ||
| responseObserver.onNext(MutateRowResult.newBuilder().setStatus(convertStatus(e)).build()); | ||
| responseObserver.onCompleted(); | ||
|
|
@@ -318,7 +353,16 @@ public void bulkMutateRows( | |
|
|
||
| BulkMutation batch = BulkMutation.fromProto(request.getRequest()); | ||
| try { | ||
| client.dataClient().bulkMutateRows(batch); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client.settings().getStubSettings().bulkMutateRowsSettings().getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| client.dataClient().bulkMutationCallable().call(batch, callContext); | ||
| } else { | ||
| client.dataClient().bulkMutateRows(batch); | ||
| } | ||
| } catch (MutateRowsException e) { | ||
| MutateRowsResult.Builder resultBuilder = MutateRowsResult.newBuilder(); | ||
| com.google.rpc.Status topLevelStatus = | ||
|
|
@@ -385,10 +429,23 @@ public void readRow(ReadRowRequest request, StreamObserver<RowResult> responseOb | |
|
|
||
| com.google.cloud.bigtable.data.v2.models.Row row; | ||
| try { | ||
| row = | ||
| client | ||
| .dataClient() | ||
| .readRow(tableId, request.getRowKey(), FILTERS.fromProto(request.getFilter())); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| Query query = | ||
| Query.create(TableId.of(tableId)) | ||
| .rowKey(request.getRowKey()) | ||
| .filter(FILTERS.fromProto(request.getFilter())); | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client.settings().getStubSettings().readRowSettings().getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| row = client.dataClient().readRowCallable().call(query, callContext); | ||
| } else { | ||
| row = | ||
| client | ||
| .dataClient() | ||
| .readRow(tableId, request.getRowKey(), FILTERS.fromProto(request.getFilter())); | ||
| } | ||
| if (row != null) { | ||
| RowResult.Builder resultBuilder = convertRowResult(row); | ||
| responseObserver.onNext( | ||
|
|
@@ -438,7 +495,16 @@ public void readRows(ReadRowsRequest request, StreamObserver<RowsResult> respons | |
| ServerStream<com.google.cloud.bigtable.data.v2.models.Row> rows; | ||
| Query query = Query.fromProto(request.getRequest()); | ||
| try { | ||
| rows = client.dataClient().readRows(query); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client.settings().getStubSettings().readRowsSettings().getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| rows = client.dataClient().readRowsCallable().call(query, callContext); | ||
| } else { | ||
| rows = client.dataClient().readRows(query); | ||
| } | ||
| int cancelAfterRows = request.getCancelAfterRows(); | ||
| RowsResult.Builder resultBuilder = convertRowsResult(rows, cancelAfterRows); | ||
| responseObserver.onNext( | ||
|
|
@@ -560,7 +626,16 @@ public void sampleRowKeys( | |
|
|
||
| List<KeyOffset> keyOffsets; | ||
| try { | ||
| keyOffsets = client.dataClient().sampleRowKeys(tableId); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client.settings().getStubSettings().sampleRowKeysSettings().getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| keyOffsets = client.dataClient().sampleRowKeysCallable().call(tableId, callContext); | ||
| } else { | ||
| keyOffsets = client.dataClient().sampleRowKeys(tableId); | ||
| } | ||
| } catch (ApiException e) { | ||
| responseObserver.onNext(SampleRowKeysResult.newBuilder().setStatus(convertStatus(e)).build()); | ||
| responseObserver.onCompleted(); | ||
|
|
@@ -599,7 +674,20 @@ public void checkAndMutateRow( | |
| ConditionalRowMutation mutation = ConditionalRowMutation.fromProto(request.getRequest()); | ||
| Boolean matched; | ||
| try { | ||
| matched = client.dataClient().checkAndMutateRow(mutation); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client | ||
| .settings() | ||
| .getStubSettings() | ||
| .checkAndMutateRowSettings() | ||
| .getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| matched = client.dataClient().checkAndMutateRowCallable().call(mutation, callContext); | ||
| } else { | ||
| matched = client.dataClient().checkAndMutateRow(mutation); | ||
| } | ||
| } catch (ApiException e) { | ||
| responseObserver.onNext( | ||
| CheckAndMutateRowResult.newBuilder().setStatus(convertStatus(e)).build()); | ||
|
|
@@ -634,7 +722,20 @@ public void readModifyWriteRow( | |
| com.google.cloud.bigtable.data.v2.models.Row row; | ||
| ReadModifyWriteRow mutation = ReadModifyWriteRow.fromProto(request.getRequest()); | ||
| try { | ||
| row = client.dataClient().readModifyWriteRow(mutation); | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| ApiCallContext callContext = | ||
| createCallContextWithTimeout( | ||
| client | ||
| .settings() | ||
| .getStubSettings() | ||
| .readModifyWriteRowSettings() | ||
| .getRetrySettings(), | ||
| request.getPerOperationTimeout()); | ||
| row = client.dataClient().readModifyWriteRowCallable().call(mutation, callContext); | ||
| } else { | ||
| row = client.dataClient().readModifyWriteRow(mutation); | ||
| } | ||
| if (row != null) { | ||
| RowResult.Builder resultBuilder = convertRowResult(row); | ||
| responseObserver.onNext( | ||
|
|
@@ -684,7 +785,20 @@ public void executeQuery( | |
| return; | ||
| } | ||
| ResultSet resultSet = null; | ||
| Context.CancellableContext cancellableContext = null; | ||
| Context previousContext = null; | ||
| try { | ||
| if (hasPositiveTimeout( | ||
| request.hasPerOperationTimeout(), request.getPerOperationTimeout())) { | ||
| Duration timeout = | ||
| Duration.ofSeconds( | ||
| request.getPerOperationTimeout().getSeconds(), | ||
| request.getPerOperationTimeout().getNanos()); | ||
| cancellableContext = | ||
| Context.current() | ||
| .withDeadlineAfter(timeout.toNanos(), TimeUnit.NANOSECONDS, null); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, use MoreExecutores.directExecutor() instead of null? |
||
| previousContext = cancellableContext.attach(); | ||
| } | ||
| Map<String, SqlType<?>> paramTypes = new HashMap<>(); | ||
| for (Map.Entry<String, Value> entry : request.getRequest().getParamsMap().entrySet()) { | ||
| paramTypes.put(entry.getKey(), SqlType.fromProto(entry.getValue().getType())); | ||
|
|
@@ -729,6 +843,10 @@ public void executeQuery( | |
| if (resultSet != null) { | ||
| resultSet.close(); | ||
| } | ||
| if (cancellableContext != null) { | ||
| cancellableContext.detach(previousContext); | ||
| cancellableContext.close(); | ||
| } | ||
|
Comment on lines
+846
to
+849
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If To prevent this, guard the if (previousContext != null) {
cancellableContext.detach(previousContext);
}
if (cancellableContext != null) {
cancellableContext.close();
} |
||
| } | ||
| responseObserver.onCompleted(); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of mutating
BigtableDataSettingsincreateClient(which overwrites the library defaults and requires theequals(oldTotalTimeout)heuristic to unclamprpcTimeout/maxRpcTimeout), could we storeOptional<Duration> perOperationTimeoutonCbtClientand leaveclient.settings()at the library defaults (similar to the C++ test proxy)?Then every operation can resolve the effective timeout (request override -> client override -> empty) and only clamp down from the untouched library defaults:
And in each operation handler: