Skip to content

Commit d25f5ac

Browse files
authored
xds: implement xDS timeout (#7481)
The xDS timeout retrieves per-route timeout value from RouteAction.max_stream_duration.grpc_timeout_header_max or RouteAction.max_stream_duration.max_stream_duration if the former is not set. If neither is set, it eventually falls back to the max_stream_duration setting in HttpConnectionManager.common_http_options retrieved from the Route's upstream Listener resource. The final timeout value applied to the call is the minimum of the xDS timeout value and the per-call timeout set by application.
1 parent ef90da0 commit d25f5ac

File tree

4 files changed

+160
-101
lines changed

4 files changed

+160
-101
lines changed

xds/src/main/java/io/grpc/xds/EnvoyProtoData.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import java.util.List;
4343
import java.util.Map;
4444
import java.util.Objects;
45-
import java.util.concurrent.TimeUnit;
4645
import javax.annotation.Nullable;
4746

4847
/**
@@ -1118,24 +1117,23 @@ static StructOrError<HeaderMatcher> convertEnvoyProtoHeaderMatcher(
11181117
* See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.route.v3.RouteAction}.
11191118
*/
11201119
static final class RouteAction {
1121-
private final long timeoutNano;
1120+
@Nullable
1121+
private final Long timeoutNano;
11221122
// Exactly one of the following fields is non-null.
11231123
@Nullable
11241124
private final String cluster;
11251125
@Nullable
11261126
private final List<ClusterWeight> weightedClusters;
11271127

11281128
@VisibleForTesting
1129-
RouteAction(
1130-
long timeoutNano,
1131-
@Nullable String cluster,
1129+
RouteAction(@Nullable Long timeoutNano, @Nullable String cluster,
11321130
@Nullable List<ClusterWeight> weightedClusters) {
11331131
this.timeoutNano = timeoutNano;
11341132
this.cluster = cluster;
11351133
this.weightedClusters = weightedClusters;
11361134
}
11371135

1138-
1136+
@Nullable
11391137
Long getTimeoutNano() {
11401138
return timeoutNano;
11411139
}
@@ -1172,7 +1170,9 @@ public int hashCode() {
11721170
@Override
11731171
public String toString() {
11741172
ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
1175-
toStringHelper.add("timeout", timeoutNano + "ns");
1173+
if (timeoutNano != null) {
1174+
toStringHelper.add("timeout", timeoutNano + "ns");
1175+
}
11761176
if (cluster != null) {
11771177
toStringHelper.add("cluster", cluster);
11781178
}
@@ -1212,14 +1212,15 @@ static StructOrError<RouteAction> fromEnvoyProtoRouteAction(
12121212
return StructOrError.fromError(
12131213
"Unknown cluster specifier: " + proto.getClusterSpecifierCase());
12141214
}
1215-
long timeoutNano = TimeUnit.SECONDS.toNanos(15L); // default 15s
1216-
if (proto.hasMaxGrpcTimeout()) {
1217-
timeoutNano = Durations.toNanos(proto.getMaxGrpcTimeout());
1218-
} else if (proto.hasTimeout()) {
1219-
timeoutNano = Durations.toNanos(proto.getTimeout());
1220-
}
1221-
if (timeoutNano == 0) {
1222-
timeoutNano = Long.MAX_VALUE;
1215+
Long timeoutNano = null;
1216+
if (proto.hasMaxStreamDuration()) {
1217+
io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration
1218+
= proto.getMaxStreamDuration();
1219+
if (maxStreamDuration.hasGrpcTimeoutHeaderMax()) {
1220+
timeoutNano = Durations.toNanos(maxStreamDuration.getGrpcTimeoutHeaderMax());
1221+
} else if (maxStreamDuration.hasMaxStreamDuration()) {
1222+
timeoutNano = Durations.toNanos(maxStreamDuration.getMaxStreamDuration());
1223+
}
12231224
}
12241225
return StructOrError.fromStruct(new RouteAction(timeoutNano, cluster, weightedClusters));
12251226
}

xds/src/main/java/io/grpc/xds/XdsNameResolver.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ final class XdsNameResolver extends NameResolver {
8686
private final ConcurrentMap<String, AtomicInteger> clusterRefs = new ConcurrentHashMap<>();
8787
private final ConfigSelector configSelector = new ConfigSelector();
8888

89-
private volatile List<Route> currentRoutes = Collections.emptyList();
89+
private volatile RoutingConfig routingConfig = RoutingConfig.empty;
9090
private Listener2 listener;
9191
private ObjectPool<XdsClient> xdsClientPool;
9292
private XdsClient xdsClient;
@@ -326,7 +326,7 @@ public Result selectConfig(PickSubchannelArgs args) {
326326
String cluster = null;
327327
Route selectedRoute = null;
328328
do {
329-
for (Route route : currentRoutes) {
329+
for (Route route : routingConfig.routes) {
330330
if (route.getRouteMatch().matches(
331331
"/" + args.getMethodDescriptor().getFullMethodName(), asciiHeaders)) {
332332
selectedRoute = route;
@@ -359,8 +359,13 @@ public Result selectConfig(PickSubchannelArgs args) {
359359
// TODO(chengyuanzhang): avoid service config generation and parsing for each call.
360360
Map<String, ?> rawServiceConfig = Collections.emptyMap();
361361
if (enableTimeout) {
362-
rawServiceConfig = generateServiceConfigWithMethodTimeoutConfig(
363-
selectedRoute.getRouteAction().getTimeoutNano());
362+
Long timeoutNano = selectedRoute.getRouteAction().getTimeoutNano();
363+
if (timeoutNano == null) {
364+
timeoutNano = routingConfig.fallbackTimeoutNano;
365+
}
366+
if (timeoutNano > 0) {
367+
rawServiceConfig = generateServiceConfigWithMethodTimeoutConfig(timeoutNano);
368+
}
364369
}
365370
ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(rawServiceConfig);
366371
Object config = parsedServiceConfig.getConfig();
@@ -430,10 +435,12 @@ private class ResolveState implements LdsResourceWatcher {
430435
private String rdsResource;
431436
@Nullable
432437
private RdsResourceWatcher rdsWatcher;
438+
private long httpMaxStreamDurationNano;
433439

434440
@Override
435441
public void onChanged(LdsUpdate update) {
436442
logger.log(XdsLogLevel.INFO, "Receive LDS resource update: {0}", update);
443+
httpMaxStreamDurationNano = update.getHttpMaxStreamDurationNano();
437444
List<VirtualHost> virtualHosts = update.getVirtualHosts();
438445
String rdsName = update.getRdsName();
439446
if (rdsName != null && rdsName.equals(rdsResource)) {
@@ -479,6 +486,8 @@ private void stop() {
479486
private void updateRoutes(List<VirtualHost> virtualHosts) {
480487
VirtualHost virtualHost = findVirtualHostForHostName(virtualHosts, authority);
481488
if (virtualHost == null) {
489+
logger.log(XdsLogLevel.WARNING,
490+
"Failed to find virtual host matching hostname {0}", authority);
482491
listener.onResult(emptyResult);
483492
return;
484493
}
@@ -515,7 +524,7 @@ private void updateRoutes(List<VirtualHost> virtualHosts) {
515524
}
516525
// Make newly added clusters selectable by config selector and deleted clusters no longer
517526
// selectable.
518-
currentRoutes = routes;
527+
routingConfig = new RoutingConfig(httpMaxStreamDurationNano, routes);
519528
shouldUpdateResult = false;
520529
for (String cluster : deletedClusters) {
521530
int count = clusterRefs.get(cluster).decrementAndGet();
@@ -560,4 +569,19 @@ public void onResourceDoesNotExist(String resourceName) {
560569
}
561570
}
562571
}
572+
573+
/**
574+
* Grouping of the list of usable routes and their corresponding fallback timeout value.
575+
*/
576+
private static class RoutingConfig {
577+
private long fallbackTimeoutNano;
578+
private List<Route> routes;
579+
580+
private static RoutingConfig empty = new RoutingConfig(0L, Collections.<Route>emptyList());
581+
582+
private RoutingConfig(long fallbackTimeoutNano, List<Route> routes) {
583+
this.fallbackTimeoutNano = fallbackTimeoutNano;
584+
this.routes = routes;
585+
}
586+
}
563587
}

xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent;
3030
import io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher;
3131
import io.envoyproxy.envoy.config.route.v3.RedirectAction;
32+
import io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration;
3233
import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
3334
import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher;
3435
import io.envoyproxy.envoy.type.v3.FractionalPercent;
@@ -212,7 +213,7 @@ public void convertRoute() {
212213
new Route(
213214
new RouteMatch(PathMatcher.fromPath("/service/method", false),
214215
Collections.<HeaderMatcher>emptyList(), null),
215-
new RouteAction(TimeUnit.SECONDS.toNanos(15L), "cluster-foo", null)));
216+
new RouteAction(null, "cluster-foo", null)));
216217

217218
io.envoyproxy.envoy.config.route.v3.Route unsupportedProto =
218219
io.envoyproxy.envoy.config.route.v3.Route.newBuilder()
@@ -370,74 +371,84 @@ public void convertRouteMatch_withRuntimeFraction() {
370371
}
371372

372373
@Test
373-
public void convertRouteAction() {
374-
// cluster_specifier = cluster, default timeout
375-
io.envoyproxy.envoy.config.route.v3.RouteAction proto1 =
374+
public void convertRouteAction_cluster() {
375+
io.envoyproxy.envoy.config.route.v3.RouteAction proto =
376376
io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder()
377377
.setCluster("cluster-foo")
378378
.build();
379-
StructOrError<RouteAction> struct1 = RouteAction.fromEnvoyProtoRouteAction(proto1);
380-
assertThat(struct1.getErrorDetail()).isNull();
381-
assertThat(struct1.getStruct().getTimeoutNano())
382-
.isEqualTo(TimeUnit.SECONDS.toNanos(15L)); // default value
383-
assertThat(struct1.getStruct().getCluster()).isEqualTo("cluster-foo");
384-
assertThat(struct1.getStruct().getWeightedCluster()).isNull();
379+
StructOrError<RouteAction> struct = RouteAction.fromEnvoyProtoRouteAction(proto);
380+
assertThat(struct.getErrorDetail()).isNull();
381+
assertThat(struct.getStruct().getCluster()).isEqualTo("cluster-foo");
382+
assertThat(struct.getStruct().getWeightedCluster()).isNull();
383+
}
385384

386-
// cluster_specifier = cluster, infinity timeout
387-
io.envoyproxy.envoy.config.route.v3.RouteAction proto2 =
385+
@Test
386+
public void convertRouteAction_weightedCluster() {
387+
io.envoyproxy.envoy.config.route.v3.RouteAction proto =
388388
io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder()
389-
.setMaxGrpcTimeout(Durations.fromNanos(0))
390-
.setTimeout(Durations.fromMicros(20L))
391-
.setCluster("cluster-foo")
389+
.setWeightedClusters(
390+
WeightedCluster.newBuilder()
391+
.addClusters(
392+
WeightedCluster.ClusterWeight
393+
.newBuilder()
394+
.setName("cluster-foo")
395+
.setWeight(UInt32Value.newBuilder().setValue(30)))
396+
.addClusters(WeightedCluster.ClusterWeight
397+
.newBuilder()
398+
.setName("cluster-bar")
399+
.setWeight(UInt32Value.newBuilder().setValue(70))))
392400
.build();
393-
StructOrError<RouteAction> struct2 = RouteAction.fromEnvoyProtoRouteAction(proto2);
394-
assertThat(struct2.getStruct().getTimeoutNano())
395-
.isEqualTo(Long.MAX_VALUE); // infinite
401+
StructOrError<RouteAction> struct = RouteAction.fromEnvoyProtoRouteAction(proto);
402+
assertThat(struct.getErrorDetail()).isNull();
403+
assertThat(struct.getStruct().getCluster()).isNull();
404+
assertThat(struct.getStruct().getWeightedCluster()).containsExactly(
405+
new ClusterWeight("cluster-foo", 30), new ClusterWeight("cluster-bar", 70));
406+
}
407+
408+
@Test
409+
public void convertRouteAction_unspecifiedClusterError() {
410+
io.envoyproxy.envoy.config.route.v3.RouteAction proto =
411+
io.envoyproxy.envoy.config.route.v3.RouteAction.getDefaultInstance();
412+
StructOrError<RouteAction> unsetStruct = RouteAction.fromEnvoyProtoRouteAction(proto);
413+
assertThat(unsetStruct.getStruct()).isNull();
414+
assertThat(unsetStruct.getErrorDetail()).isNotNull();
415+
}
396416

397-
// cluster_specifier = cluster, infinity timeout
398-
io.envoyproxy.envoy.config.route.v3.RouteAction proto3 =
417+
@Test
418+
public void convertRouteAction_timeoutByGrpcTimeoutHeaderMax() {
419+
io.envoyproxy.envoy.config.route.v3.RouteAction proto =
399420
io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder()
400-
.setTimeout(Durations.fromNanos(0))
401421
.setCluster("cluster-foo")
422+
.setMaxStreamDuration(
423+
MaxStreamDuration.newBuilder()
424+
.setGrpcTimeoutHeaderMax(Durations.fromSeconds(5L))
425+
.setMaxStreamDuration(Durations.fromMillis(20L)))
402426
.build();
403-
StructOrError<RouteAction> struct3 = RouteAction.fromEnvoyProtoRouteAction(proto3);
404-
assertThat(struct3.getStruct().getTimeoutNano()).isEqualTo(Long.MAX_VALUE); // infinite
427+
StructOrError<RouteAction> struct = RouteAction.fromEnvoyProtoRouteAction(proto);
428+
assertThat(struct.getStruct().getTimeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L));
429+
}
405430

406-
// cluster_specifier = cluster_header
407-
io.envoyproxy.envoy.config.route.v3.RouteAction proto4 =
431+
@Test
432+
public void convertRouteAction_timeoutByMaxStreamDuration() {
433+
io.envoyproxy.envoy.config.route.v3.RouteAction proto =
408434
io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder()
409-
.setClusterHeader("cluster-bar")
435+
.setCluster("cluster-foo")
436+
.setMaxStreamDuration(
437+
MaxStreamDuration.newBuilder()
438+
.setMaxStreamDuration(Durations.fromSeconds(5L)))
410439
.build();
411-
StructOrError<RouteAction> struct4 = RouteAction.fromEnvoyProtoRouteAction(proto4);
412-
assertThat(struct4).isNull();
440+
StructOrError<RouteAction> struct = RouteAction.fromEnvoyProtoRouteAction(proto);
441+
assertThat(struct.getStruct().getTimeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L));
442+
}
413443

414-
// cluster_specifier = weighted_cluster
415-
io.envoyproxy.envoy.config.route.v3.RouteAction proto5 =
444+
@Test
445+
public void convertRouteAction_timeoutUnset() {
446+
io.envoyproxy.envoy.config.route.v3.RouteAction proto =
416447
io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder()
417-
.setMaxGrpcTimeout(Durations.fromSeconds(6L))
418-
.setTimeout(Durations.fromMicros(20L))
419-
.setWeightedClusters(
420-
WeightedCluster.newBuilder()
421-
.addClusters(
422-
WeightedCluster.ClusterWeight
423-
.newBuilder()
424-
.setName("cluster-baz")
425-
.setWeight(UInt32Value.newBuilder().setValue(100))))
448+
.setCluster("cluster-foo")
426449
.build();
427-
StructOrError<RouteAction> struct5 = RouteAction.fromEnvoyProtoRouteAction(proto5);
428-
assertThat(struct5.getErrorDetail()).isNull();
429-
assertThat(struct5.getStruct().getTimeoutNano())
430-
.isEqualTo(TimeUnit.SECONDS.toNanos(6L));
431-
assertThat(struct5.getStruct().getCluster()).isNull();
432-
assertThat(struct5.getStruct().getWeightedCluster())
433-
.containsExactly(new ClusterWeight("cluster-baz", 100));
434-
435-
// cluster_specifier unset
436-
io.envoyproxy.envoy.config.route.v3.RouteAction unsetProto =
437-
io.envoyproxy.envoy.config.route.v3.RouteAction.getDefaultInstance();
438-
StructOrError<RouteAction> unsetStruct = RouteAction.fromEnvoyProtoRouteAction(unsetProto);
439-
assertThat(unsetStruct.getErrorDetail()).isNotNull();
440-
assertThat(unsetStruct.getStruct()).isNull();
450+
StructOrError<RouteAction> struct = RouteAction.fromEnvoyProtoRouteAction(proto);
451+
assertThat(struct.getStruct().getTimeoutNano()).isNull();
441452
}
442453

443454
@Test

0 commit comments

Comments
 (0)