Skip to content

Commit a535ed7

Browse files
committed
Catch Errors when calling complex parsing code
When we have involved parsing/validation logic, it would be easy to have bugs. There's little reason we can't handle those bugs, as the logic shouldn't be modifying global state. While the current config is bad or trigger a bug, we want to keep working until we get good/working config again. For XDS, we do have the problem of losing the exception's stack trace. We could consider increasing the log level, but we could also consider propagating the error to the listener. Let's skip figuring out that exact behavior for now, and just move a step in the right direction.
1 parent ebb9420 commit a535ed7

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

core/src/main/java/io/grpc/internal/ScParser.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,19 @@ public ConfigOrError parseServiceConfig(Map<String, ?> rawServiceConfig) {
6969
maxHedgedAttemptsLimit,
7070
loadBalancingPolicySelection));
7171
} catch (RuntimeException e) {
72+
// TODO(ejona): We really don't want parsers throwing exceptions; they should return an error.
73+
// However, right now ManagedChannelServiceConfig itself uses exceptions like
74+
// ClassCastException. We should handle those with a graceful return within
75+
// ManagedChannelServiceConfig and then get rid of this case. Then all exceptions are
76+
// "unexpected" and the INTERNAL status code makes it clear a bug needs to be fixed.
7277
return ConfigOrError.fromError(
7378
Status.UNKNOWN.withDescription("failed to parse service config").withCause(e));
79+
} catch (Throwable t) {
80+
// Even catch Errors, since broken config parsing could trigger AssertionError,
81+
// StackOverflowError, and other errors we can reasonably safely recover. Since the config
82+
// could be untrusted, we want to error on the side of recovering.
83+
return ConfigOrError.fromError(
84+
Status.INTERNAL.withDescription("Unexpected error parsing service config").withCause(t));
7485
}
7586
}
7687
}

core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3942,6 +3942,28 @@ public void nameResolverHelper_badConfigFails() {
39423942
assertThat(coe.getError().getCause()).isInstanceOf(ClassCastException.class);
39433943
}
39443944

3945+
@Test
3946+
public void nameResolverHelper_badParser_failsGracefully() {
3947+
boolean retryEnabled = false;
3948+
int maxRetryAttemptsLimit = 2;
3949+
int maxHedgedAttemptsLimit = 3;
3950+
3951+
Throwable t = new Error("really poor config parser");
3952+
when(mockLoadBalancerProvider.parseLoadBalancingPolicyConfig(any())).thenThrow(t);
3953+
ScParser parser = new ScParser(
3954+
retryEnabled,
3955+
maxRetryAttemptsLimit,
3956+
maxHedgedAttemptsLimit,
3957+
mockLoadBalancerProvider);
3958+
3959+
ConfigOrError coe = parser.parseServiceConfig(ImmutableMap.of());
3960+
3961+
assertThat(coe.getError()).isNotNull();
3962+
assertThat(coe.getError().getCode()).isEqualTo(Code.INTERNAL);
3963+
assertThat(coe.getError().getDescription()).contains("Unexpected error parsing service config");
3964+
assertThat(coe.getError().getCause()).isSameInstanceAs(t);
3965+
}
3966+
39453967
@Test
39463968
public void nameResolverHelper_noConfigChosen() {
39473969
boolean retryEnabled = false;

xds/src/main/java/io/grpc/xds/client/XdsResourceType.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@
3333
import java.util.List;
3434
import java.util.Map;
3535
import java.util.Set;
36+
import java.util.logging.Level;
37+
import java.util.logging.Logger;
3638
import javax.annotation.Nullable;
3739

3840
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10847")
3941
public abstract class XdsResourceType<T extends ResourceUpdate> {
42+
private static final Logger log = Logger.getLogger(XdsResourceType.class.getName());
43+
4044
static final String TYPE_URL_RESOURCE =
4145
"type.googleapis.com/envoy.service.discovery.v3.Resource";
4246
protected static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls";
@@ -176,6 +180,16 @@ ValidatedResourceUpdate<T> parse(Args args, List<Any> resources) {
176180
typeName(), unpackedClassName().getSimpleName(), cname, e.getMessage()));
177181
invalidResources.add(cname);
178182
continue;
183+
} catch (Throwable t) {
184+
log.log(Level.FINE, "Unexpected error in doParse()", t);
185+
String errorMessage = t.getClass().getSimpleName();
186+
if (t.getMessage() != null) {
187+
errorMessage = errorMessage + ": " + t.getMessage();
188+
}
189+
errors.add(String.format("%s response '%s' unexpected error: %s",
190+
typeName(), cname, errorMessage));
191+
invalidResources.add(cname);
192+
continue;
179193
}
180194

181195
// Resource parsed successfully.

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

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static com.google.common.truth.Truth.assertWithMessage;
21+
import static io.grpc.StatusMatcher.statusHasCode;
2122
import static org.mockito.ArgumentMatchers.any;
2223
import static org.mockito.ArgumentMatchers.argThat;
2324
import static org.mockito.ArgumentMatchers.eq;
@@ -42,6 +43,7 @@
4243
import com.google.protobuf.Duration;
4344
import com.google.protobuf.InvalidProtocolBufferException;
4445
import com.google.protobuf.Message;
46+
import com.google.protobuf.StringValue;
4547
import com.google.protobuf.UInt32Value;
4648
import com.google.protobuf.util.Durations;
4749
import io.envoyproxy.envoy.config.cluster.v3.OutlierDetection;
@@ -59,6 +61,7 @@
5961
import io.grpc.Status;
6062
import io.grpc.Status.Code;
6163
import io.grpc.StatusOr;
64+
import io.grpc.StatusOrMatcher;
6265
import io.grpc.inprocess.InProcessChannelBuilder;
6366
import io.grpc.inprocess.InProcessServerBuilder;
6467
import io.grpc.internal.BackoffPolicy;
@@ -111,6 +114,7 @@
111114
import java.util.Collections;
112115
import java.util.List;
113116
import java.util.Map;
117+
import java.util.Objects;
114118
import java.util.Queue;
115119
import java.util.concurrent.BlockingDeque;
116120
import java.util.concurrent.CountDownLatch;
@@ -278,6 +282,8 @@ public long currentTimeNanos() {
278282
@Mock
279283
private ResourceWatcher<EdsUpdate> edsResourceWatcher;
280284
@Mock
285+
private ResourceWatcher<StringUpdate> stringResourceWatcher;
286+
@Mock
281287
private XdsClientMetricReporter xdsClientMetricReporter;
282288
@Mock
283289
private ServerConnectionCallback serverConnectionCallback;
@@ -667,6 +673,58 @@ private void verifyServerConnection(int times, boolean isConnected, String xdsSe
667673
eq(xdsServer));
668674
}
669675

676+
@Test
677+
public void doParse_returnsSuccessfully() {
678+
XdsStringResource resourceType = new XdsStringResource();
679+
xdsClient.watchXdsResource(
680+
resourceType, "resource1", stringResourceWatcher, MoreExecutors.directExecutor());
681+
DiscoveryRpcCall call = resourceDiscoveryCalls.poll();
682+
683+
Any resource = Any.pack(StringValue.newBuilder().setValue("resource1").build());
684+
call.sendResponse(resourceType, resource, VERSION_1, "0000");
685+
verify(stringResourceWatcher).onResourceChanged(argThat(StatusOrMatcher.hasValue(
686+
(StringUpdate arg) -> new StringUpdate("resource1").equals(arg))));
687+
}
688+
689+
@Test
690+
public void doParse_throwsResourceInvalidException_resourceInvalid() {
691+
XdsStringResource resourceType = new XdsStringResource() {
692+
@Override
693+
protected StringUpdate doParse(Args args, Message unpackedMessage)
694+
throws ResourceInvalidException {
695+
throw new ResourceInvalidException("some bad input");
696+
}
697+
};
698+
xdsClient.watchXdsResource(
699+
resourceType, "resource1", stringResourceWatcher, MoreExecutors.directExecutor());
700+
DiscoveryRpcCall call = resourceDiscoveryCalls.poll();
701+
702+
Any resource = Any.pack(StringValue.newBuilder().setValue("resource1").build());
703+
call.sendResponse(resourceType, resource, VERSION_1, "0000");
704+
verify(stringResourceWatcher).onResourceChanged(argThat(StatusOrMatcher.hasStatus(
705+
statusHasCode(Status.Code.UNAVAILABLE)
706+
.andDescriptionContains("validation error: some bad input"))));
707+
}
708+
709+
@Test
710+
public void doParse_throwsError_resourceInvalid() throws Exception {
711+
XdsStringResource resourceType = new XdsStringResource() {
712+
@Override
713+
protected StringUpdate doParse(Args args, Message unpackedMessage) {
714+
throw new AssertionError("something bad happened");
715+
}
716+
};
717+
xdsClient.watchXdsResource(
718+
resourceType, "resource1", stringResourceWatcher, MoreExecutors.directExecutor());
719+
DiscoveryRpcCall call = resourceDiscoveryCalls.poll();
720+
721+
Any resource = Any.pack(StringValue.newBuilder().setValue("resource1").build());
722+
call.sendResponse(resourceType, resource, VERSION_1, "0000");
723+
verify(stringResourceWatcher).onResourceChanged(argThat(StatusOrMatcher.hasStatus(
724+
statusHasCode(Status.Code.UNAVAILABLE)
725+
.andDescriptionContains("unexpected error: AssertionError: something bad happened"))));
726+
}
727+
670728
@Test
671729
public void ldsResourceNotFound() {
672730
DiscoveryRpcCall call = startResourceWatcher(XdsListenerResource.getInstance(), LDS_RESOURCE,
@@ -5318,4 +5376,70 @@ protected abstract Message buildHttpConnectionManagerFilter(
53185376

53195377
protected abstract Message buildTerminalFilter();
53205378
}
5379+
5380+
private static class XdsStringResource extends XdsResourceType<StringUpdate> {
5381+
@Override
5382+
@SuppressWarnings("unchecked")
5383+
protected Class<? extends com.google.protobuf.Message> unpackedClassName() {
5384+
return StringValue.class;
5385+
}
5386+
5387+
@Override
5388+
public String typeName() {
5389+
return "EMPTY";
5390+
}
5391+
5392+
@Override
5393+
public String typeUrl() {
5394+
return "type.googleapis.com/google.protobuf.StringValue";
5395+
}
5396+
5397+
@Override
5398+
public boolean shouldRetrieveResourceKeysForArgs() {
5399+
return false;
5400+
}
5401+
5402+
@Override
5403+
protected boolean isFullStateOfTheWorld() {
5404+
return false;
5405+
}
5406+
5407+
@Override
5408+
@Nullable
5409+
protected String extractResourceName(Message unpackedResource) {
5410+
if (!(unpackedResource instanceof StringValue)) {
5411+
return null;
5412+
}
5413+
return ((StringValue) unpackedResource).getValue();
5414+
}
5415+
5416+
@Override
5417+
protected StringUpdate doParse(Args args, Message unpackedMessage)
5418+
throws ResourceInvalidException {
5419+
return new StringUpdate(((StringValue) unpackedMessage).getValue());
5420+
}
5421+
}
5422+
5423+
private static final class StringUpdate implements ResourceUpdate {
5424+
@SuppressWarnings("UnusedVariable")
5425+
public final String value;
5426+
5427+
public StringUpdate(String value) {
5428+
this.value = value;
5429+
}
5430+
5431+
@Override
5432+
public boolean equals(Object o) {
5433+
if (!(o instanceof StringUpdate)) {
5434+
return false;
5435+
}
5436+
StringUpdate that = (StringUpdate) o;
5437+
return Objects.equals(this.value, that.value);
5438+
}
5439+
5440+
@Override
5441+
public int hashCode() {
5442+
return Objects.hash(value);
5443+
}
5444+
}
53215445
}

0 commit comments

Comments
 (0)