mirror of https://github.com/grpc/grpc-java.git
This change has these main aspects to it:
1. Removal of any name resolution responsibility from ManagedChannelImpl
2. Creation of a new RetryScheduler to own generic retry logic
- Can also be used outside the name resolution context
3. Creation of a new RetryingNameScheduler that can be used to wrap any
polling name resolver to add retry capability
4. A new facility in NameResolver to allow implementations to notify
listeners on the success of name resolution attempts
- RetryingNameScheduler relies on this
This commit is contained in:
parent
5a2adcc7c6
commit
fcb5c54e4b
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.internal;
|
||||||
|
|
||||||
|
import io.grpc.SynchronizationContext;
|
||||||
|
import io.grpc.SynchronizationContext.ScheduledHandle;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a retry operation according to a {@link BackoffPolicy}. The retry is run within a
|
||||||
|
* {@link SynchronizationContext}. At most one retry is scheduled at a time.
|
||||||
|
*/
|
||||||
|
final class BackoffPolicyRetryScheduler implements RetryScheduler {
|
||||||
|
private final ScheduledExecutorService scheduledExecutorService;
|
||||||
|
private final SynchronizationContext syncContext;
|
||||||
|
private final BackoffPolicy.Provider policyProvider;
|
||||||
|
|
||||||
|
private BackoffPolicy policy;
|
||||||
|
private ScheduledHandle scheduledHandle;
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(
|
||||||
|
BackoffPolicyRetryScheduler.class.getName());
|
||||||
|
|
||||||
|
BackoffPolicyRetryScheduler(BackoffPolicy.Provider policyProvider,
|
||||||
|
ScheduledExecutorService scheduledExecutorService,
|
||||||
|
SynchronizationContext syncContext) {
|
||||||
|
this.policyProvider = policyProvider;
|
||||||
|
this.scheduledExecutorService = scheduledExecutorService;
|
||||||
|
this.syncContext = syncContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a future retry operation. Only allows one retry to be scheduled at any given time.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void schedule(Runnable retryOperation) {
|
||||||
|
syncContext.throwIfNotInThisSynchronizationContext();
|
||||||
|
|
||||||
|
if (policy == null) {
|
||||||
|
policy = policyProvider.get();
|
||||||
|
}
|
||||||
|
// If a retry is already scheduled, take no further action.
|
||||||
|
if (scheduledHandle != null && scheduledHandle.isPending()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long delayNanos = policy.nextBackoffNanos();
|
||||||
|
scheduledHandle = syncContext.schedule(retryOperation, delayNanos, TimeUnit.NANOSECONDS,
|
||||||
|
scheduledExecutorService);
|
||||||
|
logger.log(Level.FINE, "Scheduling DNS resolution backoff for {0}ns", delayNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the {@link BackoffPolicyRetryScheduler} and cancels any pending retry task. The policy
|
||||||
|
* will be cleared thus also resetting any state associated with it (e.g. a backoff multiplier).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
syncContext.throwIfNotInThisSynchronizationContext();
|
||||||
|
|
||||||
|
syncContext.execute(() -> {
|
||||||
|
if (scheduledHandle != null && scheduledHandle.isPending()) {
|
||||||
|
scheduledHandle.cancel();
|
||||||
|
}
|
||||||
|
policy = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -47,19 +47,25 @@ public final class DnsNameResolverProvider extends NameResolverProvider {
|
||||||
private static final String SCHEME = "dns";
|
private static final String SCHEME = "dns";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DnsNameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
|
public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
|
||||||
if (SCHEME.equals(targetUri.getScheme())) {
|
if (SCHEME.equals(targetUri.getScheme())) {
|
||||||
String targetPath = Preconditions.checkNotNull(targetUri.getPath(), "targetPath");
|
String targetPath = Preconditions.checkNotNull(targetUri.getPath(), "targetPath");
|
||||||
Preconditions.checkArgument(targetPath.startsWith("/"),
|
Preconditions.checkArgument(targetPath.startsWith("/"),
|
||||||
"the path component (%s) of the target (%s) must start with '/'", targetPath, targetUri);
|
"the path component (%s) of the target (%s) must start with '/'", targetPath, targetUri);
|
||||||
String name = targetPath.substring(1);
|
String name = targetPath.substring(1);
|
||||||
return new DnsNameResolver(
|
return new RetryingNameResolver(
|
||||||
targetUri.getAuthority(),
|
new DnsNameResolver(
|
||||||
name,
|
targetUri.getAuthority(),
|
||||||
args,
|
name,
|
||||||
GrpcUtil.SHARED_CHANNEL_EXECUTOR,
|
args,
|
||||||
Stopwatch.createUnstarted(),
|
GrpcUtil.SHARED_CHANNEL_EXECUTOR,
|
||||||
InternalServiceProviders.isAndroid(getClass().getClassLoader()));
|
Stopwatch.createUnstarted(),
|
||||||
|
InternalServiceProviders.isAndroid(getClass().getClassLoader())),
|
||||||
|
new BackoffPolicyRetryScheduler(
|
||||||
|
new ExponentialBackoffPolicy.Provider(),
|
||||||
|
args.getScheduledExecutorService(),
|
||||||
|
args.getSynchronizationContext()),
|
||||||
|
args.getSynchronizationContext());
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ import io.grpc.internal.ManagedChannelServiceConfig.MethodInfo;
|
||||||
import io.grpc.internal.ManagedChannelServiceConfig.ServiceConfigConvertedSelector;
|
import io.grpc.internal.ManagedChannelServiceConfig.ServiceConfigConvertedSelector;
|
||||||
import io.grpc.internal.RetriableStream.ChannelBufferMeter;
|
import io.grpc.internal.RetriableStream.ChannelBufferMeter;
|
||||||
import io.grpc.internal.RetriableStream.Throttle;
|
import io.grpc.internal.RetriableStream.Throttle;
|
||||||
|
import io.grpc.internal.RetryingNameResolver.ResolutionResultListener;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -367,7 +368,6 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
checkState(lbHelper != null, "lbHelper is null");
|
checkState(lbHelper != null, "lbHelper is null");
|
||||||
}
|
}
|
||||||
if (nameResolver != null) {
|
if (nameResolver != null) {
|
||||||
cancelNameResolverBackoff();
|
|
||||||
nameResolver.shutdown();
|
nameResolver.shutdown();
|
||||||
nameResolverStarted = false;
|
nameResolverStarted = false;
|
||||||
if (channelIsActive) {
|
if (channelIsActive) {
|
||||||
|
|
@ -450,42 +450,10 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
idleTimer.reschedule(idleTimeoutMillis, TimeUnit.MILLISECONDS);
|
idleTimer.reschedule(idleTimeoutMillis, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run from syncContext
|
|
||||||
@VisibleForTesting
|
|
||||||
class DelayedNameResolverRefresh implements Runnable {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
scheduledNameResolverRefresh = null;
|
|
||||||
refreshNameResolution();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be used from syncContext
|
|
||||||
@Nullable private ScheduledHandle scheduledNameResolverRefresh;
|
|
||||||
// The policy to control backoff between name resolution attempts. Non-null when an attempt is
|
|
||||||
// scheduled. Must be used from syncContext
|
|
||||||
@Nullable private BackoffPolicy nameResolverBackoffPolicy;
|
|
||||||
|
|
||||||
// Must be run from syncContext
|
|
||||||
private void cancelNameResolverBackoff() {
|
|
||||||
syncContext.throwIfNotInThisSynchronizationContext();
|
|
||||||
if (scheduledNameResolverRefresh != null) {
|
|
||||||
scheduledNameResolverRefresh.cancel();
|
|
||||||
scheduledNameResolverRefresh = null;
|
|
||||||
nameResolverBackoffPolicy = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force name resolution refresh to happen immediately and reset refresh back-off. Must be run
|
* Force name resolution refresh to happen immediately. Must be run
|
||||||
* from syncContext.
|
* from syncContext.
|
||||||
*/
|
*/
|
||||||
private void refreshAndResetNameResolution() {
|
|
||||||
syncContext.throwIfNotInThisSynchronizationContext();
|
|
||||||
cancelNameResolverBackoff();
|
|
||||||
refreshNameResolution();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshNameResolution() {
|
private void refreshNameResolution() {
|
||||||
syncContext.throwIfNotInThisSynchronizationContext();
|
syncContext.throwIfNotInThisSynchronizationContext();
|
||||||
if (nameResolverStarted) {
|
if (nameResolverStarted) {
|
||||||
|
|
@ -783,7 +751,24 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
if (overrideAuthority == null) {
|
if (overrideAuthority == null) {
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
return new ForwardingNameResolver(resolver) {
|
|
||||||
|
// If the nameResolver is not already a RetryingNameResolver, then wrap it with it.
|
||||||
|
// This helps guarantee that name resolution retry remains supported even as it has been
|
||||||
|
// removed from ManagedChannelImpl.
|
||||||
|
// TODO: After a transition period, all NameResolver implementations that need retry should use
|
||||||
|
// RetryingNameResolver directly and this step can be removed.
|
||||||
|
NameResolver usedNameResolver;
|
||||||
|
if (resolver instanceof RetryingNameResolver) {
|
||||||
|
usedNameResolver = resolver;
|
||||||
|
} else {
|
||||||
|
usedNameResolver = new RetryingNameResolver(resolver,
|
||||||
|
new BackoffPolicyRetryScheduler(new ExponentialBackoffPolicy.Provider(),
|
||||||
|
nameResolverArgs.getScheduledExecutorService(),
|
||||||
|
nameResolverArgs.getSynchronizationContext()),
|
||||||
|
nameResolverArgs.getSynchronizationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ForwardingNameResolver(usedNameResolver) {
|
||||||
@Override
|
@Override
|
||||||
public String getServiceAuthority() {
|
public String getServiceAuthority() {
|
||||||
return overrideAuthority;
|
return overrideAuthority;
|
||||||
|
|
@ -1291,7 +1276,7 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
// Must be called from syncContext
|
// Must be called from syncContext
|
||||||
private void handleInternalSubchannelState(ConnectivityStateInfo newState) {
|
private void handleInternalSubchannelState(ConnectivityStateInfo newState) {
|
||||||
if (newState.getState() == TRANSIENT_FAILURE || newState.getState() == IDLE) {
|
if (newState.getState() == TRANSIENT_FAILURE || newState.getState() == IDLE) {
|
||||||
refreshAndResetNameResolution();
|
refreshNameResolution();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1338,9 +1323,8 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
if (shutdown.get()) {
|
if (shutdown.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (scheduledNameResolverRefresh != null && scheduledNameResolverRefresh.isPending()) {
|
if (nameResolverStarted) {
|
||||||
checkState(nameResolverStarted, "name resolver must be started");
|
refreshNameResolution();
|
||||||
refreshAndResetNameResolution();
|
|
||||||
}
|
}
|
||||||
for (InternalSubchannel subchannel : subchannels) {
|
for (InternalSubchannel subchannel : subchannels) {
|
||||||
subchannel.resetConnectBackoff();
|
subchannel.resetConnectBackoff();
|
||||||
|
|
@ -1496,7 +1480,7 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
final class LoadBalancerRefreshNameResolution implements Runnable {
|
final class LoadBalancerRefreshNameResolution implements Runnable {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
refreshAndResetNameResolution();
|
ManagedChannelImpl.this.refreshNameResolution();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1727,7 +1711,7 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class NameResolverListener extends NameResolver.Listener2 {
|
final class NameResolverListener extends NameResolver.Listener2 {
|
||||||
final LbHelperImpl helper;
|
final LbHelperImpl helper;
|
||||||
final NameResolver resolver;
|
final NameResolver resolver;
|
||||||
|
|
||||||
|
|
@ -1759,8 +1743,9 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
lastResolutionState = ResolutionState.SUCCESS;
|
lastResolutionState = ResolutionState.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
nameResolverBackoffPolicy = null;
|
|
||||||
ConfigOrError configOrError = resolutionResult.getServiceConfig();
|
ConfigOrError configOrError = resolutionResult.getServiceConfig();
|
||||||
|
ResolutionResultListener resolutionResultListener = resolutionResult.getAttributes()
|
||||||
|
.get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY);
|
||||||
InternalConfigSelector resolvedConfigSelector =
|
InternalConfigSelector resolvedConfigSelector =
|
||||||
resolutionResult.getAttributes().get(InternalConfigSelector.KEY);
|
resolutionResult.getAttributes().get(InternalConfigSelector.KEY);
|
||||||
ManagedChannelServiceConfig validServiceConfig =
|
ManagedChannelServiceConfig validServiceConfig =
|
||||||
|
|
@ -1817,6 +1802,9 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
// we later check for these error codes when investigating pick results in
|
// we later check for these error codes when investigating pick results in
|
||||||
// GrpcUtil.getTransportFromPickResult().
|
// GrpcUtil.getTransportFromPickResult().
|
||||||
onError(configOrError.getError());
|
onError(configOrError.getError());
|
||||||
|
if (resolutionResultListener != null) {
|
||||||
|
resolutionResultListener.resolutionAttempted(false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
effectiveServiceConfig = lastServiceConfig;
|
effectiveServiceConfig = lastServiceConfig;
|
||||||
|
|
@ -1861,15 +1849,15 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
}
|
}
|
||||||
Attributes attributes = attrBuilder.build();
|
Attributes attributes = attrBuilder.build();
|
||||||
|
|
||||||
boolean addressesAccepted = helper.lb.tryAcceptResolvedAddresses(
|
boolean lastAddressesAccepted = helper.lb.tryAcceptResolvedAddresses(
|
||||||
ResolvedAddresses.newBuilder()
|
ResolvedAddresses.newBuilder()
|
||||||
.setAddresses(servers)
|
.setAddresses(servers)
|
||||||
.setAttributes(attributes)
|
.setAttributes(attributes)
|
||||||
.setLoadBalancingPolicyConfig(effectiveServiceConfig.getLoadBalancingConfig())
|
.setLoadBalancingPolicyConfig(effectiveServiceConfig.getLoadBalancingConfig())
|
||||||
.build());
|
.build());
|
||||||
|
// If a listener is provided, let it know if the addresses were accepted.
|
||||||
if (!addressesAccepted) {
|
if (resolutionResultListener != null) {
|
||||||
scheduleExponentialBackOffInSyncContext();
|
resolutionResultListener.resolutionAttempted(lastAddressesAccepted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1905,29 +1893,6 @@ final class ManagedChannelImpl extends ManagedChannel implements
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.lb.handleNameResolutionError(error);
|
helper.lb.handleNameResolutionError(error);
|
||||||
|
|
||||||
scheduleExponentialBackOffInSyncContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scheduleExponentialBackOffInSyncContext() {
|
|
||||||
if (scheduledNameResolverRefresh != null && scheduledNameResolverRefresh.isPending()) {
|
|
||||||
// The name resolver may invoke onError multiple times, but we only want to
|
|
||||||
// schedule one backoff attempt
|
|
||||||
// TODO(ericgribkoff) Update contract of NameResolver.Listener or decide if we
|
|
||||||
// want to reset the backoff interval upon repeated onError() calls
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (nameResolverBackoffPolicy == null) {
|
|
||||||
nameResolverBackoffPolicy = backoffPolicyProvider.get();
|
|
||||||
}
|
|
||||||
long delayNanos = nameResolverBackoffPolicy.nextBackoffNanos();
|
|
||||||
channelLogger.log(
|
|
||||||
ChannelLogLevel.DEBUG,
|
|
||||||
"Scheduling DNS resolution backoff for {0} ns", delayNanos);
|
|
||||||
scheduledNameResolverRefresh =
|
|
||||||
syncContext.schedule(
|
|
||||||
new DelayedNameResolverRefresh(), delayNanos, TimeUnit.NANOSECONDS,
|
|
||||||
transportFactory .getScheduledExecutorService());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.internal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface is used to schedule future retry attempts for a failed operation. The retry delay
|
||||||
|
* and the number of attempt is defined by implementing classes. Implementations should assure
|
||||||
|
* that only one future retry operation is ever scheduled at a time.
|
||||||
|
*/
|
||||||
|
public interface RetryScheduler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request to schedule a future retry (or retries) for a failed operation. Noop if an operation
|
||||||
|
* has already been scheduled.
|
||||||
|
*/
|
||||||
|
void schedule(Runnable retryOperation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the scheduler, effectively cancelling any future retry operation.
|
||||||
|
*/
|
||||||
|
void reset();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.internal;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.NameResolver;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.SynchronizationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This wrapper class can add retry capability to any polling {@link NameResolver} implementation
|
||||||
|
* that supports calling {@link ResolutionResultListener}s with the outcome of each resolution.
|
||||||
|
*
|
||||||
|
* <p>The {@link NameResolver} used with this
|
||||||
|
*/
|
||||||
|
final class RetryingNameResolver extends ForwardingNameResolver {
|
||||||
|
|
||||||
|
private final NameResolver retriedNameResolver;
|
||||||
|
private final RetryScheduler retryScheduler;
|
||||||
|
private final SynchronizationContext syncContext;
|
||||||
|
|
||||||
|
static final Attributes.Key<ResolutionResultListener> RESOLUTION_RESULT_LISTENER_KEY
|
||||||
|
= Attributes.Key.create(
|
||||||
|
"io.grpc.internal.RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link RetryingNameResolver}.
|
||||||
|
*
|
||||||
|
* @param retriedNameResolver A {@link NameResolver} that will have failed attempt retried.
|
||||||
|
* @param retryScheduler Used to schedule the retry attempts.
|
||||||
|
*/
|
||||||
|
RetryingNameResolver(NameResolver retriedNameResolver, RetryScheduler retryScheduler,
|
||||||
|
SynchronizationContext syncContext) {
|
||||||
|
super(retriedNameResolver);
|
||||||
|
this.retriedNameResolver = retriedNameResolver;
|
||||||
|
this.retryScheduler = retryScheduler;
|
||||||
|
this.syncContext = syncContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Listener2 listener) {
|
||||||
|
super.start(new RetryingListener(listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
super.shutdown();
|
||||||
|
retryScheduler.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to get the underlying {@link NameResolver} that is getting its failed attempts retried.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
NameResolver getRetriedNameResolver() {
|
||||||
|
return retriedNameResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
class DelayedNameResolverRefresh implements Runnable {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RetryingListener extends Listener2 {
|
||||||
|
private Listener2 delegateListener;
|
||||||
|
|
||||||
|
RetryingListener(Listener2 delegateListener) {
|
||||||
|
this.delegateListener = delegateListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResult(ResolutionResult resolutionResult) {
|
||||||
|
// If the resolution result listener is already an attribute it indicates that a name resolver
|
||||||
|
// has already been wrapped with this class. This indicates a misconfiguration.
|
||||||
|
if (resolutionResult.getAttributes().get(RESOLUTION_RESULT_LISTENER_KEY) != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"RetryingNameResolver can only be used once to wrap a NameResolver");
|
||||||
|
}
|
||||||
|
|
||||||
|
delegateListener.onResult(resolutionResult.toBuilder().setAttributes(
|
||||||
|
resolutionResult.getAttributes().toBuilder()
|
||||||
|
.set(RESOLUTION_RESULT_LISTENER_KEY, new ResolutionResultListener()).build())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Status error) {
|
||||||
|
delegateListener.onError(error);
|
||||||
|
syncContext.execute(() -> retryScheduler.schedule(new DelayedNameResolverRefresh()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple callback class to store in {@link ResolutionResult} attributes so that
|
||||||
|
* ManagedChannel can indicate if the resolved addresses were accepted. Temporary until
|
||||||
|
* the Listener2.onResult() API can be changed to return a boolean for this purpose.
|
||||||
|
*/
|
||||||
|
class ResolutionResultListener {
|
||||||
|
public void resolutionAttempted(boolean successful) {
|
||||||
|
if (successful) {
|
||||||
|
retryScheduler.reset();
|
||||||
|
} else {
|
||||||
|
retryScheduler.schedule(new DelayedNameResolverRefresh());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.internal;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
import io.grpc.SynchronizationContext;
|
||||||
|
import java.lang.Thread.UncaughtExceptionHandler;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link BackoffPolicyRetryScheduler}.
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class BackoffPolicyRetrySchedulerTest {
|
||||||
|
|
||||||
|
private final FakeClock fakeClock = new FakeClock();
|
||||||
|
|
||||||
|
private BackoffPolicyRetryScheduler scheduler;
|
||||||
|
private final SynchronizationContext syncContext = new SynchronizationContext(
|
||||||
|
mock(UncaughtExceptionHandler.class));
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
scheduler = new BackoffPolicyRetryScheduler(new FakeBackoffPolicyProvider(),
|
||||||
|
fakeClock.getScheduledExecutorService(), syncContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void schedule() {
|
||||||
|
AtomicInteger retryCount = new AtomicInteger();
|
||||||
|
Runnable retry = retryCount::incrementAndGet;
|
||||||
|
syncContext.execute(() -> scheduler.schedule(retry));
|
||||||
|
|
||||||
|
fakeClock.forwardTime(2, TimeUnit.NANOSECONDS);
|
||||||
|
|
||||||
|
assertThat(retryCount.get()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void schedule_noMultiple() {
|
||||||
|
AtomicInteger retryCount = new AtomicInteger();
|
||||||
|
Runnable retry = retryCount::incrementAndGet;
|
||||||
|
|
||||||
|
// We schedule multiple retries...
|
||||||
|
syncContext.execute(() -> scheduler.schedule(retry));
|
||||||
|
syncContext.execute(() -> scheduler.schedule(retry));
|
||||||
|
|
||||||
|
fakeClock.forwardTime(2, TimeUnit.NANOSECONDS);
|
||||||
|
|
||||||
|
// But only one of them should have run.
|
||||||
|
assertThat(retryCount.get()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reset() {
|
||||||
|
AtomicInteger retryCount = new AtomicInteger();
|
||||||
|
Runnable retry = retryCount::incrementAndGet;
|
||||||
|
Runnable retryTwo = () -> {
|
||||||
|
retryCount.getAndAdd(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// We schedule one retry.
|
||||||
|
syncContext.execute(() -> scheduler.schedule(retry));
|
||||||
|
|
||||||
|
// But then reset.
|
||||||
|
syncContext.execute(() -> scheduler.reset());
|
||||||
|
|
||||||
|
// And schedule a different retry.
|
||||||
|
syncContext.execute(() -> scheduler.schedule(retryTwo));
|
||||||
|
|
||||||
|
fakeClock.forwardTime(2, TimeUnit.NANOSECONDS);
|
||||||
|
|
||||||
|
// The retry after the reset should have been run.
|
||||||
|
assertThat(retryCount.get()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FakeBackoffPolicyProvider implements BackoffPolicy.Provider {
|
||||||
|
@Override
|
||||||
|
public BackoffPolicy get() {
|
||||||
|
return new BackoffPolicy() {
|
||||||
|
@Override
|
||||||
|
public long nextBackoffNanos() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,8 @@ import org.junit.runners.JUnit4;
|
||||||
/** Unit tests for {@link DnsNameResolverProvider}. */
|
/** Unit tests for {@link DnsNameResolverProvider}. */
|
||||||
@RunWith(JUnit4.class)
|
@RunWith(JUnit4.class)
|
||||||
public class DnsNameResolverProviderTest {
|
public class DnsNameResolverProviderTest {
|
||||||
|
private final FakeClock fakeClock = new FakeClock();
|
||||||
|
|
||||||
private final SynchronizationContext syncContext = new SynchronizationContext(
|
private final SynchronizationContext syncContext = new SynchronizationContext(
|
||||||
new Thread.UncaughtExceptionHandler() {
|
new Thread.UncaughtExceptionHandler() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -46,6 +48,7 @@ public class DnsNameResolverProviderTest {
|
||||||
.setSynchronizationContext(syncContext)
|
.setSynchronizationContext(syncContext)
|
||||||
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
||||||
.setChannelLogger(mock(ChannelLogger.class))
|
.setChannelLogger(mock(ChannelLogger.class))
|
||||||
|
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private DnsNameResolverProvider provider = new DnsNameResolverProvider();
|
private DnsNameResolverProvider provider = new DnsNameResolverProvider();
|
||||||
|
|
@ -58,7 +61,9 @@ public class DnsNameResolverProviderTest {
|
||||||
@Test
|
@Test
|
||||||
public void newNameResolver() {
|
public void newNameResolver() {
|
||||||
assertSame(DnsNameResolver.class,
|
assertSame(DnsNameResolver.class,
|
||||||
provider.newNameResolver(URI.create("dns:///localhost:443"), args).getClass());
|
((RetryingNameResolver) provider.newNameResolver(
|
||||||
|
URI.create("dns:///localhost:443"), args))
|
||||||
|
.getRetriedNameResolver().getClass());
|
||||||
assertNull(
|
assertNull(
|
||||||
provider.newNameResolver(URI.create("notdns:///localhost:443"), args));
|
provider.newNameResolver(URI.create("notdns:///localhost:443"), args));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
|
|
@ -37,7 +39,6 @@ import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.net.InetAddresses;
|
import com.google.common.net.InetAddresses;
|
||||||
import com.google.common.testing.FakeTicker;
|
import com.google.common.testing.FakeTicker;
|
||||||
import io.grpc.Attributes;
|
|
||||||
import io.grpc.ChannelLogger;
|
import io.grpc.ChannelLogger;
|
||||||
import io.grpc.EquivalentAddressGroup;
|
import io.grpc.EquivalentAddressGroup;
|
||||||
import io.grpc.HttpConnectProxiedSocketAddress;
|
import io.grpc.HttpConnectProxiedSocketAddress;
|
||||||
|
|
@ -111,17 +112,18 @@ public class DnsNameResolverTest {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
private final NameResolver.Args args = NameResolver.Args.newBuilder()
|
|
||||||
.setDefaultPort(DEFAULT_PORT)
|
|
||||||
.setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
|
|
||||||
.setSynchronizationContext(syncContext)
|
|
||||||
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
|
||||||
.setChannelLogger(mock(ChannelLogger.class))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private final DnsNameResolverProvider provider = new DnsNameResolverProvider();
|
private final DnsNameResolverProvider provider = new DnsNameResolverProvider();
|
||||||
private final FakeClock fakeClock = new FakeClock();
|
private final FakeClock fakeClock = new FakeClock();
|
||||||
private final FakeClock fakeExecutor = new FakeClock();
|
private final FakeClock fakeExecutor = new FakeClock();
|
||||||
|
private static final FakeClock.TaskFilter NAME_RESOLVER_REFRESH_TASK_FILTER =
|
||||||
|
new FakeClock.TaskFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean shouldAccept(Runnable command) {
|
||||||
|
return command.toString().contains(
|
||||||
|
RetryingNameResolver.DelayedNameResolverRefresh.class.getName());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private final FakeExecutorResource fakeExecutorResource = new FakeExecutorResource();
|
private final FakeExecutorResource fakeExecutorResource = new FakeExecutorResource();
|
||||||
|
|
||||||
|
|
@ -138,6 +140,15 @@ public class DnsNameResolverTest {
|
||||||
public void close(Executor instance) {}
|
public void close(Executor instance) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final NameResolver.Args args = NameResolver.Args.newBuilder()
|
||||||
|
.setDefaultPort(DEFAULT_PORT)
|
||||||
|
.setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
|
||||||
|
.setSynchronizationContext(syncContext)
|
||||||
|
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
||||||
|
.setChannelLogger(mock(ChannelLogger.class))
|
||||||
|
.setScheduledExecutorService(fakeExecutor.getScheduledExecutorService())
|
||||||
|
.build();
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private NameResolver.Listener2 mockListener;
|
private NameResolver.Listener2 mockListener;
|
||||||
@Captor
|
@Captor
|
||||||
|
|
@ -149,18 +160,18 @@ public class DnsNameResolverTest {
|
||||||
@Mock
|
@Mock
|
||||||
private RecordFetcher recordFetcher;
|
private RecordFetcher recordFetcher;
|
||||||
|
|
||||||
private DnsNameResolver newResolver(String name, int defaultPort) {
|
private RetryingNameResolver newResolver(String name, int defaultPort) {
|
||||||
return newResolver(
|
return newResolver(
|
||||||
name, defaultPort, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted());
|
name, defaultPort, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted());
|
||||||
}
|
}
|
||||||
|
|
||||||
private DnsNameResolver newResolver(String name, int defaultPort, boolean isAndroid) {
|
private RetryingNameResolver newResolver(String name, int defaultPort, boolean isAndroid) {
|
||||||
return newResolver(
|
return newResolver(
|
||||||
name, defaultPort, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(),
|
name, defaultPort, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(),
|
||||||
isAndroid);
|
isAndroid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DnsNameResolver newResolver(
|
private RetryingNameResolver newResolver(
|
||||||
String name,
|
String name,
|
||||||
int defaultPort,
|
int defaultPort,
|
||||||
ProxyDetector proxyDetector,
|
ProxyDetector proxyDetector,
|
||||||
|
|
@ -168,7 +179,7 @@ public class DnsNameResolverTest {
|
||||||
return newResolver(name, defaultPort, proxyDetector, stopwatch, false);
|
return newResolver(name, defaultPort, proxyDetector, stopwatch, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DnsNameResolver newResolver(
|
private RetryingNameResolver newResolver(
|
||||||
String name,
|
String name,
|
||||||
final int defaultPort,
|
final int defaultPort,
|
||||||
final ProxyDetector proxyDetector,
|
final ProxyDetector proxyDetector,
|
||||||
|
|
@ -181,21 +192,31 @@ public class DnsNameResolverTest {
|
||||||
.setSynchronizationContext(syncContext)
|
.setSynchronizationContext(syncContext)
|
||||||
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
||||||
.setChannelLogger(mock(ChannelLogger.class))
|
.setChannelLogger(mock(ChannelLogger.class))
|
||||||
|
.setScheduledExecutorService(fakeExecutor.getScheduledExecutorService())
|
||||||
.build();
|
.build();
|
||||||
return newResolver(name, stopwatch, isAndroid, args);
|
return newResolver(name, stopwatch, isAndroid, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DnsNameResolver newResolver(
|
private RetryingNameResolver newResolver(
|
||||||
String name,
|
String name,
|
||||||
Stopwatch stopwatch,
|
Stopwatch stopwatch,
|
||||||
boolean isAndroid,
|
boolean isAndroid,
|
||||||
NameResolver.Args args) {
|
NameResolver.Args args) {
|
||||||
DnsNameResolver dnsResolver =
|
DnsNameResolver dnsResolver = new DnsNameResolver(null, name, args, fakeExecutorResource,
|
||||||
new DnsNameResolver(
|
stopwatch, isAndroid);
|
||||||
null, name, args, fakeExecutorResource, stopwatch, isAndroid);
|
|
||||||
// By default, using the mocked ResourceResolver to avoid I/O
|
// By default, using the mocked ResourceResolver to avoid I/O
|
||||||
dnsResolver.setResourceResolver(new JndiResourceResolver(recordFetcher));
|
dnsResolver.setResourceResolver(new JndiResourceResolver(recordFetcher));
|
||||||
return dnsResolver;
|
|
||||||
|
// In practice the DNS name resolver provider always wraps the resolver in a
|
||||||
|
// RetryingNameResolver which adds retry capabilities to it. We use the same setup here.
|
||||||
|
return new RetryingNameResolver(
|
||||||
|
dnsResolver,
|
||||||
|
new BackoffPolicyRetryScheduler(
|
||||||
|
new ExponentialBackoffPolicy.Provider(),
|
||||||
|
fakeExecutor.getScheduledExecutorService(),
|
||||||
|
syncContext
|
||||||
|
),
|
||||||
|
syncContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
@ -203,6 +224,15 @@ public class DnsNameResolverTest {
|
||||||
DnsNameResolver.enableJndi = true;
|
DnsNameResolver.enableJndi = true;
|
||||||
networkaddressCacheTtlPropertyValue =
|
networkaddressCacheTtlPropertyValue =
|
||||||
System.getProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY);
|
System.getProperty(DnsNameResolver.NETWORKADDRESS_CACHE_TTL_PROPERTY);
|
||||||
|
|
||||||
|
// By default the mock listener processes the result successfully.
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
ResolutionResult result = invocation.getArgument(0);
|
||||||
|
syncContext.execute(
|
||||||
|
() -> result.getAttributes().get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY)
|
||||||
|
.resolutionAttempted(true));
|
||||||
|
return null;
|
||||||
|
}).when(mockListener).onResult(isA(ResolutionResult.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|
@ -216,12 +246,6 @@ public class DnsNameResolverTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
public void noMorePendingTasks() {
|
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
|
||||||
assertEquals(0, fakeExecutor.numPendingTasks());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidDnsName() throws Exception {
|
public void invalidDnsName() throws Exception {
|
||||||
testInvalidUri(new URI("dns", null, "/[invalid]", null));
|
testInvalidUri(new URI("dns", null, "/[invalid]", null));
|
||||||
|
|
@ -287,10 +311,11 @@ public class DnsNameResolverTest {
|
||||||
final List<InetAddress> answer2 = createAddressList(1);
|
final List<InetAddress> answer2 = createAddressList(1);
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
|
|
||||||
DnsNameResolver resolver = newResolver(name, 81, isAndroid);
|
RetryingNameResolver resolver = newResolver(name, 81, isAndroid);
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString())).thenReturn(answer1).thenReturn(answer2);
|
when(mockResolver.resolveAddress(anyString())).thenReturn(answer1).thenReturn(answer2);
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
@ -303,8 +328,9 @@ public class DnsNameResolverTest {
|
||||||
verify(mockListener, times(2)).onResult(resultCaptor.capture());
|
verify(mockListener, times(2)).onResult(resultCaptor.capture());
|
||||||
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
|
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
verify(mockResolver, times(2)).resolveAddress(anyString());
|
verify(mockResolver, times(2)).resolveAddress(anyString());
|
||||||
}
|
}
|
||||||
|
|
@ -313,18 +339,20 @@ public class DnsNameResolverTest {
|
||||||
public void testExecutor_default() throws Exception {
|
public void testExecutor_default() throws Exception {
|
||||||
final List<InetAddress> answer = createAddressList(2);
|
final List<InetAddress> answer = createAddressList(2);
|
||||||
|
|
||||||
DnsNameResolver resolver = newResolver("foo.googleapis.com", 81);
|
RetryingNameResolver resolver = newResolver("foo.googleapis.com", 81);
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString())).thenReturn(answer);
|
when(mockResolver.resolveAddress(anyString())).thenReturn(answer);
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onResult(resultCaptor.capture());
|
verify(mockListener).onResult(resultCaptor.capture());
|
||||||
assertAnswerMatches(answer, 81, resultCaptor.getValue());
|
assertAnswerMatches(answer, 81, resultCaptor.getValue());
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
assertThat(fakeExecutorResource.createCount.get()).isEqualTo(1);
|
assertThat(fakeExecutorResource.createCount.get()).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
@ -341,6 +369,7 @@ public class DnsNameResolverTest {
|
||||||
.setSynchronizationContext(syncContext)
|
.setSynchronizationContext(syncContext)
|
||||||
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
||||||
.setChannelLogger(mock(ChannelLogger.class))
|
.setChannelLogger(mock(ChannelLogger.class))
|
||||||
|
.setScheduledExecutorService(fakeExecutor.getScheduledExecutorService())
|
||||||
.setOffloadExecutor(
|
.setOffloadExecutor(
|
||||||
new Executor() {
|
new Executor() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -351,19 +380,21 @@ public class DnsNameResolverTest {
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
DnsNameResolver resolver =
|
RetryingNameResolver resolver = newResolver(
|
||||||
newResolver("foo.googleapis.com", Stopwatch.createUnstarted(), false, args);
|
"foo.googleapis.com", Stopwatch.createUnstarted(), false, args);
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString())).thenReturn(answer);
|
when(mockResolver.resolveAddress(anyString())).thenReturn(answer);
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(0, fakeExecutor.runDueTasks());
|
assertEquals(0, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onResult(resultCaptor.capture());
|
verify(mockListener).onResult(resultCaptor.capture());
|
||||||
assertAnswerMatches(answer, 81, resultCaptor.getValue());
|
assertAnswerMatches(answer, 81, resultCaptor.getValue());
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
assertThat(fakeExecutorResource.createCount.get()).isEqualTo(0);
|
assertThat(fakeExecutorResource.createCount.get()).isEqualTo(0);
|
||||||
assertThat(executions.get()).isEqualTo(1);
|
assertThat(executions.get()).isEqualTo(1);
|
||||||
|
|
@ -376,13 +407,14 @@ public class DnsNameResolverTest {
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
FakeTicker fakeTicker = new FakeTicker();
|
FakeTicker fakeTicker = new FakeTicker();
|
||||||
|
|
||||||
DnsNameResolver resolver =
|
RetryingNameResolver resolver = newResolver(
|
||||||
newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString()))
|
when(mockResolver.resolveAddress(anyString()))
|
||||||
.thenReturn(answer1)
|
.thenReturn(answer1)
|
||||||
.thenThrow(new AssertionError("should not called twice"));
|
.thenThrow(new AssertionError("should not called twice"));
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
@ -396,7 +428,7 @@ public class DnsNameResolverTest {
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
verifyNoMoreInteractions(mockListener);
|
verifyNoMoreInteractions(mockListener);
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
verify(mockResolver).resolveAddress(anyString());
|
verify(mockResolver).resolveAddress(anyString());
|
||||||
}
|
}
|
||||||
|
|
@ -409,13 +441,14 @@ public class DnsNameResolverTest {
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
FakeTicker fakeTicker = new FakeTicker();
|
FakeTicker fakeTicker = new FakeTicker();
|
||||||
|
|
||||||
DnsNameResolver resolver =
|
RetryingNameResolver resolver = newResolver(
|
||||||
newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString()))
|
when(mockResolver.resolveAddress(anyString()))
|
||||||
.thenReturn(answer)
|
.thenReturn(answer)
|
||||||
.thenThrow(new AssertionError("should not reach here."));
|
.thenThrow(new AssertionError("should not reach here."));
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
@ -430,7 +463,7 @@ public class DnsNameResolverTest {
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
verifyNoMoreInteractions(mockListener);
|
verifyNoMoreInteractions(mockListener);
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
verify(mockResolver).resolveAddress(anyString());
|
verify(mockResolver).resolveAddress(anyString());
|
||||||
}
|
}
|
||||||
|
|
@ -444,12 +477,13 @@ public class DnsNameResolverTest {
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
FakeTicker fakeTicker = new FakeTicker();
|
FakeTicker fakeTicker = new FakeTicker();
|
||||||
|
|
||||||
DnsNameResolver resolver =
|
RetryingNameResolver resolver = newResolver(
|
||||||
newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString())).thenReturn(answer1)
|
when(mockResolver.resolveAddress(anyString())).thenReturn(answer1)
|
||||||
.thenReturn(answer2);
|
.thenReturn(answer2);
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
@ -463,8 +497,9 @@ public class DnsNameResolverTest {
|
||||||
verify(mockListener, times(2)).onResult(resultCaptor.capture());
|
verify(mockListener, times(2)).onResult(resultCaptor.capture());
|
||||||
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
|
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
verify(mockResolver, times(2)).resolveAddress(anyString());
|
verify(mockResolver, times(2)).resolveAddress(anyString());
|
||||||
}
|
}
|
||||||
|
|
@ -487,11 +522,12 @@ public class DnsNameResolverTest {
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
FakeTicker fakeTicker = new FakeTicker();
|
FakeTicker fakeTicker = new FakeTicker();
|
||||||
|
|
||||||
DnsNameResolver resolver =
|
RetryingNameResolver resolver = newResolver(
|
||||||
newResolver(name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
name, 81, GrpcUtil.NOOP_PROXY_DETECTOR, Stopwatch.createUnstarted(fakeTicker));
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockResolver = mock(AddressResolver.class);
|
AddressResolver mockResolver = mock(AddressResolver.class);
|
||||||
when(mockResolver.resolveAddress(anyString())).thenReturn(answer1).thenReturn(answer2);
|
when(mockResolver.resolveAddress(anyString())).thenReturn(answer1).thenReturn(answer2);
|
||||||
resolver.setAddressResolver(mockResolver);
|
dnsResolver.setAddressResolver(mockResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
@ -511,8 +547,9 @@ public class DnsNameResolverTest {
|
||||||
verify(mockListener, times(2)).onResult(resultCaptor.capture());
|
verify(mockListener, times(2)).onResult(resultCaptor.capture());
|
||||||
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
|
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
|
||||||
assertEquals(0, fakeClock.numPendingTasks());
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
|
|
||||||
resolver.shutdown();
|
syncContext.execute(() -> resolver.shutdown());
|
||||||
|
|
||||||
verify(mockResolver, times(2)).resolveAddress(anyString());
|
verify(mockResolver, times(2)).resolveAddress(anyString());
|
||||||
}
|
}
|
||||||
|
|
@ -520,8 +557,9 @@ public class DnsNameResolverTest {
|
||||||
@Test
|
@Test
|
||||||
public void resolve_emptyResult() throws Exception {
|
public void resolve_emptyResult() throws Exception {
|
||||||
DnsNameResolver.enableTxt = true;
|
DnsNameResolver.enableTxt = true;
|
||||||
DnsNameResolver nr = newResolver("dns:///addr.fake:1234", 443);
|
RetryingNameResolver resolver = newResolver("dns:///addr.fake:1234", 443);
|
||||||
nr.setAddressResolver(new AddressResolver() {
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
|
dnsResolver.setAddressResolver(new AddressResolver() {
|
||||||
@Override
|
@Override
|
||||||
public List<InetAddress> resolveAddress(String host) throws Exception {
|
public List<InetAddress> resolveAddress(String host) throws Exception {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
@ -531,18 +569,60 @@ public class DnsNameResolverTest {
|
||||||
when(mockResourceResolver.resolveTxt(anyString()))
|
when(mockResourceResolver.resolveTxt(anyString()))
|
||||||
.thenReturn(Collections.<String>emptyList());
|
.thenReturn(Collections.<String>emptyList());
|
||||||
|
|
||||||
nr.setResourceResolver(mockResourceResolver);
|
dnsResolver.setResourceResolver(mockResourceResolver);
|
||||||
|
|
||||||
nr.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertThat(fakeExecutor.runDueTasks()).isEqualTo(1);
|
assertThat(fakeExecutor.runDueTasks()).isEqualTo(1);
|
||||||
|
|
||||||
ArgumentCaptor<ResolutionResult> ac = ArgumentCaptor.forClass(ResolutionResult.class);
|
ArgumentCaptor<ResolutionResult> ac = ArgumentCaptor.forClass(ResolutionResult.class);
|
||||||
verify(mockListener).onResult(ac.capture());
|
verify(mockListener).onResult(ac.capture());
|
||||||
verifyNoMoreInteractions(mockListener);
|
verifyNoMoreInteractions(mockListener);
|
||||||
assertThat(ac.getValue().getAddresses()).isEmpty();
|
assertThat(ac.getValue().getAddresses()).isEmpty();
|
||||||
assertThat(ac.getValue().getAttributes()).isEqualTo(Attributes.EMPTY);
|
|
||||||
assertThat(ac.getValue().getServiceConfig()).isNull();
|
assertThat(ac.getValue().getServiceConfig()).isNull();
|
||||||
verify(mockResourceResolver, never()).resolveSrv(anyString());
|
verify(mockResourceResolver, never()).resolveSrv(anyString());
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load balancer rejects the empty addresses.
|
||||||
|
@Test
|
||||||
|
public void resolve_emptyResult_notAccepted() throws Exception {
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
ResolutionResult result = invocation.getArgument(0);
|
||||||
|
result.getAttributes().get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY)
|
||||||
|
.resolutionAttempted(false);
|
||||||
|
return null;
|
||||||
|
}).when(mockListener).onResult(isA(ResolutionResult.class));
|
||||||
|
|
||||||
|
DnsNameResolver.enableTxt = true;
|
||||||
|
RetryingNameResolver resolver = newResolver("dns:///addr.fake:1234", 443);
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
|
dnsResolver.setAddressResolver(new AddressResolver() {
|
||||||
|
@Override
|
||||||
|
public List<InetAddress> resolveAddress(String host) throws Exception {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
|
||||||
|
when(mockResourceResolver.resolveTxt(anyString()))
|
||||||
|
.thenReturn(Collections.<String>emptyList());
|
||||||
|
|
||||||
|
dnsResolver.setResourceResolver(mockResourceResolver);
|
||||||
|
|
||||||
|
resolver.start(mockListener);
|
||||||
|
syncContext.execute(() -> assertThat(fakeExecutor.runDueTasks()).isEqualTo(1));
|
||||||
|
|
||||||
|
ArgumentCaptor<ResolutionResult> ac = ArgumentCaptor.forClass(ResolutionResult.class);
|
||||||
|
verify(mockListener).onResult(ac.capture());
|
||||||
|
verifyNoMoreInteractions(mockListener);
|
||||||
|
assertThat(ac.getValue().getAddresses()).isEmpty();
|
||||||
|
assertThat(ac.getValue().getServiceConfig()).isNull();
|
||||||
|
verify(mockResourceResolver, never()).resolveSrv(anyString());
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
// A retry should be scheduled
|
||||||
|
assertThat(fakeExecutor.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER)).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -554,9 +634,10 @@ public class DnsNameResolverTest {
|
||||||
.thenReturn(Collections.singletonList(backendAddr));
|
.thenReturn(Collections.singletonList(backendAddr));
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
|
|
||||||
DnsNameResolver resolver = newResolver(name, 81);
|
RetryingNameResolver resolver = newResolver(name, 81);
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
resolver.setResourceResolver(null);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
|
dnsResolver.setResourceResolver(null);
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onResult(resultCaptor.capture());
|
verify(mockListener).onResult(resultCaptor.capture());
|
||||||
|
|
@ -566,8 +647,10 @@ public class DnsNameResolverTest {
|
||||||
Iterables.getOnlyElement(result.getAddresses()).getAddresses());
|
Iterables.getOnlyElement(result.getAddresses()).getAddresses());
|
||||||
assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
|
assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
|
||||||
verify(mockAddressResolver).resolveAddress(name);
|
verify(mockAddressResolver).resolveAddress(name);
|
||||||
assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY);
|
|
||||||
assertThat(result.getServiceConfig()).isNull();
|
assertThat(result.getServiceConfig()).isNull();
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -578,15 +661,20 @@ public class DnsNameResolverTest {
|
||||||
.thenThrow(new IOException("no addr"));
|
.thenThrow(new IOException("no addr"));
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
|
|
||||||
DnsNameResolver resolver = newResolver(name, 81);
|
RetryingNameResolver resolver = newResolver(name, 81);
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
resolver.setResourceResolver(null);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
|
dnsResolver.setResourceResolver(null);
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onError(errorCaptor.capture());
|
verify(mockListener).onError(errorCaptor.capture());
|
||||||
Status errorStatus = errorCaptor.getValue();
|
Status errorStatus = errorCaptor.getValue();
|
||||||
assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
|
assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
|
||||||
assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr");
|
assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr");
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
// A retry should be scheduled
|
||||||
|
assertThat(fakeExecutor.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER)).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -613,12 +701,14 @@ public class DnsNameResolverTest {
|
||||||
.setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
|
.setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
|
||||||
.setSynchronizationContext(syncContext)
|
.setSynchronizationContext(syncContext)
|
||||||
.setServiceConfigParser(serviceConfigParser)
|
.setServiceConfigParser(serviceConfigParser)
|
||||||
|
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
DnsNameResolver resolver = newResolver(name, Stopwatch.createUnstarted(), false, args);
|
RetryingNameResolver resolver = newResolver(name, Stopwatch.createUnstarted(), false, args);
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
resolver.setResourceResolver(mockResourceResolver);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
|
dnsResolver.setResourceResolver(mockResourceResolver);
|
||||||
|
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
@ -631,6 +721,9 @@ public class DnsNameResolverTest {
|
||||||
assertThat(result.getServiceConfig().getConfig()).isNotNull();
|
assertThat(result.getServiceConfig().getConfig()).isNotNull();
|
||||||
verify(mockAddressResolver).resolveAddress(name);
|
verify(mockAddressResolver).resolveAddress(name);
|
||||||
verify(mockResourceResolver).resolveTxt("_grpc_config." + name);
|
verify(mockResourceResolver).resolveTxt("_grpc_config." + name);
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -642,9 +735,10 @@ public class DnsNameResolverTest {
|
||||||
String name = "foo.googleapis.com";
|
String name = "foo.googleapis.com";
|
||||||
|
|
||||||
ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
|
ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
|
||||||
DnsNameResolver resolver = newResolver(name, 81);
|
RetryingNameResolver resolver = newResolver(name, 81);
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
DnsNameResolver dnsResolver = (DnsNameResolver)resolver.getRetriedNameResolver();
|
||||||
resolver.setResourceResolver(mockResourceResolver);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
|
dnsResolver.setResourceResolver(mockResourceResolver);
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onError(errorCaptor.capture());
|
verify(mockListener).onError(errorCaptor.capture());
|
||||||
|
|
@ -652,6 +746,10 @@ public class DnsNameResolverTest {
|
||||||
assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
|
assertThat(errorStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
|
||||||
assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr");
|
assertThat(errorStatus.getCause()).hasMessageThat().contains("no addr");
|
||||||
verify(mockResourceResolver, never()).resolveTxt(anyString());
|
verify(mockResourceResolver, never()).resolveTxt(anyString());
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
// A retry should be scheduled
|
||||||
|
assertThat(fakeExecutor.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER)).isEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -666,9 +764,10 @@ public class DnsNameResolverTest {
|
||||||
when(mockResourceResolver.resolveTxt(anyString()))
|
when(mockResourceResolver.resolveTxt(anyString()))
|
||||||
.thenThrow(new Exception("something like javax.naming.NamingException"));
|
.thenThrow(new Exception("something like javax.naming.NamingException"));
|
||||||
|
|
||||||
DnsNameResolver resolver = newResolver(name, 81);
|
RetryingNameResolver resolver = newResolver(name, 81);
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
resolver.setResourceResolver(mockResourceResolver);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
|
dnsResolver.setResourceResolver(mockResourceResolver);
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onResult(resultCaptor.capture());
|
verify(mockListener).onResult(resultCaptor.capture());
|
||||||
|
|
@ -678,9 +777,11 @@ public class DnsNameResolverTest {
|
||||||
Iterables.getOnlyElement(result.getAddresses()).getAddresses());
|
Iterables.getOnlyElement(result.getAddresses()).getAddresses());
|
||||||
assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
|
assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
|
||||||
verify(mockAddressResolver).resolveAddress(name);
|
verify(mockAddressResolver).resolveAddress(name);
|
||||||
assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY);
|
|
||||||
assertThat(result.getServiceConfig()).isNull();
|
assertThat(result.getServiceConfig()).isNull();
|
||||||
verify(mockResourceResolver).resolveTxt(anyString());
|
verify(mockResourceResolver).resolveTxt(anyString());
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -695,9 +796,10 @@ public class DnsNameResolverTest {
|
||||||
when(mockResourceResolver.resolveTxt(anyString()))
|
when(mockResourceResolver.resolveTxt(anyString()))
|
||||||
.thenReturn(Collections.singletonList("grpc_config=something invalid"));
|
.thenReturn(Collections.singletonList("grpc_config=something invalid"));
|
||||||
|
|
||||||
DnsNameResolver resolver = newResolver(name, 81);
|
RetryingNameResolver resolver = newResolver(name, 81);
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
resolver.setResourceResolver(mockResourceResolver);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
|
dnsResolver.setResourceResolver(mockResourceResolver);
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
verify(mockListener).onResult(resultCaptor.capture());
|
verify(mockListener).onResult(resultCaptor.capture());
|
||||||
|
|
@ -707,10 +809,12 @@ public class DnsNameResolverTest {
|
||||||
Iterables.getOnlyElement(result.getAddresses()).getAddresses());
|
Iterables.getOnlyElement(result.getAddresses()).getAddresses());
|
||||||
assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
|
assertThat(resolvedBackendAddr.getAddress()).isEqualTo(backendAddr);
|
||||||
verify(mockAddressResolver).resolveAddress(name);
|
verify(mockAddressResolver).resolveAddress(name);
|
||||||
assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY);
|
|
||||||
assertThat(result.getServiceConfig()).isNotNull();
|
assertThat(result.getServiceConfig()).isNotNull();
|
||||||
assertThat(result.getServiceConfig().getError()).isNotNull();
|
assertThat(result.getServiceConfig().getError()).isNotNull();
|
||||||
verify(mockResourceResolver).resolveTxt(anyString());
|
verify(mockResourceResolver).resolveTxt(anyString());
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -757,11 +861,12 @@ public class DnsNameResolverTest {
|
||||||
.setPassword("password").build();
|
.setPassword("password").build();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
DnsNameResolver resolver =
|
RetryingNameResolver resolver = newResolver(
|
||||||
newResolver(name, port, alwaysDetectProxy, Stopwatch.createUnstarted());
|
name, port, alwaysDetectProxy, Stopwatch.createUnstarted());
|
||||||
|
DnsNameResolver dnsResolver = (DnsNameResolver) resolver.getRetriedNameResolver();
|
||||||
AddressResolver mockAddressResolver = mock(AddressResolver.class);
|
AddressResolver mockAddressResolver = mock(AddressResolver.class);
|
||||||
when(mockAddressResolver.resolveAddress(anyString())).thenThrow(new AssertionError());
|
when(mockAddressResolver.resolveAddress(anyString())).thenThrow(new AssertionError());
|
||||||
resolver.setAddressResolver(mockAddressResolver);
|
dnsResolver.setAddressResolver(mockAddressResolver);
|
||||||
resolver.start(mockListener);
|
resolver.start(mockListener);
|
||||||
assertEquals(1, fakeExecutor.runDueTasks());
|
assertEquals(1, fakeExecutor.runDueTasks());
|
||||||
|
|
||||||
|
|
@ -777,6 +882,9 @@ public class DnsNameResolverTest {
|
||||||
assertEquals("username", socketAddress.getUsername());
|
assertEquals("username", socketAddress.getUsername());
|
||||||
assertEquals("password", socketAddress.getPassword());
|
assertEquals("password", socketAddress.getPassword());
|
||||||
assertTrue(socketAddress.getTargetAddress().isUnresolved());
|
assertTrue(socketAddress.getTargetAddress().isUnresolved());
|
||||||
|
|
||||||
|
assertEquals(0, fakeClock.numPendingTasks());
|
||||||
|
assertEquals(0, fakeExecutor.numPendingTasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1185,7 +1293,8 @@ public class DnsNameResolverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testValidUri(URI uri, String exportedAuthority, int expectedPort) {
|
private void testValidUri(URI uri, String exportedAuthority, int expectedPort) {
|
||||||
DnsNameResolver resolver = provider.newNameResolver(uri, args);
|
DnsNameResolver resolver = (DnsNameResolver) ((RetryingNameResolver) provider.newNameResolver(
|
||||||
|
uri, args)).getRetriedNameResolver();
|
||||||
assertNotNull(resolver);
|
assertNotNull(resolver);
|
||||||
assertEquals(expectedPort, resolver.getPort());
|
assertEquals(expectedPort, resolver.getPort());
|
||||||
assertEquals(exportedAuthority, resolver.getServiceAuthority());
|
assertEquals(exportedAuthority, resolver.getServiceAuthority());
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ public class ForwardingNameResolverTest {
|
||||||
NameResolver.Listener2 listener = new NameResolver.Listener2() {
|
NameResolver.Listener2 listener = new NameResolver.Listener2() {
|
||||||
@Override
|
@Override
|
||||||
public void onResult(ResolutionResult result) {
|
public void onResult(ResolutionResult result) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public class ManagedChannelImplGetNameResolverTest {
|
||||||
.setSynchronizationContext(new SynchronizationContext(mock(UncaughtExceptionHandler.class)))
|
.setSynchronizationContext(new SynchronizationContext(mock(UncaughtExceptionHandler.class)))
|
||||||
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
.setServiceConfigParser(mock(ServiceConfigParser.class))
|
||||||
.setChannelLogger(mock(ChannelLogger.class))
|
.setChannelLogger(mock(ChannelLogger.class))
|
||||||
|
.setScheduledExecutorService(new FakeClock().getScheduledExecutorService())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ import static org.mockito.Mockito.when;
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
@ -205,14 +204,6 @@ public class ManagedChannelImplTest {
|
||||||
private final FakeClock timer = new FakeClock();
|
private final FakeClock timer = new FakeClock();
|
||||||
private final FakeClock executor = new FakeClock();
|
private final FakeClock executor = new FakeClock();
|
||||||
private final FakeClock balancerRpcExecutor = new FakeClock();
|
private final FakeClock balancerRpcExecutor = new FakeClock();
|
||||||
private static final FakeClock.TaskFilter NAME_RESOLVER_REFRESH_TASK_FILTER =
|
|
||||||
new FakeClock.TaskFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean shouldAccept(Runnable command) {
|
|
||||||
return command.toString().contains(
|
|
||||||
ManagedChannelImpl.DelayedNameResolverRefresh.class.getName());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final InternalChannelz channelz = new InternalChannelz();
|
private final InternalChannelz channelz = new InternalChannelz();
|
||||||
|
|
||||||
|
|
@ -309,10 +300,6 @@ public class ManagedChannelImplTest {
|
||||||
numExpectedTasks += 1;
|
numExpectedTasks += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getNameResolverRefresh() != null) {
|
|
||||||
numExpectedTasks += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(numExpectedTasks, timer.numPendingTasks());
|
assertEquals(numExpectedTasks, timer.numPendingTasks());
|
||||||
|
|
||||||
ArgumentCaptor<Helper> helperCaptor = ArgumentCaptor.forClass(Helper.class);
|
ArgumentCaptor<Helper> helperCaptor = ArgumentCaptor.forClass(Helper.class);
|
||||||
|
|
@ -1062,139 +1049,6 @@ public class ManagedChannelImplTest {
|
||||||
TimeUnit.SECONDS.toNanos(ManagedChannelImpl.SUBCHANNEL_SHUTDOWN_DELAY_SECONDS));
|
TimeUnit.SECONDS.toNanos(ManagedChannelImpl.SUBCHANNEL_SHUTDOWN_DELAY_SECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nameResolutionFailed() {
|
|
||||||
Status error = Status.UNAVAILABLE.withCause(new Throwable("fake name resolution error"));
|
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
|
||||||
new FakeNameResolverFactory.Builder(expectedUri)
|
|
||||||
.setServers(Collections.singletonList(new EquivalentAddressGroup(socketAddress)))
|
|
||||||
.setError(error)
|
|
||||||
.build();
|
|
||||||
channelBuilder.nameResolverFactory(nameResolverFactory);
|
|
||||||
// Name resolution is started as soon as channel is created.
|
|
||||||
createChannel();
|
|
||||||
FakeNameResolverFactory.FakeNameResolver resolver = nameResolverFactory.resolvers.get(0);
|
|
||||||
verify(mockLoadBalancer).handleNameResolutionError(same(error));
|
|
||||||
assertEquals(1, timer.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER));
|
|
||||||
|
|
||||||
timer.forwardNanos(RECONNECT_BACKOFF_INTERVAL_NANOS - 1);
|
|
||||||
assertEquals(0, resolver.refreshCalled);
|
|
||||||
|
|
||||||
timer.forwardNanos(1);
|
|
||||||
assertEquals(1, resolver.refreshCalled);
|
|
||||||
verify(mockLoadBalancer, times(2)).handleNameResolutionError(same(error));
|
|
||||||
|
|
||||||
// Verify an additional name resolution failure does not schedule another timer
|
|
||||||
resolver.refresh();
|
|
||||||
verify(mockLoadBalancer, times(3)).handleNameResolutionError(same(error));
|
|
||||||
assertEquals(1, timer.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER));
|
|
||||||
|
|
||||||
// Allow the next refresh attempt to succeed
|
|
||||||
resolver.error = null;
|
|
||||||
|
|
||||||
// For the second attempt, the backoff should occur at RECONNECT_BACKOFF_INTERVAL_NANOS * 2
|
|
||||||
timer.forwardNanos(RECONNECT_BACKOFF_INTERVAL_NANOS * 2 - 1);
|
|
||||||
assertEquals(2, resolver.refreshCalled);
|
|
||||||
timer.forwardNanos(1);
|
|
||||||
assertEquals(3, resolver.refreshCalled);
|
|
||||||
assertEquals(0, timer.numPendingTasks());
|
|
||||||
|
|
||||||
// Verify that the successful resolution reset the backoff policy
|
|
||||||
resolver.listener.onError(error);
|
|
||||||
timer.forwardNanos(RECONNECT_BACKOFF_INTERVAL_NANOS - 1);
|
|
||||||
assertEquals(3, resolver.refreshCalled);
|
|
||||||
timer.forwardNanos(1);
|
|
||||||
assertEquals(4, resolver.refreshCalled);
|
|
||||||
assertEquals(0, timer.numPendingTasks());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nameResolutionFailed_delayedTransportShutdownCancelsBackoff() {
|
|
||||||
Status error = Status.UNAVAILABLE.withCause(new Throwable("fake name resolution error"));
|
|
||||||
|
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
|
||||||
new FakeNameResolverFactory.Builder(expectedUri).setError(error).build();
|
|
||||||
channelBuilder.nameResolverFactory(nameResolverFactory);
|
|
||||||
// Name resolution is started as soon as channel is created.
|
|
||||||
createChannel();
|
|
||||||
verify(mockLoadBalancer).handleNameResolutionError(same(error));
|
|
||||||
|
|
||||||
FakeClock.ScheduledTask nameResolverBackoff = getNameResolverRefresh();
|
|
||||||
assertNotNull(nameResolverBackoff);
|
|
||||||
assertFalse(nameResolverBackoff.isCancelled());
|
|
||||||
|
|
||||||
// Add a pending call to the delayed transport
|
|
||||||
ClientCall<String, Integer> call = channel.newCall(method, CallOptions.DEFAULT);
|
|
||||||
Metadata headers = new Metadata();
|
|
||||||
call.start(mockCallListener, headers);
|
|
||||||
|
|
||||||
// The pending call on the delayed transport stops the name resolver backoff from cancelling
|
|
||||||
channel.shutdown();
|
|
||||||
assertFalse(nameResolverBackoff.isCancelled());
|
|
||||||
|
|
||||||
// Notify that a subchannel is ready, which drains the delayed transport
|
|
||||||
SubchannelPicker picker = mock(SubchannelPicker.class);
|
|
||||||
Status status = Status.UNAVAILABLE.withDescription("for test");
|
|
||||||
when(picker.pickSubchannel(any(PickSubchannelArgs.class)))
|
|
||||||
.thenReturn(PickResult.withDrop(status));
|
|
||||||
updateBalancingStateSafely(helper, READY, picker);
|
|
||||||
executor.runDueTasks();
|
|
||||||
verify(mockCallListener).onClose(same(status), any(Metadata.class));
|
|
||||||
|
|
||||||
assertTrue(nameResolverBackoff.isCancelled());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nameResolverReturnsEmptySubLists_resolutionRetry() throws Exception {
|
|
||||||
// The mock LB is set to reject the addresses.
|
|
||||||
when(mockLoadBalancer.acceptResolvedAddresses(isA(ResolvedAddresses.class))).thenReturn(false);
|
|
||||||
|
|
||||||
// Pass a FakeNameResolverFactory with an empty list and LB config
|
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
|
||||||
new FakeNameResolverFactory.Builder(expectedUri).build();
|
|
||||||
Map<String, Object> rawServiceConfig =
|
|
||||||
parseConfig("{\"loadBalancingConfig\": [ {\"mock_lb\": { \"setting1\": \"high\" } } ] }");
|
|
||||||
ManagedChannelServiceConfig parsedServiceConfig =
|
|
||||||
createManagedChannelServiceConfig(rawServiceConfig, null);
|
|
||||||
nameResolverFactory.nextConfigOrError.set(ConfigOrError.fromConfig(parsedServiceConfig));
|
|
||||||
channelBuilder.nameResolverFactory(nameResolverFactory);
|
|
||||||
createChannel();
|
|
||||||
|
|
||||||
// A resolution retry has been scheduled
|
|
||||||
assertEquals(1, timer.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nameResolverReturnsEmptySubLists_optionallyAllowed() throws Exception {
|
|
||||||
// Pass a FakeNameResolverFactory with an empty list and LB config
|
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
|
||||||
new FakeNameResolverFactory.Builder(expectedUri).build();
|
|
||||||
String rawLbConfig = "{ \"setting1\": \"high\" }";
|
|
||||||
Object parsedLbConfig = new Object();
|
|
||||||
Map<String, Object> rawServiceConfig =
|
|
||||||
parseConfig("{\"loadBalancingConfig\": [ {\"mock_lb\": " + rawLbConfig + " } ] }");
|
|
||||||
ManagedChannelServiceConfig parsedServiceConfig =
|
|
||||||
createManagedChannelServiceConfig(
|
|
||||||
rawServiceConfig,
|
|
||||||
new PolicySelection(
|
|
||||||
mockLoadBalancerProvider,
|
|
||||||
parsedLbConfig));
|
|
||||||
nameResolverFactory.nextConfigOrError.set(ConfigOrError.fromConfig(parsedServiceConfig));
|
|
||||||
channelBuilder.nameResolverFactory(nameResolverFactory);
|
|
||||||
createChannel();
|
|
||||||
|
|
||||||
// LoadBalancer received the empty list and the LB config
|
|
||||||
verify(mockLoadBalancerProvider).newLoadBalancer(any(Helper.class));
|
|
||||||
ArgumentCaptor<ResolvedAddresses> resultCaptor =
|
|
||||||
ArgumentCaptor.forClass(ResolvedAddresses.class);
|
|
||||||
verify(mockLoadBalancer).acceptResolvedAddresses(resultCaptor.capture());
|
|
||||||
assertThat(resultCaptor.getValue().getAddresses()).isEmpty();
|
|
||||||
assertThat(resultCaptor.getValue().getLoadBalancingPolicyConfig()).isEqualTo(parsedLbConfig);
|
|
||||||
|
|
||||||
// A no resolution retry
|
|
||||||
assertEquals(0, timer.numPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loadBalancerThrowsInHandleResolvedAddresses() {
|
public void loadBalancerThrowsInHandleResolvedAddresses() {
|
||||||
RuntimeException ex = new RuntimeException("simulated");
|
RuntimeException ex = new RuntimeException("simulated");
|
||||||
|
|
@ -3019,52 +2873,6 @@ public class ManagedChannelImplTest {
|
||||||
assertEquals(initialRefreshCount + 1, resolver.refreshCalled);
|
assertEquals(initialRefreshCount + 1, resolver.refreshCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void resetConnectBackoff() {
|
|
||||||
// Start with a name resolution failure to trigger backoff attempts
|
|
||||||
Status error = Status.UNAVAILABLE.withCause(new Throwable("fake name resolution error"));
|
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
|
||||||
new FakeNameResolverFactory.Builder(expectedUri).setError(error).build();
|
|
||||||
channelBuilder.nameResolverFactory(nameResolverFactory);
|
|
||||||
// Name resolution is started as soon as channel is created.
|
|
||||||
createChannel();
|
|
||||||
FakeNameResolverFactory.FakeNameResolver resolver = nameResolverFactory.resolvers.get(0);
|
|
||||||
verify(mockLoadBalancer).handleNameResolutionError(same(error));
|
|
||||||
|
|
||||||
FakeClock.ScheduledTask nameResolverBackoff = getNameResolverRefresh();
|
|
||||||
assertNotNull("There should be a name resolver backoff task", nameResolverBackoff);
|
|
||||||
assertEquals(0, resolver.refreshCalled);
|
|
||||||
|
|
||||||
// Verify resetConnectBackoff() calls refresh and cancels the scheduled backoff
|
|
||||||
channel.resetConnectBackoff();
|
|
||||||
assertEquals(1, resolver.refreshCalled);
|
|
||||||
assertTrue(nameResolverBackoff.isCancelled());
|
|
||||||
|
|
||||||
// Simulate a race between cancel and the task scheduler. Should be a no-op.
|
|
||||||
nameResolverBackoff.command.run();
|
|
||||||
assertEquals(1, resolver.refreshCalled);
|
|
||||||
|
|
||||||
// Verify that the reconnect policy was recreated and the backoff multiplier reset to 1
|
|
||||||
timer.forwardNanos(RECONNECT_BACKOFF_INTERVAL_NANOS);
|
|
||||||
assertEquals(2, resolver.refreshCalled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void resetConnectBackoff_noOpWithoutPendingResolverBackoff() {
|
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
|
||||||
new FakeNameResolverFactory.Builder(expectedUri)
|
|
||||||
.setServers(Collections.singletonList(new EquivalentAddressGroup(socketAddress)))
|
|
||||||
.build();
|
|
||||||
channelBuilder.nameResolverFactory(nameResolverFactory);
|
|
||||||
createChannel();
|
|
||||||
FakeNameResolverFactory.FakeNameResolver nameResolver = nameResolverFactory.resolvers.get(0);
|
|
||||||
assertEquals(0, nameResolver.refreshCalled);
|
|
||||||
|
|
||||||
channel.resetConnectBackoff();
|
|
||||||
|
|
||||||
assertEquals(0, nameResolver.refreshCalled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resetConnectBackoff_noOpWhenChannelShutdown() {
|
public void resetConnectBackoff_noOpWhenChannelShutdown() {
|
||||||
FakeNameResolverFactory nameResolverFactory =
|
FakeNameResolverFactory nameResolverFactory =
|
||||||
|
|
@ -4517,10 +4325,6 @@ public class ManagedChannelImplTest {
|
||||||
return instrumented.getStats().get();
|
return instrumented.getStats().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private FakeClock.ScheduledTask getNameResolverRefresh() {
|
|
||||||
return Iterables.getOnlyElement(timer.getPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods to call methods from SynchronizationContext
|
// Helper methods to call methods from SynchronizationContext
|
||||||
private static Subchannel createSubchannelSafely(
|
private static Subchannel createSubchannelSafely(
|
||||||
final Helper helper, final EquivalentAddressGroup addressGroup, final Attributes attrs,
|
final Helper helper, final EquivalentAddressGroup addressGroup, final Attributes attrs,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.internal;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import io.grpc.NameResolver;
|
||||||
|
import io.grpc.NameResolver.Listener2;
|
||||||
|
import io.grpc.NameResolver.ResolutionResult;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.SynchronizationContext;
|
||||||
|
import io.grpc.internal.RetryingNameResolver.ResolutionResultListener;
|
||||||
|
import java.lang.Thread.UncaughtExceptionHandler;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnit;
|
||||||
|
import org.mockito.junit.MockitoRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for {@link RetryingNameResolver}.
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class RetryingNameResolverTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final MockitoRule mocks = MockitoJUnit.rule();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private NameResolver mockNameResolver;
|
||||||
|
@Mock
|
||||||
|
private Listener2 mockListener;
|
||||||
|
@Mock
|
||||||
|
private RetryScheduler mockRetryScheduler;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Listener2> listenerCaptor;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<ResolutionResult> onResultCaptor;
|
||||||
|
private final SynchronizationContext syncContext = new SynchronizationContext(
|
||||||
|
mock(UncaughtExceptionHandler.class));
|
||||||
|
|
||||||
|
private RetryingNameResolver retryingNameResolver;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
retryingNameResolver = new RetryingNameResolver(mockNameResolver, mockRetryScheduler,
|
||||||
|
syncContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startAndShutdown() {
|
||||||
|
retryingNameResolver.start(mockListener);
|
||||||
|
retryingNameResolver.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the ResolutionResultListener callback is added to the ResolutionResult attributes,
|
||||||
|
// and the retry scheduler is reset since the name resolution was successful.
|
||||||
|
@Test
|
||||||
|
public void onResult_sucess() {
|
||||||
|
retryingNameResolver.start(mockListener);
|
||||||
|
verify(mockNameResolver).start(listenerCaptor.capture());
|
||||||
|
|
||||||
|
listenerCaptor.getValue().onResult(ResolutionResult.newBuilder().build());
|
||||||
|
verify(mockListener).onResult(onResultCaptor.capture());
|
||||||
|
ResolutionResultListener resolutionResultListener = onResultCaptor.getValue()
|
||||||
|
.getAttributes()
|
||||||
|
.get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY);
|
||||||
|
assertThat(resolutionResultListener).isNotNull();
|
||||||
|
|
||||||
|
resolutionResultListener.resolutionAttempted(true);
|
||||||
|
verify(mockRetryScheduler).reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the ResolutionResultListener callback is added to the ResolutionResult attributes,
|
||||||
|
// and that a retry gets scheduled when the resolution results are rejected.
|
||||||
|
@Test
|
||||||
|
public void onResult_failure() {
|
||||||
|
retryingNameResolver.start(mockListener);
|
||||||
|
verify(mockNameResolver).start(listenerCaptor.capture());
|
||||||
|
|
||||||
|
listenerCaptor.getValue().onResult(ResolutionResult.newBuilder().build());
|
||||||
|
verify(mockListener).onResult(onResultCaptor.capture());
|
||||||
|
ResolutionResultListener resolutionResultListener = onResultCaptor.getValue()
|
||||||
|
.getAttributes()
|
||||||
|
.get(RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY);
|
||||||
|
assertThat(resolutionResultListener).isNotNull();
|
||||||
|
|
||||||
|
resolutionResultListener.resolutionAttempted(false);
|
||||||
|
verify(mockRetryScheduler).schedule(isA(Runnable.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapping a NameResolver more than once is a misconfiguration.
|
||||||
|
@Test
|
||||||
|
public void onResult_failure_doubleWrapped() {
|
||||||
|
NameResolver doubleWrappedResolver = new RetryingNameResolver(retryingNameResolver,
|
||||||
|
mockRetryScheduler, syncContext);
|
||||||
|
|
||||||
|
doubleWrappedResolver.start(mockListener);
|
||||||
|
verify(mockNameResolver).start(listenerCaptor.capture());
|
||||||
|
|
||||||
|
try {
|
||||||
|
listenerCaptor.getValue().onResult(ResolutionResult.newBuilder().build());
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("can only be used once");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fail("An exception should have been thrown for a double wrapped NAmeResolver");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A retry should get scheduled when name resolution fails.
|
||||||
|
@Test
|
||||||
|
public void onError() {
|
||||||
|
retryingNameResolver.start(mockListener);
|
||||||
|
verify(mockNameResolver).start(listenerCaptor.capture());
|
||||||
|
listenerCaptor.getValue().onError(Status.DEADLINE_EXCEEDED);
|
||||||
|
verify(mockListener).onError(Status.DEADLINE_EXCEEDED);
|
||||||
|
verify(mockRetryScheduler).schedule(isA(Runnable.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -98,7 +98,7 @@ public class ServiceConfigErrorHandlingTest {
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldAccept(Runnable command) {
|
public boolean shouldAccept(Runnable command) {
|
||||||
return command.toString().contains(
|
return command.toString().contains(
|
||||||
ManagedChannelImpl.DelayedNameResolverRefresh.class.getName());
|
RetryingNameResolver.DelayedNameResolverRefresh.class.getName());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -542,7 +542,7 @@ public class ServiceConfigErrorHandlingTest {
|
||||||
final URI expectedUri;
|
final URI expectedUri;
|
||||||
final List<EquivalentAddressGroup> servers;
|
final List<EquivalentAddressGroup> servers;
|
||||||
final boolean resolvedAtStart;
|
final boolean resolvedAtStart;
|
||||||
final ArrayList<FakeNameResolver> resolvers = new ArrayList<>();
|
final ArrayList<RetryingNameResolver> resolvers = new ArrayList<>();
|
||||||
final AtomicReference<Map<String, ?>> nextRawServiceConfig = new AtomicReference<>();
|
final AtomicReference<Map<String, ?>> nextRawServiceConfig = new AtomicReference<>();
|
||||||
final AtomicReference<Attributes> nextAttributes = new AtomicReference<>(Attributes.EMPTY);
|
final AtomicReference<Attributes> nextAttributes = new AtomicReference<>(Attributes.EMPTY);
|
||||||
|
|
||||||
|
|
@ -561,7 +561,13 @@ public class ServiceConfigErrorHandlingTest {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
assertEquals(DEFAULT_PORT, args.getDefaultPort());
|
assertEquals(DEFAULT_PORT, args.getDefaultPort());
|
||||||
FakeNameResolver resolver = new FakeNameResolver(args.getServiceConfigParser());
|
RetryingNameResolver resolver = new RetryingNameResolver(
|
||||||
|
new FakeNameResolver(args.getServiceConfigParser()),
|
||||||
|
new BackoffPolicyRetryScheduler(
|
||||||
|
new FakeBackoffPolicyProvider(),
|
||||||
|
args.getScheduledExecutorService(),
|
||||||
|
args.getSynchronizationContext()),
|
||||||
|
args.getSynchronizationContext());
|
||||||
resolvers.add(resolver);
|
resolvers.add(resolver);
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
@ -572,8 +578,8 @@ public class ServiceConfigErrorHandlingTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
void allResolved() {
|
void allResolved() {
|
||||||
for (FakeNameResolver resolver : resolvers) {
|
for (RetryingNameResolver resolver : resolvers) {
|
||||||
resolver.resolved();
|
((FakeNameResolver)resolver.getRetriedNameResolver()).resolved();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -647,7 +653,8 @@ public class ServiceConfigErrorHandlingTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private FakeClock.ScheduledTask getNameResolverRefresh() {
|
private FakeClock.ScheduledTask getNameResolverRefresh() {
|
||||||
return Iterables.getOnlyElement(timer.getPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER), null);
|
return Iterables.getOnlyElement(
|
||||||
|
timer.getPendingTasks(NAME_RESOLVER_REFRESH_TASK_FILTER), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class FakeLoadBalancer extends LoadBalancer {
|
private static class FakeLoadBalancer extends LoadBalancer {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue