Skip to content

Commit ff9bece

Browse files
committed
fix(spanner): scope server-timing metrics per call and guard interceptor lifecycle callbacks
- Scope gfeLatency and afeLatency to per-call listener instances in HeaderInterceptor to eliminate data races and cross-RPC telemetry pollution. - Guard onHeaders and onClose in HeaderInterceptor with try...finally to guarantee downstream callback propagation (preventing hung futures) and RequestIdTargetTracker cleanup. - Catch Throwable in SpannerErrorInterceptor.onClose to prevent unexpected metadata parsing errors from escaping into gRPC transport threads. The above changes should guarantee that all interceptors in the entire chain of interceptors are always executed, and that no exceptions escape to the gRPC thread executing them.
1 parent 0bcc963 commit ff9bece

4 files changed

Lines changed: 704 additions & 49 deletions

File tree

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java

Lines changed: 87 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,23 @@
2525
import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_LATENCY;
2626

2727
import com.google.api.gax.tracing.ApiTracer;
28-
import com.google.cloud.spanner.*;
28+
import com.google.cloud.spanner.BuiltInMetricsConstant;
29+
import com.google.cloud.spanner.CompositeTracer;
30+
import com.google.cloud.spanner.SpannerExceptionFactory;
31+
import com.google.cloud.spanner.SpannerRpcMetrics;
32+
import com.google.cloud.spanner.XGoogSpannerRequestId;
2933
import com.google.common.cache.Cache;
3034
import com.google.common.cache.CacheBuilder;
3135
import com.google.spanner.admin.database.v1.DatabaseName;
32-
import io.grpc.*;
36+
import io.grpc.CallOptions;
37+
import io.grpc.Channel;
38+
import io.grpc.ClientCall;
39+
import io.grpc.ClientInterceptor;
3340
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
3441
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
42+
import io.grpc.Metadata;
43+
import io.grpc.MethodDescriptor;
44+
import io.grpc.Status;
3545
import io.grpc.alts.AltsContextUtil;
3646
import io.opencensus.stats.MeasureMap;
3747
import io.opencensus.stats.Stats;
@@ -52,6 +62,7 @@
5262
import java.util.logging.Logger;
5363
import java.util.regex.Matcher;
5464
import java.util.regex.Pattern;
65+
import javax.annotation.Nullable;
5566

5667
/**
5768
* Intercepts all gRPC calls to extract server-timing header. Captures GFE Latency and GFE Header
@@ -89,8 +100,6 @@ class HeaderInterceptor implements ClientInterceptor {
89100
private static final Logger LOGGER = Logger.getLogger(HeaderInterceptor.class.getName());
90101
private static final Level LEVEL = Level.INFO;
91102
private final SpannerRpcMetrics spannerRpcMetrics;
92-
private Float gfeLatency;
93-
private Float afeLatency;
94103

95104
HeaderInterceptor(SpannerRpcMetrics spannerRpcMetrics) {
96105
this.spannerRpcMetrics = spannerRpcMetrics;
@@ -118,48 +127,63 @@ public void start(Listener<RespT> responseListener, Metadata headers) {
118127

119128
super.start(
120129
new SimpleForwardingClientCallListener<RespT>(responseListener) {
130+
private Float gfeLatency;
131+
private Float afeLatency;
132+
121133
@Override
122134
public void onHeaders(Metadata metadata) {
123-
recordFirstResponseLatency(requestId, startedAtNanos, firstResponseRecorded);
124-
String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
125135
try {
126-
// Get gfe and afe Latency value
127-
Map<String, Float> serverTimingMetrics = parseServerTimingHeader(serverTiming);
128-
gfeLatency = serverTimingMetrics.get(GFE_TIMING_HEADER);
129-
afeLatency = serverTimingMetrics.get(AFE_TIMING_HEADER);
130-
} catch (NumberFormatException e) {
131-
LOGGER.log(LEVEL, "Invalid server-timing object in header: {}", serverTiming);
136+
recordFirstResponseLatency(requestId, startedAtNanos, firstResponseRecorded);
137+
String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
138+
try {
139+
// Get gfe and afe Latency value
140+
Map<String, Float> serverTimingMetrics = parseServerTimingHeader(serverTiming);
141+
gfeLatency = serverTimingMetrics.get(GFE_TIMING_HEADER);
142+
afeLatency = serverTimingMetrics.get(AFE_TIMING_HEADER);
143+
} catch (NumberFormatException e) {
144+
LOGGER.log(LEVEL, "Invalid server-timing object in header: {0}", serverTiming);
145+
}
146+
} catch (Throwable throwable) {
147+
LOGGER.log(
148+
Level.WARNING, "Error processing headers in HeaderInterceptor", throwable);
149+
} finally {
150+
super.onHeaders(metadata);
132151
}
133-
134-
super.onHeaders(metadata);
135152
}
136153

137154
@Override
138155
public void onClose(Status status, Metadata trailers) {
139-
// Record Built-in Metrics
140-
boolean isDirectPathUsed = AltsContextUtil.check(getAttributes());
141-
boolean isAfeEnabled = GapicSpannerRpc.isEnableAFEServerTiming();
142-
recordSpan(span, requestId);
143-
recordCustomMetrics(tagContext, attributes, isDirectPathUsed);
144-
Map<String, String> builtInMetricsAttributes = new HashMap<>();
145156
try {
146-
builtInMetricsAttributes =
147-
new HashMap<>(getBuiltInMetricAttributes(key, databaseName));
148-
} catch (ExecutionException e) {
149-
LOGGER.log(
150-
LEVEL, "Unable to get built-in metric attributes {}", e.getMessage());
157+
// Record Built-in Metrics
158+
boolean isDirectPathUsed = AltsContextUtil.check(getAttributes());
159+
boolean isAfeEnabled = GapicSpannerRpc.isEnableAFEServerTiming();
160+
recordSpan(span, requestId, gfeLatency, afeLatency);
161+
recordCustomMetrics(tagContext, attributes, isDirectPathUsed, gfeLatency);
162+
Map<String, String> builtInMetricsAttributes = new HashMap<>();
163+
try {
164+
builtInMetricsAttributes =
165+
new HashMap<>(getBuiltInMetricAttributes(key, databaseName));
166+
} catch (ExecutionException e) {
167+
LOGGER.log(
168+
LEVEL, "Unable to get built-in metric attributes {0}", e.getMessage());
169+
}
170+
if (status.isOk()) {
171+
recordFirstResponseLatency(requestId, startedAtNanos, firstResponseRecorded);
172+
}
173+
recordBuiltInMetrics(
174+
compositeTracer,
175+
builtInMetricsAttributes,
176+
requestId,
177+
isDirectPathUsed,
178+
isAfeEnabled,
179+
gfeLatency,
180+
afeLatency);
181+
} catch (Throwable throwable) {
182+
LOGGER.log(Level.WARNING, "Error recording metrics in onClose", throwable);
183+
} finally {
184+
RequestIdTargetTracker.remove(requestId);
185+
super.onClose(status, trailers);
151186
}
152-
if (status.isOk()) {
153-
recordFirstResponseLatency(requestId, startedAtNanos, firstResponseRecorded);
154-
}
155-
recordBuiltInMetrics(
156-
compositeTracer,
157-
builtInMetricsAttributes,
158-
requestId,
159-
isDirectPathUsed,
160-
isAfeEnabled);
161-
RequestIdTargetTracker.remove(requestId);
162-
super.onClose(status, trailers);
163187
}
164188
},
165189
headers);
@@ -172,11 +196,14 @@ public void onClose(Status status, Metadata trailers) {
172196
}
173197

174198
private void recordCustomMetrics(
175-
TagContext tagContext, Attributes attributes, Boolean isDirectPathUsed) {
199+
TagContext tagContext,
200+
Attributes attributes,
201+
Boolean isDirectPathUsed,
202+
@Nullable Float gfeLatency) {
176203
// Record OpenCensus and Custom OpenTelemetry Metrics
177204
MeasureMap measureMap = STATS_RECORDER.newMeasureMap();
178205

179-
if (!isDirectPathUsed) {
206+
if (!Boolean.TRUE.equals(isDirectPathUsed)) {
180207
if (gfeLatency != null) {
181208
long gfeVal = gfeLatency.longValue();
182209
measureMap.put(SPANNER_GFE_LATENCY, gfeVal);
@@ -191,31 +218,45 @@ private void recordCustomMetrics(
191218
measureMap.record(tagContext);
192219
}
193220

194-
private void recordSpan(Span span, String requestId) {
221+
private void recordSpan(
222+
@Nullable Span span,
223+
@Nullable String requestId,
224+
@Nullable Float gfeLatency,
225+
@Nullable Float afeLatency) {
195226
if (span != null) {
196227
if (gfeLatency != null) {
197228
span.setAttribute("gfe_latency", gfeLatency.toString());
198229
}
199230
if (afeLatency != null) {
200231
span.setAttribute("afe_latency", afeLatency.toString());
201232
}
202-
span.setAttribute(XGoogSpannerRequestId.REQUEST_ID_HEADER_NAME, requestId);
233+
if (requestId != null) {
234+
span.setAttribute(XGoogSpannerRequestId.REQUEST_ID_HEADER_NAME, requestId);
235+
}
203236
}
204237
}
205238

206239
private void recordBuiltInMetrics(
207-
CompositeTracer compositeTracer,
240+
@Nullable CompositeTracer compositeTracer,
208241
Map<String, String> builtInMetricsAttributes,
209-
String requestId,
242+
@Nullable String requestId,
210243
Boolean isDirectPathUsed,
211-
Boolean isAfeEnabled) {
244+
Boolean isAfeEnabled,
245+
@Nullable Float gfeLatency,
246+
@Nullable Float afeLatency) {
212247
if (compositeTracer != null) {
213-
builtInMetricsAttributes.put(BuiltInMetricsConstant.REQUEST_ID_KEY.getKey(), requestId);
248+
if (requestId != null) {
249+
builtInMetricsAttributes.put(BuiltInMetricsConstant.REQUEST_ID_KEY.getKey(), requestId);
250+
}
214251
builtInMetricsAttributes.put(
215-
BuiltInMetricsConstant.DIRECT_PATH_USED_KEY.getKey(), Boolean.toString(isDirectPathUsed));
252+
BuiltInMetricsConstant.DIRECT_PATH_USED_KEY.getKey(),
253+
Boolean.toString(Boolean.TRUE.equals(isDirectPathUsed)));
216254
compositeTracer.addAttributes(builtInMetricsAttributes);
217255
compositeTracer.recordServerTimingHeaderMetrics(
218-
gfeLatency, afeLatency, isDirectPathUsed, isAfeEnabled);
256+
gfeLatency,
257+
afeLatency,
258+
Boolean.TRUE.equals(isDirectPathUsed),
259+
Boolean.TRUE.equals(isAfeEnabled));
219260
}
220261
}
221262

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerErrorInterceptor.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,13 @@ public void onClose(Status status, Metadata trailers) {
118118
if (trailers.containsKey(RETRY_INFO_KEY)) {
119119
status = status.augmentDescription(trailers.get(RETRY_INFO_KEY).toString());
120120
}
121-
} catch (IllegalArgumentException e) {
121+
} catch (Throwable throwable) {
122122
// Messages could be invalid if, say, some invalid UTF8 is echoed back in some
123-
// error text.
124-
logger.log(Level.WARNING, "Invalid protocol message in metadata", e);
123+
// error text, or if an unexpected exception occurs during metadata inspection.
124+
logger.log(
125+
Level.WARNING,
126+
"Error processing error details in SpannerErrorInterceptor",
127+
throwable);
125128
} finally {
126129
super.onClose(status, trailers);
127130
}

0 commit comments

Comments
 (0)