Skip to content

Commit f35c570

Browse files
authored
fix(spanner): scope server-timing metrics per call and guard interceptor lifecycle callbacks (#14053)
- 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 f35c570

4 files changed

Lines changed: 702 additions & 49 deletions

File tree

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

Lines changed: 89 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,65 @@ 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 =
141+
parseServerTimingHeader(serverTiming);
142+
gfeLatency = serverTimingMetrics.get(GFE_TIMING_HEADER);
143+
afeLatency = serverTimingMetrics.get(AFE_TIMING_HEADER);
144+
} catch (NumberFormatException e) {
145+
LOGGER.log(
146+
LEVEL, "Invalid server-timing object in header: {0}", serverTiming);
147+
}
148+
} catch (Throwable throwable) {
149+
LOGGER.log(
150+
Level.WARNING, "Error processing headers in HeaderInterceptor", throwable);
151+
} finally {
152+
super.onHeaders(metadata);
132153
}
133-
134-
super.onHeaders(metadata);
135154
}
136155

137156
@Override
138157
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<>();
145158
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());
159+
// Record Built-in Metrics
160+
boolean isDirectPathUsed = AltsContextUtil.check(getAttributes());
161+
boolean isAfeEnabled = GapicSpannerRpc.isEnableAFEServerTiming();
162+
recordSpan(span, requestId, gfeLatency, afeLatency);
163+
recordCustomMetrics(tagContext, attributes, isDirectPathUsed, gfeLatency);
164+
Map<String, String> builtInMetricsAttributes = new HashMap<>();
165+
try {
166+
builtInMetricsAttributes =
167+
new HashMap<>(getBuiltInMetricAttributes(key, databaseName));
168+
} catch (ExecutionException e) {
169+
LOGGER.log(
170+
LEVEL, "Unable to get built-in metric attributes {0}", e.getMessage());
171+
}
172+
if (status.isOk()) {
173+
recordFirstResponseLatency(requestId, startedAtNanos, firstResponseRecorded);
174+
}
175+
recordBuiltInMetrics(
176+
compositeTracer,
177+
builtInMetricsAttributes,
178+
requestId,
179+
isDirectPathUsed,
180+
isAfeEnabled,
181+
gfeLatency,
182+
afeLatency);
183+
} catch (Throwable throwable) {
184+
LOGGER.log(Level.WARNING, "Error recording metrics in onClose", throwable);
185+
} finally {
186+
RequestIdTargetTracker.remove(requestId);
187+
super.onClose(status, trailers);
151188
}
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);
163189
}
164190
},
165191
headers);
@@ -172,11 +198,14 @@ public void onClose(Status status, Metadata trailers) {
172198
}
173199

174200
private void recordCustomMetrics(
175-
TagContext tagContext, Attributes attributes, Boolean isDirectPathUsed) {
201+
TagContext tagContext,
202+
Attributes attributes,
203+
Boolean isDirectPathUsed,
204+
@Nullable Float gfeLatency) {
176205
// Record OpenCensus and Custom OpenTelemetry Metrics
177206
MeasureMap measureMap = STATS_RECORDER.newMeasureMap();
178207

179-
if (!isDirectPathUsed) {
208+
if (!Boolean.TRUE.equals(isDirectPathUsed)) {
180209
if (gfeLatency != null) {
181210
long gfeVal = gfeLatency.longValue();
182211
measureMap.put(SPANNER_GFE_LATENCY, gfeVal);
@@ -191,31 +220,45 @@ private void recordCustomMetrics(
191220
measureMap.record(tagContext);
192221
}
193222

194-
private void recordSpan(Span span, String requestId) {
223+
private void recordSpan(
224+
@Nullable Span span,
225+
@Nullable String requestId,
226+
@Nullable Float gfeLatency,
227+
@Nullable Float afeLatency) {
195228
if (span != null) {
196229
if (gfeLatency != null) {
197230
span.setAttribute("gfe_latency", gfeLatency.toString());
198231
}
199232
if (afeLatency != null) {
200233
span.setAttribute("afe_latency", afeLatency.toString());
201234
}
202-
span.setAttribute(XGoogSpannerRequestId.REQUEST_ID_HEADER_NAME, requestId);
235+
if (requestId != null) {
236+
span.setAttribute(XGoogSpannerRequestId.REQUEST_ID_HEADER_NAME, requestId);
237+
}
203238
}
204239
}
205240

206241
private void recordBuiltInMetrics(
207-
CompositeTracer compositeTracer,
242+
@Nullable CompositeTracer compositeTracer,
208243
Map<String, String> builtInMetricsAttributes,
209-
String requestId,
244+
@Nullable String requestId,
210245
Boolean isDirectPathUsed,
211-
Boolean isAfeEnabled) {
246+
Boolean isAfeEnabled,
247+
@Nullable Float gfeLatency,
248+
@Nullable Float afeLatency) {
212249
if (compositeTracer != null) {
213-
builtInMetricsAttributes.put(BuiltInMetricsConstant.REQUEST_ID_KEY.getKey(), requestId);
250+
if (requestId != null) {
251+
builtInMetricsAttributes.put(BuiltInMetricsConstant.REQUEST_ID_KEY.getKey(), requestId);
252+
}
214253
builtInMetricsAttributes.put(
215-
BuiltInMetricsConstant.DIRECT_PATH_USED_KEY.getKey(), Boolean.toString(isDirectPathUsed));
254+
BuiltInMetricsConstant.DIRECT_PATH_USED_KEY.getKey(),
255+
Boolean.toString(Boolean.TRUE.equals(isDirectPathUsed)));
216256
compositeTracer.addAttributes(builtInMetricsAttributes);
217257
compositeTracer.recordServerTimingHeaderMetrics(
218-
gfeLatency, afeLatency, isDirectPathUsed, isAfeEnabled);
258+
gfeLatency,
259+
afeLatency,
260+
Boolean.TRUE.equals(isDirectPathUsed),
261+
Boolean.TRUE.equals(isAfeEnabled));
219262
}
220263
}
221264

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)