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

Commit 4b3f21b

Browse files
Add response metadata handling support (#490)
Add support for handling response metadata headers and trailers
1 parent b06fc12 commit 4b3f21b

File tree

10 files changed

+610
-1
lines changed

10 files changed

+610
-1
lines changed

gax-grpc/src/main/java/com/google/api/gax/grpc/CallOptionsUtil.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*/
3030
package com.google.api.gax.grpc;
3131

32+
import com.google.common.base.Preconditions;
3233
import com.google.common.collect.ImmutableMap;
3334
import io.grpc.CallOptions;
3435
import io.grpc.Metadata;
@@ -44,6 +45,8 @@ class CallOptionsUtil {
4445
// this is the header name, it is transferred over the wire
4546
static Metadata.Key<String> REQUEST_PARAMS_HEADER_KEY =
4647
Metadata.Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER);
48+
private static final CallOptions.Key<ResponseMetadataHandler> METADATA_HANDLER_CALL_OPTION_KEY =
49+
CallOptions.Key.of("gax_metadata_handler", null);
4750

4851
private CallOptionsUtil() {}
4952

@@ -69,4 +72,15 @@ static CallOptions putRequestParamsDynamicHeaderOption(
6972
static Map<Key<String>, String> getDynamicHeadersOption(CallOptions callOptions) {
7073
return callOptions.getOption(DYNAMIC_HEADERS_CALL_OPTION_KEY);
7174
}
75+
76+
static CallOptions putMetadataHandlerOption(
77+
CallOptions callOptions, ResponseMetadataHandler handler) {
78+
Preconditions.checkNotNull(callOptions);
79+
Preconditions.checkNotNull(handler);
80+
return callOptions.withOption(METADATA_HANDLER_CALL_OPTION_KEY, handler);
81+
}
82+
83+
public static ResponseMetadataHandler getMetadataHandlerOption(CallOptions callOptions) {
84+
return callOptions.getOption(METADATA_HANDLER_CALL_OPTION_KEY);
85+
}
7286
}

gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ public ApiCallContext merge(ApiCallContext inputCallContext) {
221221
}
222222

223223
CallOptions newCallOptions =
224-
this.callOptions.withCallCredentials(newCallCredentials).withDeadline(newDeadline);
224+
grpcCallContext
225+
.callOptions
226+
.withCallCredentials(newCallCredentials)
227+
.withDeadline(newDeadline);
225228

226229
return new GrpcCallContext(
227230
newChannel, newCallOptions, newStreamWaitTimeout, newStreamIdleTimeout, newChannelAffinity);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2018 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.grpc;
31+
32+
import com.google.api.core.InternalApi;
33+
import io.grpc.CallOptions;
34+
import io.grpc.Channel;
35+
import io.grpc.ClientCall;
36+
import io.grpc.ClientCall.Listener;
37+
import io.grpc.ClientInterceptor;
38+
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
39+
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
40+
import io.grpc.Metadata;
41+
import io.grpc.MethodDescriptor;
42+
import io.grpc.Status;
43+
44+
/**
45+
* An interceptor to handle receiving the response headers.
46+
*
47+
* <p>Package-private for internal usage.
48+
*/
49+
@InternalApi
50+
class GrpcMetadataHandlerInterceptor implements ClientInterceptor {
51+
52+
@Override
53+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
54+
MethodDescriptor<ReqT, RespT> method, final CallOptions callOptions, Channel next) {
55+
ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
56+
57+
final ResponseMetadataHandler metadataHandler =
58+
CallOptionsUtil.getMetadataHandlerOption(callOptions);
59+
60+
if (metadataHandler == null) {
61+
return call;
62+
}
63+
return new SimpleForwardingClientCall<ReqT, RespT>(call) {
64+
@Override
65+
public void start(Listener<RespT> responseListener, Metadata headers) {
66+
Listener<RespT> forwardingResponseListener =
67+
new SimpleForwardingClientCallListener<RespT>(responseListener) {
68+
@Override
69+
public void onHeaders(Metadata headers) {
70+
super.onHeaders(headers);
71+
metadataHandler.onHeaders(headers);
72+
}
73+
74+
@Override
75+
public void onClose(Status status, Metadata trailers) {
76+
super.onClose(status, trailers);
77+
metadataHandler.onTrailers(trailers);
78+
}
79+
};
80+
super.start(forwardingResponseListener, headers);
81+
}
82+
};
83+
}
84+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2018 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.grpc;
31+
32+
import com.google.api.core.BetaApi;
33+
import com.google.api.gax.rpc.ApiCallContext;
34+
import com.google.common.base.Preconditions;
35+
import io.grpc.Metadata;
36+
37+
/**
38+
* GrpcResponseMetadata provides a mechanism to access the headers and trailers returned by a gRPC
39+
* client method.
40+
*
41+
* <p>NOTE: the GrpcResponseMetadata class is not thread-safe and should NOT be re-used for multiple
42+
* calls. A new instance of GrpcResponseMetadata should be constructed for each call that requires
43+
* metadata to be accessed.
44+
*
45+
* <p>Example usage:
46+
*
47+
* <pre>
48+
* <code>
49+
* GrpcResponseMetadata grpcResponseMetadata = new GrpcResponseMetadata();
50+
* Foo foo = client.getFooCallable().call(getFooRequest, grpcResponseMetadata.createContextWithHandlers());
51+
* Metadata headers = grpcResponseMetadata.getMetadata();
52+
* Metadata trailers = grpcResponseMetadata.getTrailingMetadata();
53+
* </code>
54+
* </pre>
55+
*/
56+
@BetaApi("The surface for response metadata is not stable yet and may change in the future.")
57+
public class GrpcResponseMetadata implements ResponseMetadataHandler {
58+
59+
private volatile Metadata responseMetadata;
60+
private volatile Metadata trailingMetadata;
61+
62+
/**
63+
* Constructs a new call context from an existing ApiCallContext, and sets the CallOptions to add
64+
* handlers to retrieve the headers and trailers, and make them available via the getMetadata and
65+
* getTrailingMetadata methods.
66+
*/
67+
public GrpcCallContext addHandlers(ApiCallContext apiCallContext) {
68+
if (Preconditions.checkNotNull(apiCallContext) instanceof GrpcCallContext) {
69+
return addHandlers((GrpcCallContext) apiCallContext);
70+
}
71+
throw new IllegalArgumentException(
72+
"context must be an instance of GrpcCallContext, but found "
73+
+ apiCallContext.getClass().getName());
74+
}
75+
76+
/**
77+
* Constructs a new call context and sets the CallOptions to add handlers to retrieve the headers
78+
* and trailers, and make them available via the getMetadata and getTrailingMetadata methods.
79+
*/
80+
public GrpcCallContext createContextWithHandlers() {
81+
return addHandlers(GrpcCallContext.createDefault());
82+
}
83+
84+
private GrpcCallContext addHandlers(GrpcCallContext grpcCallContext) {
85+
return Preconditions.checkNotNull(grpcCallContext)
86+
.withCallOptions(
87+
CallOptionsUtil.putMetadataHandlerOption(grpcCallContext.getCallOptions(), this));
88+
}
89+
90+
/**
91+
* Returns the headers from the gRPC method as Metadata. If the call has not completed, will
92+
* return null.
93+
*/
94+
public Metadata getMetadata() {
95+
return responseMetadata;
96+
}
97+
98+
/**
99+
* Returns the trailers from the gRPC method as Metadata. If the call has not completed, will
100+
* return null.
101+
*/
102+
public Metadata getTrailingMetadata() {
103+
return trailingMetadata;
104+
}
105+
106+
@Override
107+
public void onHeaders(Metadata metadata) {
108+
responseMetadata = metadata;
109+
}
110+
111+
@Override
112+
public void onTrailers(Metadata metadata) {
113+
trailingMetadata = metadata;
114+
}
115+
}

gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ private ManagedChannel createSingleChannel() throws IOException {
156156
ScheduledExecutorService executor = executorProvider.getExecutor();
157157
GrpcHeaderInterceptor headerInterceptor =
158158
new GrpcHeaderInterceptor(headerProvider.getHeaders());
159+
GrpcMetadataHandlerInterceptor metadataHandlerInterceptor =
160+
new GrpcMetadataHandlerInterceptor();
159161

160162
int colon = endpoint.indexOf(':');
161163
if (colon < 0) {
@@ -167,6 +169,7 @@ private ManagedChannel createSingleChannel() throws IOException {
167169
ManagedChannelBuilder builder =
168170
ManagedChannelBuilder.forAddress(serviceAddress, port)
169171
.intercept(headerInterceptor)
172+
.intercept(metadataHandlerInterceptor)
170173
.userAgent(headerInterceptor.getUserAgentHeader())
171174
.executor(executor);
172175
if (maxInboundMessageSize != null) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2018 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.grpc;
31+
32+
import com.google.api.core.BetaApi;
33+
import io.grpc.Metadata;
34+
35+
/**
36+
* An interface to handle metadata returned from an RPC. A ResponseMetadataHandler is used by the
37+
* GrpcMetadataHandlerInterceptor class to provide custom handling of the returned headers and
38+
* trailers.
39+
*/
40+
@BetaApi("The surface for response metadata is not stable yet and may change in the future.")
41+
public interface ResponseMetadataHandler {
42+
43+
/** Handle the headers returned by an RPC. */
44+
void onHeaders(Metadata metadata);
45+
46+
/** Handle the trailers returned by an RPC. */
47+
void onTrailers(Metadata metadata);
48+
}

gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcCallContextTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.auth.Credentials;
3939
import com.google.common.collect.ImmutableMap;
4040
import com.google.common.truth.Truth;
41+
import io.grpc.CallOptions;
4142
import io.grpc.ManagedChannel;
4243
import io.grpc.Metadata.Key;
4344
import java.util.Map;
@@ -194,4 +195,19 @@ public void testMergeWithStreamingIdleTimeout() {
194195

195196
Truth.assertThat(ctx1.merge(ctx2).getStreamIdleTimeout()).isEqualTo(timeout);
196197
}
198+
199+
@Test
200+
public void testMergeWithCustomCallOptions() {
201+
CallOptions.Key<String> key = CallOptions.Key.of("somekey", "somedefault");
202+
GrpcCallContext ctx1 = GrpcCallContext.createDefault();
203+
GrpcCallContext ctx2 =
204+
GrpcCallContext.createDefault()
205+
.withCallOptions(CallOptions.DEFAULT.withOption(key, "somevalue"));
206+
207+
GrpcCallContext merged = (GrpcCallContext) ctx1.merge(ctx2);
208+
Truth.assertThat(merged.getCallOptions().getOption(key))
209+
.isNotEqualTo(ctx1.getCallOptions().getOption(key));
210+
Truth.assertThat(merged.getCallOptions().getOption(key))
211+
.isEqualTo(ctx2.getCallOptions().getOption(key));
212+
}
197213
}

0 commit comments

Comments
 (0)