Skip to content
This repository was archived by the owner on Sep 26, 2023. It is now read-only.

Commit dd5f955

Browse files
feat: optimize unary callables to not wait for trailers (#1356)
* feat: optimize unary callables to not wait for trailers [draft] gRPC ClientCalls and thus gax currently wait for trailers to resolve unary call futures. I believe the original reason for this was to mitigate misconfigured servers where a server endpoint was changed to be server streaming, but the client still expects a unary method. We measured the cost of this safety net to be O(hundreds of millis). For low latency services like Bigtable, this is very high. This PR is incomplete, but is meant to be a conversation starter. I would like to get gax's opinion on this and guidance how to proceed. Some initial proposals: 1. productionize this PR and roll it out 2. gate this behavior using a flag in UnaryCallSettings 3. expose a bit more surface in gax to allow cloud bigtable to build our callable chains (the current blocker is that GrpcUnaryRequestParamCallable & GrpcExceptionCallable are package private * add an opt-in for skipping trailers * oops * address feedback * remove separate callable * oops * format
1 parent aab5288 commit dd5f955

File tree

4 files changed

+141
-11
lines changed

4 files changed

+141
-11
lines changed

‎gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallSettings.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@
3838
public class GrpcCallSettings<RequestT, ResponseT> {
3939
private final MethodDescriptor<RequestT, ResponseT> methodDescriptor;
4040
private final RequestParamsExtractor<RequestT> paramsExtractor;
41+
private final boolean alwaysAwaitTrailers;
4142

42-
private GrpcCallSettings(
43-
MethodDescriptor<RequestT, ResponseT> methodDescriptor,
44-
RequestParamsExtractor<RequestT> paramsExtractor) {
45-
this.methodDescriptor = methodDescriptor;
46-
this.paramsExtractor = paramsExtractor;
43+
private GrpcCallSettings(Builder builder) {
44+
this.methodDescriptor = builder.methodDescriptor;
45+
this.paramsExtractor = builder.paramsExtractor;
46+
this.alwaysAwaitTrailers = builder.shouldAwaitTrailers;
4747
}
4848

4949
public MethodDescriptor<RequestT, ResponseT> getMethodDescriptor() {
@@ -55,8 +55,13 @@ public RequestParamsExtractor<RequestT> getParamsExtractor() {
5555
return paramsExtractor;
5656
}
5757

58+
@BetaApi
59+
public boolean shouldAwaitTrailers() {
60+
return alwaysAwaitTrailers;
61+
}
62+
5863
public static <RequestT, ResponseT> Builder<RequestT, ResponseT> newBuilder() {
59-
return new Builder<>();
64+
return new Builder<RequestT, ResponseT>().setShouldAwaitTrailers(true);
6065
}
6166

6267
public static <RequestT, ResponseT> GrpcCallSettings<RequestT, ResponseT> create(
@@ -73,11 +78,14 @@ public Builder toBuilder() {
7378
public static class Builder<RequestT, ResponseT> {
7479
private MethodDescriptor<RequestT, ResponseT> methodDescriptor;
7580
private RequestParamsExtractor<RequestT> paramsExtractor;
81+
private boolean shouldAwaitTrailers;
7682

7783
private Builder() {}
7884

7985
private Builder(GrpcCallSettings<RequestT, ResponseT> settings) {
8086
this.methodDescriptor = settings.methodDescriptor;
87+
this.paramsExtractor = settings.paramsExtractor;
88+
this.shouldAwaitTrailers = settings.alwaysAwaitTrailers;
8189
}
8290

8391
public Builder<RequestT, ResponseT> setMethodDescriptor(
@@ -93,8 +101,14 @@ public Builder<RequestT, ResponseT> setParamsExtractor(
93101
return this;
94102
}
95103

104+
@BetaApi
105+
public Builder<RequestT, ResponseT> setShouldAwaitTrailers(boolean b) {
106+
this.shouldAwaitTrailers = b;
107+
return this;
108+
}
109+
96110
public GrpcCallSettings<RequestT, ResponseT> build() {
97-
return new GrpcCallSettings<>(methodDescriptor, paramsExtractor);
111+
return new GrpcCallSettings<>(this);
98112
}
99113
}
100114
}

‎gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcClientCalls.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
package com.google.api.gax.grpc;
3131

3232
import com.google.api.client.util.Preconditions;
33+
import com.google.api.core.AbstractApiFuture;
34+
import com.google.api.core.ApiFuture;
35+
import com.google.api.core.BetaApi;
3336
import com.google.api.gax.rpc.ApiCallContext;
3437
import com.google.api.gax.tracing.ApiTracer.Scope;
3538
import io.grpc.CallOptions;
@@ -38,16 +41,22 @@
3841
import io.grpc.ClientInterceptor;
3942
import io.grpc.ClientInterceptors;
4043
import io.grpc.Deadline;
44+
import io.grpc.Metadata;
4145
import io.grpc.MethodDescriptor;
46+
import io.grpc.Status;
4247
import io.grpc.stub.MetadataUtils;
4348
import java.util.concurrent.TimeUnit;
49+
import java.util.logging.Level;
50+
import java.util.logging.Logger;
4451

4552
/**
4653
* {@code GrpcClientCalls} creates a new {@code ClientCall} from the given call context.
4754
*
4855
* <p>Package-private for internal use.
4956
*/
5057
class GrpcClientCalls {
58+
private static final Logger LOGGER = Logger.getLogger(GrpcDirectCallable.class.getName());
59+
5160
private GrpcClientCalls() {};
5261

5362
public static <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
@@ -90,4 +99,101 @@ public static <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
9099
return channel.newCall(descriptor, callOptions);
91100
}
92101
}
102+
103+
/**
104+
* A work-alike of {@link io.grpc.stub.ClientCalls#futureUnaryCall(ClientCall, Object)}.
105+
*
106+
* <p>The only difference is that unlike grpc-stub's implementation. This implementation doesn't
107+
* wait for trailers to resolve a unary RPC. This can save milliseconds when the server is
108+
* overloaded.
109+
*/
110+
@BetaApi
111+
static <RequestT, ResponseT> ApiFuture<ResponseT> eagerFutureUnaryCall(
112+
ClientCall<RequestT, ResponseT> clientCall, RequestT request) {
113+
// Start the call
114+
GrpcFuture<ResponseT> future = new GrpcFuture<>(clientCall);
115+
clientCall.start(new EagerFutureListener<>(future), new Metadata());
116+
117+
// Send the request
118+
try {
119+
clientCall.sendMessage(request);
120+
clientCall.halfClose();
121+
// Request an extra message to detect misconfigured servers
122+
clientCall.request(2);
123+
} catch (Throwable sendError) {
124+
// Cancel if anything goes wrong
125+
try {
126+
clientCall.cancel(null, sendError);
127+
} catch (Throwable cancelError) {
128+
LOGGER.log(Level.SEVERE, "Error encountered while closing it", sendError);
129+
}
130+
131+
throw sendError;
132+
}
133+
134+
return future;
135+
}
136+
137+
/** Thin wrapper around an ApiFuture that will cancel the underlying ClientCall. */
138+
private static class GrpcFuture<T> extends AbstractApiFuture<T> {
139+
private final ClientCall<?, T> call;
140+
141+
private GrpcFuture(ClientCall<?, T> call) {
142+
this.call = call;
143+
}
144+
145+
@Override
146+
protected void interruptTask() {
147+
call.cancel("GrpcFuture was cancelled", null);
148+
}
149+
150+
@Override
151+
public boolean set(T value) {
152+
return super.set(value);
153+
}
154+
155+
@Override
156+
public boolean setException(Throwable throwable) {
157+
return super.setException(throwable);
158+
}
159+
}
160+
161+
/**
162+
* A bridge between gRPC's ClientCall.Listener to an ApiFuture.
163+
*
164+
* <p>The Listener will eagerly resolve the future when the first message is received and will not
165+
* wait for the trailers. This should cut down on the latency at the expense of safety. If the
166+
* server is misconfigured and sends a second response for a unary call, the error will be logged,
167+
* but the future will still be successful.
168+
*/
169+
private static class EagerFutureListener<T> extends ClientCall.Listener<T> {
170+
private final GrpcFuture<T> future;
171+
172+
private EagerFutureListener(GrpcFuture<T> future) {
173+
this.future = future;
174+
}
175+
176+
@Override
177+
public void onMessage(T message) {
178+
if (!future.set(message)) {
179+
throw Status.INTERNAL
180+
.withDescription("More than one value received for unary call")
181+
.asRuntimeException();
182+
}
183+
}
184+
185+
@Override
186+
public void onClose(Status status, Metadata trailers) {
187+
if (!future.isDone()) {
188+
future.setException(
189+
Status.INTERNAL
190+
.withDescription("No value received for unary call")
191+
.asException(trailers));
192+
}
193+
if (!status.isOk()) {
194+
LOGGER.log(
195+
Level.WARNING, "Received error for unary call after receiving a successful response");
196+
}
197+
}
198+
}
93199
}

‎gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcDirectCallable.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.api.gax.rpc.ApiCallContext;
3535
import com.google.api.gax.rpc.UnaryCallable;
3636
import com.google.common.base.Preconditions;
37+
import io.grpc.ClientCall;
3738
import io.grpc.MethodDescriptor;
3839
import io.grpc.stub.ClientCalls;
3940

@@ -44,18 +45,25 @@
4445
*/
4546
class GrpcDirectCallable<RequestT, ResponseT> extends UnaryCallable<RequestT, ResponseT> {
4647
private final MethodDescriptor<RequestT, ResponseT> descriptor;
48+
private final boolean awaitTrailers;
4749

48-
GrpcDirectCallable(MethodDescriptor<RequestT, ResponseT> descriptor) {
50+
GrpcDirectCallable(MethodDescriptor<RequestT, ResponseT> descriptor, boolean awaitTrailers) {
4951
this.descriptor = Preconditions.checkNotNull(descriptor);
52+
this.awaitTrailers = awaitTrailers;
5053
}
5154

5255
@Override
5356
public ApiFuture<ResponseT> futureCall(RequestT request, ApiCallContext inputContext) {
5457
Preconditions.checkNotNull(request);
5558
Preconditions.checkNotNull(inputContext);
5659

57-
return new ListenableFutureToApiFuture<>(
58-
ClientCalls.futureUnaryCall(GrpcClientCalls.newCall(descriptor, inputContext), request));
60+
ClientCall<RequestT, ResponseT> clientCall = GrpcClientCalls.newCall(descriptor, inputContext);
61+
62+
if (awaitTrailers) {
63+
return new ListenableFutureToApiFuture<>(ClientCalls.futureUnaryCall(clientCall, request));
64+
} else {
65+
return GrpcClientCalls.eagerFutureUnaryCall(clientCall, request);
66+
}
5967
}
6068

6169
@Override

‎gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcRawCallableFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ private GrpcRawCallableFactory() {}
5252
public static <RequestT, ResponseT> UnaryCallable<RequestT, ResponseT> createUnaryCallable(
5353
GrpcCallSettings<RequestT, ResponseT> grpcCallSettings, Set<StatusCode.Code> retryableCodes) {
5454
UnaryCallable<RequestT, ResponseT> callable =
55-
new GrpcDirectCallable<>(grpcCallSettings.getMethodDescriptor());
55+
new GrpcDirectCallable<>(
56+
grpcCallSettings.getMethodDescriptor(), grpcCallSettings.shouldAwaitTrailers());
57+
5658
if (grpcCallSettings.getParamsExtractor() != null) {
5759
callable =
5860
new GrpcUnaryRequestParamCallable<>(callable, grpcCallSettings.getParamsExtractor());

0 commit comments

Comments
 (0)