Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions api/src/testFixtures/java/io/grpc/StatusMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*/
public final class StatusMatcher implements ArgumentMatcher<Status> {
public static StatusMatcher statusHasCode(ArgumentMatcher<Status.Code> codeMatcher) {
return new StatusMatcher(codeMatcher, null);
return new StatusMatcher(codeMatcher, null, null);
}

public static StatusMatcher statusHasCode(Status.Code code) {
Expand All @@ -35,17 +35,20 @@ public static StatusMatcher statusHasCode(Status.Code code) {

private final ArgumentMatcher<Status.Code> codeMatcher;
private final ArgumentMatcher<String> descriptionMatcher;
private final ArgumentMatcher<Throwable> causeMatcher;

private StatusMatcher(
ArgumentMatcher<Status.Code> codeMatcher,
ArgumentMatcher<String> descriptionMatcher) {
ArgumentMatcher<String> descriptionMatcher,
ArgumentMatcher<Throwable> causeMatcher) {
this.codeMatcher = checkNotNull(codeMatcher, "codeMatcher");
this.descriptionMatcher = descriptionMatcher;
this.causeMatcher = causeMatcher;
}

public StatusMatcher andDescription(ArgumentMatcher<String> descriptionMatcher) {
checkState(this.descriptionMatcher == null, "Already has a description matcher");
return new StatusMatcher(codeMatcher, descriptionMatcher);
return new StatusMatcher(codeMatcher, descriptionMatcher, causeMatcher);
}

public StatusMatcher andDescription(String description) {
Expand All @@ -56,11 +59,21 @@ public StatusMatcher andDescriptionContains(String substring) {
return andDescription(new StringContainsMatcher(substring));
}

public StatusMatcher andCause(ArgumentMatcher<Throwable> causeMatcher) {
checkState(this.causeMatcher == null, "Already has a cause matcher");
return new StatusMatcher(codeMatcher, descriptionMatcher, causeMatcher);
}

public StatusMatcher andCause(Throwable cause) {
return andCause(new EqualsMatcher<>(cause));
}

@Override
public boolean matches(Status status) {
return status != null
&& codeMatcher.matches(status.getCode())
&& (descriptionMatcher == null || descriptionMatcher.matches(status.getDescription()));
&& (descriptionMatcher == null || descriptionMatcher.matches(status.getDescription()))
&& (causeMatcher == null || causeMatcher.matches(status.getCause()));
}

@Override
Expand All @@ -72,6 +85,10 @@ public String toString() {
sb.append(", description=");
sb.append(descriptionMatcher);
}
if (causeMatcher != null) {
sb.append(", cause=");
sb.append(causeMatcher);
}
sb.append("}");
return sb.toString();
}
Expand Down
12 changes: 5 additions & 7 deletions xds/src/main/java/io/grpc/xds/XdsDependencyManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.grpc.Status;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.RetryingNameResolver;
import io.grpc.xds.Endpoints.LocalityLbEndpoints;
import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight;
Expand Down Expand Up @@ -652,13 +653,10 @@ public void onResourceChanged(StatusOr<T> update) {
data = update;
subscribeToChildren(update.getValue());
} else {
Status status = update.getStatus();
Status translatedStatus = Status.UNAVAILABLE.withDescription(
String.format("Error retrieving %s: %s. Details: %s%s",
toContextString(),
status.getCode(),
status.getDescription() != null ? status.getDescription() : "",
nodeInfo()));
Status translatedStatus = GrpcUtil.statusWithDetails(
Status.Code.UNAVAILABLE,
"Error retrieving " + toContextString() + nodeInfo(),
update.getStatus());

data = StatusOr.fromStatus(translatedStatus);
}
Expand Down
17 changes: 9 additions & 8 deletions xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ public void nonAggregateCluster_resourceNotExist_returnErrorPicker() {
startXdsDepManager();
verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. "
+ "Details: Timed out waiting for resource " + CLUSTER
+ " from xDS server nodeID: " + NODE_ID;
String expectedDescription = "Error retrieving CDS resource " + CLUSTER
+ " nodeID: " + NODE_ID
+ ": NOT_FOUND: Timed out waiting for resource " + CLUSTER + " from xDS server";
Status unavailable = Status.UNAVAILABLE.withDescription(expectedDescription);
assertPickerStatus(pickerCaptor.getValue(), unavailable);
assertThat(childBalancers).isEmpty();
Expand Down Expand Up @@ -311,8 +311,9 @@ public void nonAggregateCluster_resourceRevoked() {
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of());

assertThat(childBalancer.shutdown).isTrue();
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. "
+ "Details: Resource " + CLUSTER + " does not exist nodeID: " + NODE_ID;
String expectedDescription = "Error retrieving CDS resource " + CLUSTER
+ " nodeID: " + NODE_ID
+ ": NOT_FOUND: Resource " + CLUSTER + " does not exist";
Status unavailable = Status.UNAVAILABLE.withDescription(expectedDescription);
verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
Expand Down Expand Up @@ -515,9 +516,9 @@ public void aggregateCluster_noNonAggregateClusterExits_returnErrorPicker() {

verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
String expectedDescription = "Error retrieving CDS resource " + cluster1 + ": NOT_FOUND. "
+ "Details: Timed out waiting for resource " + cluster1 + " from xDS server nodeID: "
+ NODE_ID;
String expectedDescription = "Error retrieving CDS resource " + cluster1
+ " nodeID: " + NODE_ID
+ ": NOT_FOUND: Timed out waiting for resource " + cluster1 + " from xDS server";
Status status = Status.UNAVAILABLE.withDescription(expectedDescription);
assertPickerStatus(pickerCaptor.getValue(), status);
assertThat(childBalancers).isEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,8 @@ public void onlyEdsClusters_resourceNeverExist_returnErrorPicker() {

verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. "
+ "Details: Timed out waiting for resource " + CLUSTER + " from xDS server nodeID: node-id";
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + " nodeID: node-id: "
+ "NOT_FOUND: Timed out waiting for resource " + CLUSTER + " from xDS server";
Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription);
assertPicker(pickerCaptor.getValue(), expectedError, null);
}
Expand All @@ -713,8 +713,8 @@ public void cdsMissing_handledDirectly() {
assertThat(childBalancers).hasSize(0); // no child LB policy created
verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. "
+ "Details: Timed out waiting for resource " + CLUSTER + " from xDS server nodeID: node-id";
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + " nodeID: node-id: "
+ "NOT_FOUND: Timed out waiting for resource " + CLUSTER + " from xDS server";
Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription);
assertPicker(pickerCaptor.getValue(), expectedError, null);
assertPicker(pickerCaptor.getValue(), expectedError, null);
Expand Down Expand Up @@ -744,8 +744,8 @@ public void cdsRevoked_handledDirectly() {
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, ImmutableMap.of());
verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + ": NOT_FOUND. "
+ "Details: Resource " + CLUSTER + " does not exist nodeID: node-id";
String expectedDescription = "Error retrieving CDS resource " + CLUSTER + " nodeID: node-id: "
+ "NOT_FOUND: Resource " + CLUSTER + " does not exist";
Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription);
assertPicker(pickerCaptor.getValue(), expectedError, null);
assertThat(childBalancer.shutdown).isTrue();
Expand All @@ -760,8 +760,8 @@ public void edsMissing_failsRpcs() {
verify(helper).updateBalancingState(
eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
String expectedDescription = "Error retrieving EDS resource " + EDS_SERVICE_NAME
+ ": NOT_FOUND. Details: Timed out waiting for resource " + EDS_SERVICE_NAME
+ " from xDS server nodeID: node-id";
+ " nodeID: node-id: "
+ "NOT_FOUND: Timed out waiting for resource " + EDS_SERVICE_NAME + " from xDS server";
Status expectedError = Status.UNAVAILABLE.withDescription(expectedDescription);
assertPicker(pickerCaptor.getValue(), expectedError, null);
}
Expand Down
50 changes: 50 additions & 0 deletions xds/src/test/java/io/grpc/xds/FailingClientInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds;

import static java.util.Objects.requireNonNull;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.NoopClientCall;
import io.grpc.Status;

/**
* An interceptor that fails all RPCs with the provided status.
*/
final class FailingClientInterceptor implements ClientInterceptor {
private final Status status;

public FailingClientInterceptor(Status status) {
this.status = requireNonNull(status, "status");
}

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new NoopClientCall<ReqT, RespT>() {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
responseListener.onClose(status, new Metadata());
}
};
}
}
26 changes: 26 additions & 0 deletions xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,32 @@ public void testTcpListenerErrors() {
testWatcher.verifyStats(0, 1);
}

@Test
public void testControlPlaneError() {
Status forcedStatus = Status.NOT_FOUND
.withDescription("expected")
.withCause(new IllegalArgumentException("a random exception"));
xdsClient.shutdown();
xdsClient = XdsTestUtils.createXdsClient(
Collections.singletonList("control-plane"),
serverInfo -> new GrpcXdsTransportFactory.GrpcXdsTransport(
InProcessChannelBuilder.forName(serverInfo.target())
.directExecutor()
.intercept(new FailingClientInterceptor(forcedStatus))
.build()),
fakeClock);
xdsDependencyManager = new XdsDependencyManager(
xdsClient, syncContext, serverName, serverName, nameResolverArgs);
xdsDependencyManager.start(xdsConfigWatcher);

verify(xdsConfigWatcher).onUpdate(
argThat(StatusOrMatcher.hasStatus(
statusHasCode(Status.Code.UNAVAILABLE)
.andDescriptionContains(forcedStatus.getDescription())
.andCause(forcedStatus.getCause()))));
testWatcher.verifyStats(0, 1);
}

@Test
public void testMissingRds() {
String rdsName = "badRdsName";
Expand Down
Loading