Move name resolution retry from managed channel to name resolver. (#9758)

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:
Terry Wilson 2022-12-16 15:30:57 -08:00 committed by GitHub
parent 46ed02ed72
commit 43bc578f20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 455 additions and 337 deletions

View File

@ -59,6 +59,10 @@ import javax.annotation.concurrent.ThreadSafe;
*/ */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770") @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
public abstract class NameResolver { public abstract class NameResolver {
// Outside listeners that get notified of the result of each name resolution.
private ArrayList<ResolutionResultListener> resolutionResultListeners = new ArrayList<>();
/** /**
* Returns the authority used to authenticate connections to servers. It <strong>must</strong> be * Returns the authority used to authenticate connections to servers. It <strong>must</strong> be
* from a trusted source, because if the authority is tampered with, RPCs may be sent to the * from a trusted source, because if the authority is tampered with, RPCs may be sent to the
@ -91,8 +95,9 @@ public abstract class NameResolver {
} }
@Override @Override
public void onResult(ResolutionResult resolutionResult) { public boolean onResult(ResolutionResult resolutionResult) {
listener.onAddresses(resolutionResult.getAddresses(), resolutionResult.getAttributes()); listener.onAddresses(resolutionResult.getAddresses(), resolutionResult.getAttributes());
return true;
} }
}); });
} }
@ -131,6 +136,43 @@ public abstract class NameResolver {
*/ */
public void refresh() {} public void refresh() {}
/**
* Adds a new {@link ResolutionResultListener} that will get notified of the outcome of each
* resolution.
*
* @since 1.53.0
*/
public final void addResolutionResultListener(ResolutionResultListener listener) {
checkArgument(listener != null, "listener");
resolutionResultListeners.add(listener);
}
/**
* Removes an existing {@link ResolutionResultListener}.
*
* @return {@code true} if the listener was removed, otherwise {@code false}
* @since 1.53.0
*/
public final boolean removeResolutionResultListener(ResolutionResultListener listener) {
checkArgument(listener != null);
return resolutionResultListeners.remove(listener);
}
/**
* Intended for extending classes to call when they know the result of a name resolution.
*
* <p>Note that while these listeners can be added to any {@link NameResolver}, only concrete
* implementations that call this method will actually support this facility.
*
* @param successful {@code true} if resolution was successful and the addresses were accepted.
* @since 1.53.0
*/
protected final void fireResolutionResultEvent(boolean successful) {
for (ResolutionResultListener listener : resolutionResultListeners) {
listener.resolutionAttempted(successful);
}
}
/** /**
* Factory that creates {@link NameResolver} instances. * Factory that creates {@link NameResolver} instances.
* *
@ -225,9 +267,12 @@ public abstract class NameResolver {
* {@link ResolutionResult#getAddresses()} is empty, {@link #onError(Status)} will be called. * {@link ResolutionResult#getAddresses()} is empty, {@link #onError(Status)} will be called.
* *
* @param resolutionResult the resolved server addresses, attributes, and Service Config. * @param resolutionResult the resolved server addresses, attributes, and Service Config.
* @return {@code true} if the listener accepts the resolved addresses, otherwise {@code false}.
* If the addresses are not accepted the {@link NameResolver} will refresh and retry
* later if it uses polling.
* @since 1.21.0 * @since 1.21.0
*/ */
public abstract void onResult(ResolutionResult resolutionResult); public abstract boolean onResult(ResolutionResult resolutionResult);
/** /**
* Handles a name resolving error from the resolver. The listener is responsible for eventually * Handles a name resolving error from the resolver. The listener is responsible for eventually
@ -249,6 +294,24 @@ public abstract class NameResolver {
@Documented @Documented
public @interface ResolutionResultAttr {} public @interface ResolutionResultAttr {}
/**
* A callback interface called at the end of every resolve operation to indicate if the operation
* was successful. Success means that there were no problems with either the name resolution part
* nor with {@link Listener} accepting the resolution results.
*/
public interface ResolutionResultListener {
/**
* Called after an attempt at name resolution.
*
* <p>Note! Implementations of this should return quickly and not throw exceptions.
*
* @param successful {@code true} if resolution was successful and the addresses were accepted.
*/
void resolutionAttempted(boolean successful);
}
/** /**
* Information that a {@link Factory} uses to create a {@link NameResolver}. * Information that a {@link Factory} uses to create a {@link NameResolver}.
* *

View File

@ -318,6 +318,7 @@ public class DnsNameResolver extends NameResolver {
result = doResolve(false); result = doResolve(false);
if (result.error != null) { if (result.error != null) {
savedListener.onError(result.error); savedListener.onError(result.error);
fireResolutionResultEvent(false);
return; return;
} }
if (result.addresses != null) { if (result.addresses != null) {
@ -330,7 +331,7 @@ public class DnsNameResolver extends NameResolver {
resolutionResultBuilder.setAttributes(result.attributes); resolutionResultBuilder.setAttributes(result.attributes);
} }
} }
savedListener.onResult(resolutionResultBuilder.build()); fireResolutionResultEvent(savedListener.onResult(resolutionResultBuilder.build()));
} catch (IOException e) { } catch (IOException e) {
savedListener.onError( savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e)); Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));

View File

@ -47,19 +47,23 @@ 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 ExponentialBackoffPolicy.Provider(),
args.getScheduledExecutorService(),
args.getSynchronizationContext());
} else { } else {
return null; return null;
} }

View File

@ -280,6 +280,10 @@ final class ManagedChannelImpl extends ManagedChannel implements
// Must be mutated and read from constructor or syncContext // Must be mutated and read from constructor or syncContext
// used for channel tracing when value changed // used for channel tracing when value changed
private ManagedChannelServiceConfig lastServiceConfig = EMPTY_SERVICE_CONFIG; private ManagedChannelServiceConfig lastServiceConfig = EMPTY_SERVICE_CONFIG;
// Must be mutated and read from constructor or syncContext
// Denotes if the last resolved addresses were accepted by the load balancer. A {@code null}
// value indicates no attempt has been made yet.
private Boolean lastAddressesAccepted;
@Nullable @Nullable
private final ManagedChannelServiceConfig defaultServiceConfig; private final ManagedChannelServiceConfig defaultServiceConfig;
@ -367,7 +371,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 +453,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) {
@ -1290,7 +1261,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();
} }
} }
@ -1337,9 +1308,9 @@ final class ManagedChannelImpl extends ManagedChannel implements
if (shutdown.get()) { if (shutdown.get()) {
return; return;
} }
if (scheduledNameResolverRefresh != null && scheduledNameResolverRefresh.isPending()) { if (lastAddressesAccepted != null && !lastAddressesAccepted) {
checkState(nameResolverStarted, "name resolver must be started"); checkState(nameResolverStarted, "name resolver must be started");
refreshAndResetNameResolution(); refreshNameResolution();
} }
for (InternalSubchannel subchannel : subchannels) { for (InternalSubchannel subchannel : subchannels) {
subchannel.resetConnectBackoff(); subchannel.resetConnectBackoff();
@ -1495,7 +1466,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();
} }
} }
@ -1736,7 +1707,7 @@ final class ManagedChannelImpl extends ManagedChannel implements
} }
@Override @Override
public void onResult(final ResolutionResult resolutionResult) { public boolean onResult(final ResolutionResult resolutionResult) {
final class NamesResolved implements Runnable { final class NamesResolved implements Runnable {
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
@ -1745,6 +1716,7 @@ final class ManagedChannelImpl extends ManagedChannel implements
if (ManagedChannelImpl.this.nameResolver != resolver) { if (ManagedChannelImpl.this.nameResolver != resolver) {
return; return;
} }
lastAddressesAccepted = false;
List<EquivalentAddressGroup> servers = resolutionResult.getAddresses(); List<EquivalentAddressGroup> servers = resolutionResult.getAddresses();
channelLogger.log( channelLogger.log(
@ -1758,7 +1730,6 @@ final class ManagedChannelImpl extends ManagedChannel implements
lastResolutionState = ResolutionState.SUCCESS; lastResolutionState = ResolutionState.SUCCESS;
} }
nameResolverBackoffPolicy = null;
ConfigOrError configOrError = resolutionResult.getServiceConfig(); ConfigOrError configOrError = resolutionResult.getServiceConfig();
InternalConfigSelector resolvedConfigSelector = InternalConfigSelector resolvedConfigSelector =
resolutionResult.getAttributes().get(InternalConfigSelector.KEY); resolutionResult.getAttributes().get(InternalConfigSelector.KEY);
@ -1816,6 +1787,7 @@ 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());
lastAddressesAccepted = false;
return; return;
} else { } else {
effectiveServiceConfig = lastServiceConfig; effectiveServiceConfig = lastServiceConfig;
@ -1859,21 +1831,24 @@ final class ManagedChannelImpl extends ManagedChannel implements
} }
Attributes attributes = attrBuilder.build(); Attributes attributes = attrBuilder.build();
boolean addressesAccepted = helper.lb.tryAcceptResolvedAddresses( 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 (!addressesAccepted) {
scheduleExponentialBackOffInSyncContext();
}
} }
} }
} }
syncContext.execute(new NamesResolved()); syncContext.execute(new NamesResolved());
// If NameResolved did not assign a value to lastAddressesAccepted, we assume there was an
// exception and set it to false.
if (lastAddressesAccepted == null) {
lastAddressesAccepted = false;
}
return lastAddressesAccepted;
} }
@Override @Override
@ -1903,29 +1878,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());
} }
} }

View File

@ -0,0 +1,80 @@
/*
* Copyright 2022 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.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 RetryScheduler {
private final Runnable retryOperation;
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(RetryScheduler.class.getName());
RetryScheduler(Runnable retryOperation, ScheduledExecutorService scheduledExecutorService,
SynchronizationContext syncContext, BackoffPolicy.Provider policyProvider) {
this.retryOperation = retryOperation;
this.scheduledExecutorService = scheduledExecutorService;
this.syncContext = syncContext;
this.policyProvider = policyProvider;
}
/**
* Schedules a future retry operation. Only allows one retry to be scheduled at any given time.
*
* @return The delay in nanos before the operation fires or -1 if it was not scheduled.
*/
long schedule() {
if (policy == null) {
policy = policyProvider.get();
}
// If a retry is already scheduled, take no further action.
if (scheduledHandle != null && scheduledHandle.isPending()) {
return -1;
}
long delayNanos = policy.nextBackoffNanos();
scheduledHandle = syncContext.schedule(retryOperation, delayNanos, TimeUnit.NANOSECONDS,
scheduledExecutorService);
logger.fine("Scheduling DNS resolution backoff for " + delayNanos + "ns");
return delayNanos;
}
/**
* Resets the {@link RetryScheduler} and cancels any pending retry task. The policy will be
* cleared thus also resetting any state associated with it (e.g. a backoff multiplier).
*/
void reset() {
if (scheduledHandle != null && scheduledHandle.isPending()) {
scheduledHandle.cancel();
}
policy = null;
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2022 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.NameResolver;
import io.grpc.SynchronizationContext;
import java.util.concurrent.ScheduledExecutorService;
/**
* 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;
/**
* Creates a new {@link RetryingNameResolver}.
*
* @param retriedNameResolver A {@link NameResolver} that will have failed attempt retried.
* @param backoffPolicyProvider Provides the policy used to backoff from retry attempts
* @param scheduledExecutorService Executes any retry attempts
* @param syncContext All retries happen within the given {@code SyncContext}
*/
RetryingNameResolver(NameResolver retriedNameResolver,
BackoffPolicy.Provider backoffPolicyProvider,
ScheduledExecutorService scheduledExecutorService,
SynchronizationContext syncContext) {
super(retriedNameResolver);
this.retriedNameResolver = retriedNameResolver;
this.retriedNameResolver.addResolutionResultListener(new RetryResolutionResultListener());
this.retryScheduler = new RetryScheduler(new DelayedNameResolverRefresh(),
scheduledExecutorService, syncContext, backoffPolicyProvider);
}
@Override
public void shutdown() {
super.shutdown();
retryScheduler.reset();
}
/**
* @return The {@link NameResolver} that is getting its failed attempts retried.
*/
public NameResolver getRetriedNameResolver() {
return retriedNameResolver;
}
@VisibleForTesting
class DelayedNameResolverRefresh implements Runnable {
@Override
public void run() {
refresh();
}
}
private class RetryResolutionResultListener implements ResolutionResultListener {
@Override
public void resolutionAttempted(boolean successful) {
if (successful) {
retryScheduler.reset();
} else {
retryScheduler.schedule();
}
}
}
}

View File

@ -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));
} }

View File

@ -25,6 +25,7 @@ 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.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;
@ -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,29 @@ 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 ExponentialBackoffPolicy.Provider(),
fakeExecutor.getScheduledExecutorService(),
syncContext
);
} }
@Before @Before
@ -203,6 +222,9 @@ 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.
when(mockListener.onResult(isA(ResolutionResult.class))).thenReturn(true);
} }
@After @After
@ -216,12 +238,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 +303,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,6 +320,7 @@ 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(); resolver.shutdown();
@ -313,16 +331,18 @@ 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(); resolver.shutdown();
@ -341,6 +361,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,17 +372,19 @@ 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(); resolver.shutdown();
@ -376,13 +399,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());
@ -409,13 +433,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());
@ -444,12 +469,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,6 +489,7 @@ 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(); resolver.shutdown();
@ -487,11 +514,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,6 +539,7 @@ 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(); resolver.shutdown();
@ -520,8 +549,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,9 +561,9 @@ 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);
@ -543,6 +573,45 @@ public class DnsNameResolverTest {
assertThat(ac.getValue().getAttributes()).isEqualTo(Attributes.EMPTY); 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 {
when(mockListener.onResult(isA(ResolutionResult.class))).thenReturn(false);
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);
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().getAttributes()).isEqualTo(Attributes.EMPTY);
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 +623,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());
@ -568,6 +638,9 @@ public class DnsNameResolverTest {
verify(mockAddressResolver).resolveAddress(name); verify(mockAddressResolver).resolveAddress(name);
assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY); 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 +651,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 +691,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 +711,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 +725,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); NameResolver resolver = newResolver(name, 81).getRetriedNameResolver();
resolver.setAddressResolver(mockAddressResolver); DnsNameResolver dnsResolver = (DnsNameResolver)resolver;
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 +736,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 +754,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());
@ -681,6 +770,9 @@ public class DnsNameResolverTest {
assertThat(result.getAttributes()).isEqualTo(Attributes.EMPTY); 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 +787,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());
@ -711,6 +804,9 @@ public class DnsNameResolverTest {
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 +853,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 +874,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 +1285,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());

View File

@ -80,8 +80,8 @@ public class ForwardingNameResolverTest {
public void start_observer() { public void start_observer() {
NameResolver.Listener2 listener = new NameResolver.Listener2() { NameResolver.Listener2 listener = new NameResolver.Listener2() {
@Override @Override
public void onResult(ResolutionResult result) { public boolean onResult(ResolutionResult result) {
return true;
} }
@Override @Override

View File

@ -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(null); ArgumentCaptor<Helper> helperCaptor = ArgumentCaptor.forClass(null);
@ -1060,139 +1047,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");
@ -3016,36 +2870,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 @Test
public void resetConnectBackoff_noOpWithoutPendingResolverBackoff() { public void resetConnectBackoff_noOpWithoutPendingResolverBackoff() {
FakeNameResolverFactory nameResolverFactory = FakeNameResolverFactory nameResolverFactory =
@ -4514,10 +4338,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,

View File

@ -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,11 @@ 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 FakeBackoffPolicyProvider(),
args.getScheduledExecutorService(),
args.getSynchronizationContext());
resolvers.add(resolver); resolvers.add(resolver);
return resolver; return resolver;
} }
@ -572,8 +576,8 @@ public class ServiceConfigErrorHandlingTest {
} }
void allResolved() { void allResolved() {
for (FakeNameResolver resolver : resolvers) { for (RetryingNameResolver resolver : resolvers) {
resolver.resolved(); ((FakeNameResolver)resolver.getRetriedNameResolver()).resolved();
} }
} }
@ -613,7 +617,7 @@ public class ServiceConfigErrorHandlingTest {
.setServiceConfig(serviceConfigParser.parseServiceConfig(rawServiceConfig)); .setServiceConfig(serviceConfigParser.parseServiceConfig(rawServiceConfig));
} }
listener.onResult(builder.build()); fireResolutionResultEvent(listener.onResult(builder.build()));
} }
@Override public void shutdown() { @Override public void shutdown() {
@ -647,7 +651,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 {

View File

@ -602,7 +602,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer {
private class NameResolverListener extends NameResolver.Listener2 { private class NameResolverListener extends NameResolver.Listener2 {
@Override @Override
public void onResult(final ResolutionResult resolutionResult) { public boolean onResult(final ResolutionResult resolutionResult) {
class NameResolved implements Runnable { class NameResolved implements Runnable {
@Override @Override
public void run() { public void run() {
@ -634,6 +634,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer {
} }
syncContext.execute(new NameResolved()); syncContext.execute(new NameResolved());
return true;
} }
@Override @Override