xds: Implement an xDS NameResolver/Provider for xDS load balancing alpha release (#6052)

* Implemented an XdsNameResolver that always returns a hard-coded service config

* Implemented XdsNameResolverProvider

* Added unit tests for XdsNameResolver and XdsNameResolverProvider

* Added META-INF file for XdsNameReresolverProvider

* Removed balancer name field in hard-coded service config

* Changed URI scheme to xds-experimental.

* Deleted unnecessary executors for running name resolution in a separate thread.

* Fixed nits.

* Fixed usage of GrpcUtil.getDefaultProxyDetector() as it was deleted.

* Removed unnecessary shutdown implementation.

* Replaced return with AssertionError as it hard-coded service config should never have error.

* Removed unused name resolver args.

* Added tail blank line.
This commit is contained in:
Chengyuan Zhang 2019-08-19 16:39:36 -07:00 committed by GitHub
parent 9fa2608430
commit 1d04601313
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 378 additions and 0 deletions

View File

@ -0,0 +1,92 @@
/*
* Copyright 2019 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Preconditions;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.JsonParser;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
/**
* A {@link NameResolver} for resolving gRPC target names with "xds-experimental" scheme.
*
* <p>The implementation is for load balancing alpha release only. No actual VHDS is involved. It
* always returns a hard-coded service config that selects the xds_experimental LB policy with
* round-robin as the child policy.
*
* @see XdsNameResolverProvider
*/
final class XdsNameResolver extends NameResolver {
private static final String SERVICE_CONFIG_HARDCODED = "{"
+ "\"loadBalancingConfig\": ["
+ "{\"xds_experimental\" : {"
+ "\"childPolicy\" : [{\"round_robin\" : {}}]"
+ "}}"
+ "]}";
private final String authority;
XdsNameResolver(String name) {
URI nameUri = URI.create("//" + checkNotNull(name, "name"));
Preconditions.checkArgument(nameUri.getHost() != null, "Invalid hostname: %s", name);
authority =
Preconditions.checkNotNull(
nameUri.getAuthority(), "nameUri (%s) doesn't have an authority", nameUri);
}
@Override
public String getServiceAuthority() {
return authority;
}
@SuppressWarnings("unchecked")
@Override
public void start(final Listener2 listener) {
Map<String, ?> config;
try {
config = (Map<String, ?>) JsonParser.parse(SERVICE_CONFIG_HARDCODED);
} catch (IOException e) {
listener.onError(
Status.UNKNOWN.withDescription("Invalid service config").withCause(e));
throw new AssertionError("Invalid service config");
}
Attributes attrs =
Attributes.newBuilder()
.set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, config)
.build();
ResolutionResult result =
ResolutionResult.newBuilder()
.setAddresses(Collections.<EquivalentAddressGroup>emptyList())
.setAttributes(attrs)
.build();
listener.onResult(result);
}
@Override
public void shutdown() {
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2019 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import com.google.common.base.Preconditions;
import io.grpc.NameResolver.Args;
import io.grpc.NameResolverProvider;
import java.net.URI;
/**
* A provider for {@link XdsNameResolver}.
*
* <p>It resolves a target URI whose scheme is {@code "xds-experimental"}. The authority of the
* target URI is never used for current release. The path of the target URI, excluding the leading
* slash {@code '/'}, will indicate the name to use in the VHDS query.
*
* <p>This class should not be directly referenced in code. The resolver should be accessed
* through {@link io.grpc.NameResolverRegistry#asFactory#newNameResolver(URI, Args)} with the URI
* scheme "xds-experimental".
*/
public final class XdsNameResolverProvider extends NameResolverProvider {
private static final String SCHEME = "xds-experimental";
@Override
public XdsNameResolver newNameResolver(URI targetUri, Args args) {
if (SCHEME.equals(targetUri.getScheme())) {
String targetPath = Preconditions.checkNotNull(targetUri.getPath(), "targetPath");
Preconditions.checkArgument(
targetPath.startsWith("/"),
"the path component (%s) of the target (%s) must start with '/'",
targetPath,
targetUri);
String name = targetPath.substring(1);
return new XdsNameResolver(name);
}
return null;
}
@Override
public String getDefaultScheme() {
return SCHEME;
}
@Override
protected boolean isAvailable() {
return true;
}
@Override
protected int priority() {
// Set priority value to be < 5 as we still want DNS resolver to be the primary default
// resolver.
return 4;
}
}

View File

@ -0,0 +1 @@
io.grpc.xds.XdsNameResolverProvider

View File

@ -0,0 +1,132 @@
/*
* Copyright 2019 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.common.collect.Iterables;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.SynchronizationContext;
import io.grpc.internal.GrpcAttributes;
import io.grpc.internal.GrpcUtil;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
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 tests for {@link XdsNameResolver}. */
@RunWith(JUnit4.class)
public class XdsNamResolverTest {
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
throw new AssertionError(e);
}
});
private final NameResolver.Args args =
NameResolver.Args.newBuilder()
.setDefaultPort(8080)
.setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(mock(ServiceConfigParser.class))
.build();
private final XdsNameResolverProvider provider = new XdsNameResolverProvider();
@Mock private NameResolver.Listener2 mockListener;
@Captor private ArgumentCaptor<ResolutionResult> resultCaptor;
@Test
public void validName_withAuthority() {
XdsNameResolver resolver =
provider.newNameResolver(
URI.create("xds-experimental://trafficdirector.google.com/foo.googleapis.com"), args);
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("foo.googleapis.com");
}
@Test
public void validName_noAuthority() {
XdsNameResolver resolver =
provider.newNameResolver(URI.create("xds-experimental:///foo.googleapis.com"), args);
assertThat(resolver).isNotNull();
assertThat(resolver.getServiceAuthority()).isEqualTo("foo.googleapis.com");
}
@Test
public void invalidName_hostnameContainsUnderscore() {
try {
provider.newNameResolver(URI.create("xds-experimental:///foo_bar.googleapis.com"), args);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected
}
}
@Test
public void resolve_hardcodedResult() {
XdsNameResolver resolver = newResolver("foo.googleapis.com");
resolver.start(mockListener);
verify(mockListener).onResult(resultCaptor.capture());
assertHardCodedServiceConfig(resultCaptor.getValue());
resolver = newResolver("bar.googleapis.com");
resolver.start(mockListener);
verify(mockListener, times(2)).onResult(resultCaptor.capture());
assertHardCodedServiceConfig(resultCaptor.getValue());
}
@SuppressWarnings("unchecked")
private static void assertHardCodedServiceConfig(ResolutionResult actualResult) {
assertThat(actualResult.getAddresses()).isEmpty();
Map<String, ?> serviceConfig =
actualResult.getAttributes().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG);
List<Map<String, ?>> rawLbConfigs =
(List<Map<String, ?>>) serviceConfig.get("loadBalancingConfig");
Map<String, ?> xdsLbConfig = Iterables.getOnlyElement(rawLbConfigs);
assertThat(xdsLbConfig.keySet()).containsExactly("xds_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) xdsLbConfig.get("xds_experimental");
assertThat(rawConfigValues)
.containsExactly("childPolicy",
Collections.singletonList(
Collections.singletonMap("round_robin", Collections.EMPTY_MAP)));
}
private XdsNameResolver newResolver(String name) {
return new XdsNameResolver(name);
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2019 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import io.grpc.InternalServiceProviders;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.NameResolverProvider;
import io.grpc.SynchronizationContext;
import io.grpc.internal.GrpcUtil;
import java.net.URI;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link XdsNameResolverProvider}. */
@RunWith(JUnit4.class)
public class XdsNameResolverProviderTest {
private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
throw new AssertionError(e);
}
});
private final NameResolver.Args args = NameResolver.Args.newBuilder()
.setDefaultPort(8080)
.setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(mock(ServiceConfigParser.class))
.build();
private XdsNameResolverProvider provider = new XdsNameResolverProvider();
@Test
public void provided() {
for (NameResolverProvider current
: InternalServiceProviders.getCandidatesViaServiceLoader(
NameResolverProvider.class, getClass().getClassLoader())) {
if (current instanceof XdsNameResolverProvider) {
return;
}
}
fail("XdsNameResolverProvider not registered");
}
@Test
public void isAvailable() {
assertThat(provider.isAvailable()).isTrue();
}
@Test
public void newNameResolver() {
assertThat(
provider.newNameResolver(URI.create("xds-experimental://1.1.1.1/foo.googleapis.com"), args))
.isInstanceOf(XdsNameResolver.class);
assertThat(
provider.newNameResolver(URI.create("xds-experimental:///foo.googleapis.com"), args))
.isInstanceOf(XdsNameResolver.class);
assertThat(
provider.newNameResolver(URI.create("notxds-experimental://1.1.1.1/foo.googleapis.com"),
args))
.isNull();
}
}