Skip to content

Commit 501ca8f

Browse files
authored
xds: Update logic so that an error being reported when stream is closed gets propagated to subscribers (#9827)
* Stop setting waitForReady in XdsClient's AbstractXdsClient. * Handle bad URL cleanly. Fix test cases to deal with asynchronous flow.
1 parent b0635fa commit 501ca8f

File tree

3 files changed

+104
-12
lines changed

3 files changed

+104
-12
lines changed

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

+17-6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
* the xDS RPC stream.
6262
*/
6363
final class AbstractXdsClient {
64+
65+
public static final String CLOSED_BY_SERVER = "Closed by server";
6466
private final SynchronizationContext syncContext;
6567
private final InternalLogId logId;
6668
private final XdsLogger logger;
@@ -217,6 +219,11 @@ void readyHandler() {
217219
return;
218220
}
219221

222+
if (isInBackoff()) {
223+
rpcRetryTimer.cancel();
224+
rpcRetryTimer = null;
225+
}
226+
220227
timerLaunch.startSubscriberTimersIfNeeded(serverInfo);
221228
}
222229

@@ -315,21 +322,25 @@ final void handleRpcError(Throwable t) {
315322
}
316323

317324
final void handleRpcCompleted() {
318-
handleRpcStreamClosed(Status.UNAVAILABLE.withDescription("Closed by server"));
325+
handleRpcStreamClosed(Status.UNAVAILABLE.withDescription(CLOSED_BY_SERVER));
319326
}
320327

321328
private void handleRpcStreamClosed(Status error) {
322-
checkArgument(!error.isOk(), "unexpected OK status");
323329
if (closed) {
324330
return;
325331
}
332+
333+
checkArgument(!error.isOk(), "unexpected OK status");
334+
String errorMsg = error.getDescription() != null
335+
&& error.getDescription().equals(CLOSED_BY_SERVER)
336+
? "ADS stream closed with status {0}: {1}. Cause: {2}"
337+
: "ADS stream failed with status {0}: {1}. Cause: {2}";
326338
logger.log(
327-
XdsLogLevel.ERROR,
328-
"ADS stream closed with status {0}: {1}. Cause: {2}",
329-
error.getCode(), error.getDescription(), error.getCause());
339+
XdsLogLevel.ERROR, errorMsg, error.getCode(), error.getDescription(), error.getCause());
330340
closed = true;
331341
xdsResponseHandler.handleStreamClosed(error);
332342
cleanUp();
343+
333344
if (responseReceived || retryBackoffPolicy == null) {
334345
// Reset the backoff sequence if had received a response, or backoff sequence
335346
// has never been initialized.
@@ -423,7 +434,7 @@ public void run() {
423434
});
424435
}
425436
};
426-
requestWriter = stub.withWaitForReady().streamAggregatedResources(responseReader);
437+
requestWriter = stub.streamAggregatedResources(responseReader);
427438
}
428439

429440
@Override

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -514,11 +514,22 @@ private final class ResourceSubscriber<T extends ResourceUpdate> {
514514
// Initialize metadata in UNKNOWN state to cover the case when resource subscriber,
515515
// is created but not yet requested because the client is in backoff.
516516
this.metadata = ResourceMetadata.newResourceMetadataUnknown();
517-
maybeCreateXdsChannelWithLrs(serverInfo);
518-
this.xdsChannel = serverChannelMap.get(serverInfo);
519-
if (xdsChannel.isInBackoff()) {
517+
518+
AbstractXdsClient xdsChannelTemp = null;
519+
try {
520+
maybeCreateXdsChannelWithLrs(serverInfo);
521+
xdsChannelTemp = serverChannelMap.get(serverInfo);
522+
if (xdsChannelTemp.isInBackoff()) {
523+
return;
524+
}
525+
} catch (IllegalArgumentException e) {
526+
xdsChannelTemp = null;
527+
this.errorDescription = "Bad configuration: " + e.getMessage();
520528
return;
529+
} finally {
530+
this.xdsChannel = xdsChannelTemp;
521531
}
532+
522533
restartTimer();
523534
}
524535

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

+73-3
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.xds.XdsClientImpl.XdsChannelFactory.DEFAULT_XDS_CHANNEL_FACTORY;
2122
import static org.mockito.ArgumentMatchers.any;
2223
import static org.mockito.ArgumentMatchers.isA;
2324
import static org.mockito.Mockito.mock;
@@ -70,6 +71,7 @@
7071
import io.grpc.stub.StreamObserver;
7172
import io.grpc.testing.GrpcCleanupRule;
7273
import io.grpc.xds.Bootstrapper.AuthorityInfo;
74+
import io.grpc.xds.Bootstrapper.BootstrapInfo;
7375
import io.grpc.xds.Bootstrapper.CertificateProviderInfo;
7476
import io.grpc.xds.Bootstrapper.ServerInfo;
7577
import io.grpc.xds.Endpoints.DropOverload;
@@ -114,6 +116,7 @@
114116
import org.junit.runner.RunWith;
115117
import org.junit.runners.JUnit4;
116118
import org.mockito.ArgumentCaptor;
119+
import org.mockito.ArgumentMatchers;
117120
import org.mockito.Captor;
118121
import org.mockito.InOrder;
119122
import org.mockito.Mock;
@@ -3226,7 +3229,8 @@ public void streamClosedAndRetryWithBackoff() {
32263229

32273230
// Management server closes the RPC stream with an error.
32283231
call.sendError(Status.UNKNOWN.asException());
3229-
verify(ldsResourceWatcher).onError(errorCaptor.capture());
3232+
verify(ldsResourceWatcher, Mockito.timeout(1000).times(1))
3233+
.onError(errorCaptor.capture());
32303234
verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, "");
32313235
verify(rdsResourceWatcher).onError(errorCaptor.capture());
32323236
verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNKNOWN, "");
@@ -3336,7 +3340,8 @@ public void streamClosedAndRetryRaceWithAddRemoveWatchers() {
33363340
RDS_RESOURCE, rdsResourceWatcher);
33373341
DiscoveryRpcCall call = resourceDiscoveryCalls.poll();
33383342
call.sendError(Status.UNAVAILABLE.asException());
3339-
verify(ldsResourceWatcher).onError(errorCaptor.capture());
3343+
verify(ldsResourceWatcher, Mockito.timeout(1000).times(1))
3344+
.onError(errorCaptor.capture());
33403345
verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, "");
33413346
verify(rdsResourceWatcher).onError(errorCaptor.capture());
33423347
verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, "");
@@ -3573,13 +3578,18 @@ public void sendingToStoppedServer() throws Exception {
35733578
.build()
35743579
.start());
35753580
fakeClock.forwardTime(5, TimeUnit.SECONDS);
3581+
verify(ldsResourceWatcher, never()).onResourceDoesNotExist(LDS_RESOURCE);
3582+
fakeClock.forwardTime(20, TimeUnit.SECONDS); // Trigger rpcRetryTimer
35763583
DiscoveryRpcCall call = resourceDiscoveryCalls.poll(3, TimeUnit.SECONDS);
3584+
if (call == null) { // The first rpcRetry may have happened before the channel was ready
3585+
fakeClock.forwardTime(50, TimeUnit.SECONDS);
3586+
call = resourceDiscoveryCalls.poll(3, TimeUnit.SECONDS);
3587+
}
35773588

35783589
// NOTE: There is a ScheduledExecutorService that may get involved due to the reconnect
35793590
// so you cannot rely on the logic being single threaded. The timeout() in verifyRequest
35803591
// is therefore necessary to avoid flakiness.
35813592
// Send a response and do verifications
3582-
verify(ldsResourceWatcher, never()).onResourceDoesNotExist(LDS_RESOURCE);
35833593
call.sendResponse(LDS, mf.buildWrappedResource(testListenerVhosts), VERSION_1, "0001");
35843594
call.verifyRequest(LDS, LDS_RESOURCE, VERSION_1, "0001", NODE);
35853595
verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture());
@@ -3592,6 +3602,66 @@ public void sendingToStoppedServer() throws Exception {
35923602
}
35933603
}
35943604

3605+
@Test
3606+
public void sendToBadUrl() throws Exception {
3607+
// Setup xdsClient to fail on stream creation
3608+
XdsClientImpl client = createXdsClient("some. garbage");
3609+
3610+
client.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher);
3611+
fakeClock.forwardTime(20, TimeUnit.SECONDS);
3612+
verify(ldsResourceWatcher, Mockito.timeout(5000).times(1)).onError(ArgumentMatchers.any());
3613+
client.shutdown();
3614+
}
3615+
3616+
@Test
3617+
public void sendToNonexistentHost() throws Exception {
3618+
// Setup xdsClient to fail on stream creation
3619+
XdsClientImpl client = createXdsClient("some.garbage");
3620+
client.watchXdsResource(XdsListenerResource.getInstance(), LDS_RESOURCE, ldsResourceWatcher);
3621+
fakeClock.forwardTime(20, TimeUnit.SECONDS);
3622+
3623+
verify(ldsResourceWatcher, Mockito.timeout(5000).times(1)).onError(ArgumentMatchers.any());
3624+
fakeClock.forwardTime(50, TimeUnit.SECONDS); // Trigger rpcRetry if appropriate
3625+
assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty();
3626+
client.shutdown();
3627+
}
3628+
3629+
private XdsClientImpl createXdsClient(String serverUri) {
3630+
BootstrapInfo bootstrapInfo = buildBootStrap(serverUri);
3631+
return new XdsClientImpl(
3632+
DEFAULT_XDS_CHANNEL_FACTORY,
3633+
bootstrapInfo,
3634+
Context.ROOT,
3635+
fakeClock.getScheduledExecutorService(),
3636+
backoffPolicyProvider,
3637+
fakeClock.getStopwatchSupplier(),
3638+
timeProvider,
3639+
tlsContextManager);
3640+
}
3641+
3642+
private BootstrapInfo buildBootStrap(String serverUri) {
3643+
3644+
ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS,
3645+
ignoreResourceDeletion());
3646+
3647+
return Bootstrapper.BootstrapInfo.builder()
3648+
.servers(Collections.singletonList(xdsServerInfo))
3649+
.node(NODE)
3650+
.authorities(ImmutableMap.of(
3651+
"authority.xds.com",
3652+
AuthorityInfo.create(
3653+
"xdstp://authority.xds.com/envoy.config.listener.v3.Listener/%s",
3654+
ImmutableList.of(Bootstrapper.ServerInfo.create(
3655+
SERVER_URI_CUSTOME_AUTHORITY, CHANNEL_CREDENTIALS))),
3656+
"",
3657+
AuthorityInfo.create(
3658+
"xdstp:///envoy.config.listener.v3.Listener/%s",
3659+
ImmutableList.of(Bootstrapper.ServerInfo.create(
3660+
SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS)))))
3661+
.certProviders(ImmutableMap.of("cert-instance-name",
3662+
CertificateProviderInfo.create("file-watcher", ImmutableMap.<String, Object>of())))
3663+
.build();
3664+
}
35953665

35963666
private <T extends ResourceUpdate> DiscoveryRpcCall startResourceWatcher(
35973667
XdsResourceType<T> type, String name, ResourceWatcher<T> watcher) {

0 commit comments

Comments
 (0)