core: apply service config

This commit is contained in:
Carl Mastrangelo 2018-04-04 17:45:12 -07:00 committed by GitHub
parent 49c8bdb60a
commit 2df76cc710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1460 additions and 394 deletions

View File

@ -164,7 +164,7 @@ final class AutoConfiguredLoadBalancerFactory extends LoadBalancer.Factory {
}
String serviceConfigChoiceBalancingPolicy =
ServiceConfigUtil.getLoadBalancingPolicy(config);
ServiceConfigUtil.getLoadBalancingPolicyFromServiceConfig(config);
// Check for an explicitly present lb choice
if (serviceConfigChoiceBalancingPolicy != null) {

View File

@ -35,9 +35,12 @@ import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -63,6 +66,22 @@ final class DnsNameResolver extends NameResolver {
private static final boolean JNDI_AVAILABLE = jndiAvailable();
private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
static final String SERVICE_CONFIG_PREFIX = "_grpc_config=";
private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
Collections.unmodifiableSet(
new HashSet<String>(
Arrays.asList(
SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
// From https://github.com/grpc/proposal/blob/master/A5-grpclb-in-dns.md
@ -196,11 +215,10 @@ final class DnsNameResolver extends NameResolver {
Map<String, Object> serviceConfig = null;
try {
for (Map<String, Object> possibleConfig :
ServiceConfigUtil.parseTxtResults(resolvedInetAddrs.txtRecords)) {
parseTxtResults(resolvedInetAddrs.txtRecords)) {
try {
serviceConfig =
ServiceConfigUtil.maybeChooseServiceConfig(
possibleConfig, random, getLocalHostname());
maybeChooseServiceConfig(possibleConfig, random, getLocalHostname());
} catch (RuntimeException e) {
logger.log(Level.WARNING, "Bad service config choice " + possibleConfig, e);
}
@ -265,6 +283,120 @@ final class DnsNameResolver extends NameResolver {
this.delegateResolver = delegateResolver;
}
@SuppressWarnings("unchecked")
@VisibleForTesting
static List<Map<String, Object>> parseTxtResults(List<String> txtRecords) {
List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>();
for (String txtRecord : txtRecords) {
if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
List<Map<String, Object>> choices;
try {
Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
if (!(rawChoices instanceof List)) {
throw new IOException("wrong type " + rawChoices);
}
List<Object> listChoices = (List<Object>) rawChoices;
for (Object obj : listChoices) {
if (!(obj instanceof Map)) {
throw new IOException("wrong element type " + rawChoices);
}
}
choices = (List<Map<String, Object>>) (List<?>) listChoices;
} catch (IOException e) {
logger.log(Level.WARNING, "Bad service config: " + txtRecord, e);
continue;
}
serviceConfigs.addAll(choices);
} else {
logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
}
}
return serviceConfigs;
}
@Nullable
private static final Double getPercentageFromChoice(
Map<String, Object> serviceConfigChoice) {
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY)) {
return null;
}
return ServiceConfigUtil.getDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
}
@Nullable
private static final List<String> getClientLanguagesFromChoice(
Map<String, Object> serviceConfigChoice) {
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY)) {
return null;
}
return ServiceConfigUtil.checkStringList(
ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY));
}
@Nullable
private static final List<String> getHostnamesFromChoice(
Map<String, Object> serviceConfigChoice) {
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY)) {
return null;
}
return ServiceConfigUtil.checkStringList(
ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY));
}
/**
* Determines if a given Service Config choice applies, and if so, returns it.
*
* @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
* Service Config in DNS</a>
* @param choice The service config choice.
* @return The service config object or {@code null} if this choice does not apply.
*/
@Nullable
@SuppressWarnings("BetaApi") // Verify isn't all that beta
@VisibleForTesting
static Map<String, Object> maybeChooseServiceConfig(
Map<String, Object> choice, Random random, String hostname) {
for (Entry<String, ?> entry : choice.entrySet()) {
Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
}
List<String> clientLanguages = getClientLanguagesFromChoice(choice);
if (clientLanguages != null && clientLanguages.size() != 0) {
boolean javaPresent = false;
for (String lang : clientLanguages) {
if ("java".equalsIgnoreCase(lang)) {
javaPresent = true;
break;
}
}
if (!javaPresent) {
return null;
}
}
Double percentage = getPercentageFromChoice(choice);
if (percentage != null) {
int pct = percentage.intValue();
Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
if (random.nextInt(100) >= pct) {
return null;
}
}
List<String> clientHostnames = getHostnamesFromChoice(choice);
if (clientHostnames != null && clientHostnames.size() != 0) {
boolean hostnamePresent = false;
for (String clientHostname : clientHostnames) {
if (clientHostname.equals(hostname)) {
hostnamePresent = true;
break;
}
}
if (!hostnamePresent) {
return null;
}
}
return ServiceConfigUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
}
/**
* Returns whether the JNDI DNS resolver is available. This is accomplished by looking up a
* particular class. It is believed to be the default (only?) DNS resolver that will actually be

View File

@ -64,6 +64,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@ -134,6 +135,8 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
private final ConnectivityStateManager channelStateManager = new ConnectivityStateManager();
private final ServiceConfigInterceptor serviceConfigInterceptor = new ServiceConfigInterceptor();
private final BackoffPolicy.Provider backoffPolicyProvider;
/**
@ -537,6 +540,7 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
this.transportFactory =
new CallCredentialsApplyingTransportFactory(clientTransportFactory, this.executor);
Channel channel = new RealChannel();
channel = ClientInterceptors.intercept(channel, serviceConfigInterceptor);
if (builder.binlogProvider != null) {
channel = builder.binlogProvider.wrapChannel(channel);
}
@ -1161,7 +1165,11 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
nameResolverBackoffPolicy = null;
Map<String, Object> serviceConfig =
config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG);
try {
serviceConfigInterceptor.handleUpdate(serviceConfig);
if (retryEnabled) {
retryPolicies = getRetryPolicies(config);
throttle = getThrottle(config);
@ -1223,7 +1231,7 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
}
}
// TODO(zdapeng): test retryEnabled = true/flase really works as expected.
// TODO(zdapeng): test retryEnabled = true/flase really works as expected
private RetryPolicies getRetryPolicies(Attributes config) {
return ServiceConfigUtil.getRetryPolicies(
config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG), maxRetryAttempts);

View File

@ -0,0 +1,325 @@
/*
* Copyright 2018, gRPC Authors All rights reserved.
*
* 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.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.Deadline;
import io.grpc.MethodDescriptor;
import io.grpc.Status.Code;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Modifies RPCs in in conformance with a Service Config.
*/
final class ServiceConfigInterceptor implements ClientInterceptor {
private static final Logger logger = Logger.getLogger(ServiceConfigInterceptor.class.getName());
// Map from method name to MethodInfo
@VisibleForTesting
final AtomicReference<Map<String, MethodInfo>> serviceMethodMap
= new AtomicReference<Map<String, MethodInfo>>();
@VisibleForTesting
final AtomicReference<Map<String, MethodInfo>> serviceMap
= new AtomicReference<Map<String, MethodInfo>>();
ServiceConfigInterceptor() {}
void handleUpdate(Map<String, Object> serviceConfig) {
Map<String, MethodInfo> newServiceMethodConfigs = new HashMap<String, MethodInfo>();
Map<String, MethodInfo> newServiceConfigs = new HashMap<String, MethodInfo>();
// Try and do as much validation here before we swap out the existing configuration. In case
// the input is invalid, we don't want to lose the existing configuration.
List<Map<String, Object>> methodConfigs =
ServiceConfigUtil.getMethodConfigFromServiceConfig(serviceConfig);
if (methodConfigs == null) {
logger.log(Level.FINE, "No method configs found, skipping");
return;
}
for (Map<String, Object> methodConfig : methodConfigs) {
MethodInfo info = new MethodInfo(methodConfig);
List<Map<String, Object>> nameList =
ServiceConfigUtil.getNameListFromMethodConfig(methodConfig);
checkArgument(
nameList != null && !nameList.isEmpty(), "no names in method config %s", methodConfig);
for (Map<String, Object> name : nameList) {
String serviceName = ServiceConfigUtil.getServiceFromName(name);
checkArgument(!Strings.isNullOrEmpty(serviceName), "missing service name");
String methodName = ServiceConfigUtil.getMethodFromName(name);
if (Strings.isNullOrEmpty(methodName)) {
// Service scoped config
checkArgument(
!newServiceConfigs.containsKey(serviceName), "Duplicate service %s", serviceName);
newServiceConfigs.put(serviceName, info);
} else {
// Method scoped config
String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName);
checkArgument(
!newServiceMethodConfigs.containsKey(fullMethodName),
"Duplicate method name %s",
fullMethodName);
newServiceMethodConfigs.put(fullMethodName, info);
}
}
}
// Okay, service config is good, swap it.
serviceMethodMap.set(Collections.unmodifiableMap(newServiceMethodConfigs));
serviceMap.set(Collections.unmodifiableMap(newServiceConfigs));
}
/**
* Equivalent of MethodConfig from a ServiceConfig.
*/
static final class MethodInfo {
final Long timeoutNanos;
final Boolean waitForReady;
final Integer maxInboundMessageSize;
final Integer maxOutboundMessageSize;
final RetryPolicy retryPolicy;
MethodInfo(Map<String, Object> methodConfig) {
timeoutNanos = ServiceConfigUtil.getTimeoutFromMethodConfig(methodConfig);
waitForReady = ServiceConfigUtil.getWaitForReadyFromMethodConfig(methodConfig);
maxInboundMessageSize =
ServiceConfigUtil.getMaxResponseMessageBytesFromMethodConfig(methodConfig);
if (maxInboundMessageSize != null) {
checkArgument(
maxInboundMessageSize >= 0,
"maxInboundMessageSize %s exceeds bounds", maxInboundMessageSize);
}
maxOutboundMessageSize =
ServiceConfigUtil.getMaxRequestMessageBytesFromMethodConfig(methodConfig);
if (maxOutboundMessageSize != null) {
checkArgument(
maxOutboundMessageSize >= 0,
"maxOutboundMessageSize %s exceeds bounds", maxOutboundMessageSize);
}
Map<String, Object> policy = ServiceConfigUtil.getRetryPolicyFromMethodConfig(methodConfig);
retryPolicy = policy == null ? null : new RetryPolicy(policy);
}
@Override
public int hashCode() {
return Objects.hashCode(
timeoutNanos, waitForReady, maxInboundMessageSize, maxOutboundMessageSize);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof MethodInfo)) {
return false;
}
MethodInfo that = (MethodInfo) other;
return Objects.equal(this.timeoutNanos, that.timeoutNanos)
&& Objects.equal(this.waitForReady, that.waitForReady)
&& Objects.equal(this.maxInboundMessageSize, that.maxInboundMessageSize)
&& Objects.equal(this.maxOutboundMessageSize, that.maxOutboundMessageSize);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("timeoutNanos", timeoutNanos)
.add("waitForReady", waitForReady)
.add("maxInboundMessageSize", maxInboundMessageSize)
.add("maxOutboundMessageSize", maxOutboundMessageSize)
.toString();
}
static final class RetryPolicy {
final int maxAttempts;
final long initialBackoffNanos;
final long maxBackoffNanos;
final double backoffMultiplier;
final Set<Code> retryableStatusCodes;
RetryPolicy(Map<String, Object> retryPolicy) {
maxAttempts = checkNotNull(
ServiceConfigUtil.getMaxAttemptsFromRetryPolicy(retryPolicy),
"maxAttempts cannot be empty");
checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts);
initialBackoffNanos = checkNotNull(
ServiceConfigUtil.getInitialBackoffNanosFromRetryPolicy(retryPolicy),
"initialBackoff cannot be empty");
checkArgument(
initialBackoffNanos > 0,
"initialBackoffNanos must be greater than 0: %s",
initialBackoffNanos);
maxBackoffNanos = checkNotNull(
ServiceConfigUtil.getMaxBackoffNanosFromRetryPolicy(retryPolicy),
"maxBackoff cannot be empty");
checkArgument(
maxBackoffNanos > 0, "maxBackoff must be greater than 0: %s", maxBackoffNanos);
backoffMultiplier = checkNotNull(
ServiceConfigUtil.getBackoffMultiplierFromRetryPolicy(retryPolicy),
"backoffMultiplier cannot be empty");
checkArgument(
backoffMultiplier > 0,
"backoffMultiplier must be greater than 0: %s",
backoffMultiplier);
List<String> rawCodes =
ServiceConfigUtil.getRetryableStatusCodesFromRetryPolicy(retryPolicy);
checkNotNull(rawCodes, "rawCodes must be present");
checkArgument(!rawCodes.isEmpty(), "rawCodes can't be empty");
EnumSet<Code> codes = EnumSet.noneOf(Code.class);
// service config doesn't say if duplicates are allowed, so just accept them.
for (String rawCode : rawCodes) {
codes.add(Code.valueOf(rawCode));
}
retryableStatusCodes = Collections.unmodifiableSet(codes);
}
@VisibleForTesting
RetryPolicy(
int maxAttempts,
long initialBackoffNanos,
long maxBackoffNanos,
double backoffMultiplier,
Set<Code> retryableStatusCodes) {
this.maxAttempts = maxAttempts;
this.initialBackoffNanos = initialBackoffNanos;
this.maxBackoffNanos = maxBackoffNanos;
this.backoffMultiplier = backoffMultiplier;
this.retryableStatusCodes = retryableStatusCodes;
}
@Override
public int hashCode() {
return Objects.hashCode(
maxAttempts,
initialBackoffNanos,
maxBackoffNanos,
backoffMultiplier,
retryableStatusCodes);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof RetryPolicy)) {
return false;
}
RetryPolicy that = (RetryPolicy) other;
return Objects.equal(this.maxAttempts, that.maxAttempts)
&& Objects.equal(this.initialBackoffNanos, that.initialBackoffNanos)
&& Objects.equal(this.maxBackoffNanos, that.maxBackoffNanos)
&& Objects.equal(this.backoffMultiplier, that.backoffMultiplier)
&& Objects.equal(this.retryableStatusCodes, that.retryableStatusCodes);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("maxAttempts", maxAttempts)
.add("initialBackoffNanos", initialBackoffNanos)
.add("maxBackoffNanos", maxBackoffNanos)
.add("backoffMultiplier", backoffMultiplier)
.add("retryableStatusCodes", retryableStatusCodes)
.toString();
}
}
}
static final CallOptions.Key<MethodInfo.RetryPolicy> RETRY_POLICY_KEY =
CallOptions.Key.of("internal-retry-policy", null);
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
Map<String, MethodInfo> localServiceMethodMap = serviceMethodMap.get();
MethodInfo info = null;
if (localServiceMethodMap != null) {
info = localServiceMethodMap.get(method.getFullMethodName());
}
if (info == null) {
Map<String, MethodInfo> localServiceMap = serviceMap.get();
if (localServiceMap != null) {
info = localServiceMap.get(
MethodDescriptor.extractFullServiceName(method.getFullMethodName()));
}
}
if (info == null) {
return next.newCall(method, callOptions);
}
if (info.timeoutNanos != null) {
Deadline newDeadline = Deadline.after(info.timeoutNanos, TimeUnit.NANOSECONDS);
Deadline existingDeadline = callOptions.getDeadline();
// If the new deadline is sooner than the existing deadline, swap them.
if (existingDeadline == null || newDeadline.compareTo(existingDeadline) < 0) {
callOptions = callOptions.withDeadline(newDeadline);
}
}
if (info.waitForReady != null) {
callOptions =
info.waitForReady ? callOptions.withWaitForReady() : callOptions.withoutWaitForReady();
}
if (info.maxInboundMessageSize != null) {
Integer existingLimit = callOptions.getMaxInboundMessageSize();
if (existingLimit != null) {
callOptions = callOptions.withMaxInboundMessageSize(
Math.min(existingLimit, info.maxInboundMessageSize));
} else {
callOptions = callOptions.withMaxInboundMessageSize(info.maxInboundMessageSize);
}
}
if (info.maxOutboundMessageSize != null) {
Integer existingLimit = callOptions.getMaxOutboundMessageSize();
if (existingLimit != null) {
callOptions = callOptions.withMaxOutboundMessageSize(
Math.min(existingLimit, info.maxOutboundMessageSize));
} else {
callOptions = callOptions.withMaxOutboundMessageSize(info.maxOutboundMessageSize);
}
}
if (info.retryPolicy != null) {
callOptions = callOptions.withOption(RETRY_POLICY_KEY, info.retryPolicy);
}
return next.newCall(method, callOptions);
}
}

View File

@ -18,26 +18,20 @@ package io.grpc.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.math.LongMath.checkedAdd;
import com.google.common.base.Verify;
import io.grpc.MethodDescriptor;
import io.grpc.Status.Code;
import io.grpc.internal.RetriableStream.RetryPolicies;
import io.grpc.internal.RetriableStream.RetryPolicy;
import io.grpc.internal.RetriableStream.Throttle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.text.ParseException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@ -48,24 +42,29 @@ final class ServiceConfigUtil {
private static final Logger logger = Logger.getLogger(ServiceConfigUtil.class.getName());
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
static final String SERVICE_CONFIG_PREFIX = "_grpc_config=";
private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
Collections.unmodifiableSet(
new HashSet<String>(
Arrays.asList("clientLanguage", "percentage", "clientHostname", "serviceConfig")));
private static final String SERVICE_CONFIG_METHOD_CONFIG_KEY = "methodConfig";
private static final String SERVICE_CONFIG_LOAD_BALANCING_POLICY_key = "loadBalancingPolicy";
private static final String METHOD_CONFIG_NAME_KEY = "name";
private static final String METHOD_CONFIG_TIMEOUT_KEY = "timeout";
private static final String METHOD_CONFIG_WAIT_FOR_READY_KEY = "waitForReady";
private static final String METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY =
"maxRequestMessageBytes";
private static final String METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY =
"maxResponseMessageBytes";
private static final String METHOD_CONFIG_RETRY_POLICY_KEY = "retryPolicy";
private static final String NAME_SERVICE_KEY = "service";
private static final String NAME_METHOD_KEY = "method";
private static final String RETRY_POLICY_MAX_ATTEMPTS_KEY = "maxAttempts";
private static final String RETRY_POLICY_INITIAL_BACKOFF_KEY = "initialBackoff";
private static final String RETRY_POLICY_MAX_BACKOFF_KEY = "maxBackoff";
private static final String RETRY_POLICY_BACKOFF_MULTIPLIER_KEY = "backoffMultiplier";
private static final String RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY = "retryableStatusCodes";
private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private ServiceConfigUtil() {}
@Nullable
static String getLoadBalancingPolicy(Map<String, Object> serviceConfig) {
String key = "loadBalancingPolicy";
if (serviceConfig.containsKey(key)) {
return getString(serviceConfig, key);
}
return null;
}
/**
* Gets retry policies from the service config.
*
@ -94,10 +93,10 @@ final class ServiceConfigUtil {
"maxBackoff": string, // Long decimal with "s" appended
"backoffMultiplier": number
"retryableStatusCodes": []
}
}
]
}
}
}
]
}
*/
if (serviceConfig.containsKey("methodConfig")) {
@ -204,98 +203,152 @@ final class ServiceConfigUtil {
return new Throttle(maxTokens, tokenRatio);
}
/**
* Determines if a given Service Config choice applies, and if so, returns it.
*
* @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
* Service Config in DNS</a>
* @param choice The service config choice.
* @return The service config object or {@code null} if this choice does not apply.
*/
@Nullable
@SuppressWarnings("BetaApi") // Verify isn't all that beta
static Map<String, Object> maybeChooseServiceConfig(
Map<String, Object> choice, Random random, String hostname) {
for (Entry<String, ?> entry : choice.entrySet()) {
Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
static Integer getMaxAttemptsFromRetryPolicy(Map<String, Object> retryPolicy) {
if (!retryPolicy.containsKey(RETRY_POLICY_MAX_ATTEMPTS_KEY)) {
return null;
}
if (choice.containsKey("clientLanguage")) {
List<Object> clientLanguages = getList(choice, "clientLanguage");
if (!clientLanguages.isEmpty()) {
boolean javaPresent = false;
for (int i = 0; i < clientLanguages.size(); i++) {
String lang = getString(clientLanguages, i).toLowerCase(Locale.ROOT);
if ("java".equals(lang)) {
javaPresent = true;
break;
}
}
if (!javaPresent) {
return null;
}
}
}
if (choice.containsKey("percentage")) {
int pct = getDouble(choice, "percentage").intValue();
Verify.verify(pct >= 0 && pct <= 100, "Bad percentage", choice.get("percentage"));
if (random.nextInt(100) >= pct) {
return null;
}
}
if (choice.containsKey("clientHostname")) {
List<Object> clientHostnames = getList(choice, "clientHostname");
if (!clientHostnames.isEmpty()) {
boolean hostnamePresent = false;
for (int i = 0; i < clientHostnames.size(); i++) {
if (getString(clientHostnames, i).equals(hostname)) {
hostnamePresent = true;
break;
}
}
if (!hostnamePresent) {
return null;
}
}
}
return getObject(choice, "serviceConfig");
return getDouble(retryPolicy, RETRY_POLICY_MAX_ATTEMPTS_KEY).intValue();
}
@SuppressWarnings("unchecked")
static List<Map<String, Object>> parseTxtResults(List<String> txtRecords) {
List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>();
for (String txtRecord : txtRecords) {
if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
List<Map<String, Object>> choices;
try {
Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
if (!(rawChoices instanceof List)) {
throw new IOException("wrong type" + rawChoices);
}
List<Object> listChoices = (List<Object>) rawChoices;
for (Object obj : listChoices) {
if (!(obj instanceof Map)) {
throw new IOException("wrong element type" + rawChoices);
}
}
choices = (List<Map<String, Object>>) (List) listChoices;
} catch (IOException e) {
logger.log(Level.WARNING, "Bad service config: " + txtRecord, e);
continue;
}
serviceConfigs.addAll(choices);
} else {
logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
}
@Nullable
static Long getInitialBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy) {
if (!retryPolicy.containsKey(RETRY_POLICY_INITIAL_BACKOFF_KEY)) {
return null;
}
return serviceConfigs;
String rawInitialBackoff = getString(retryPolicy, RETRY_POLICY_INITIAL_BACKOFF_KEY);
try {
return parseDuration(rawInitialBackoff);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Nullable
static Long getMaxBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy) {
if (!retryPolicy.containsKey(RETRY_POLICY_MAX_BACKOFF_KEY)) {
return null;
}
String rawMaxBackoff = getString(retryPolicy, RETRY_POLICY_MAX_BACKOFF_KEY);
try {
return parseDuration(rawMaxBackoff);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Nullable
static Double getBackoffMultiplierFromRetryPolicy(Map<String, Object> retryPolicy) {
if (!retryPolicy.containsKey(RETRY_POLICY_BACKOFF_MULTIPLIER_KEY)) {
return null;
}
return getDouble(retryPolicy, RETRY_POLICY_BACKOFF_MULTIPLIER_KEY);
}
@Nullable
static List<String> getRetryableStatusCodesFromRetryPolicy(Map<String, Object> retryPolicy) {
if (!retryPolicy.containsKey(RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY)) {
return null;
}
return checkStringList(getList(retryPolicy, RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY));
}
@Nullable
static String getServiceFromName(Map<String, Object> name) {
if (!name.containsKey(NAME_SERVICE_KEY)) {
return null;
}
return getString(name, NAME_SERVICE_KEY);
}
@Nullable
static String getMethodFromName(Map<String, Object> name) {
if (!name.containsKey(NAME_METHOD_KEY)) {
return null;
}
return getString(name, NAME_METHOD_KEY);
}
@Nullable
static Map<String, Object> getRetryPolicyFromMethodConfig(Map<String, Object> methodConfig) {
if (!methodConfig.containsKey(METHOD_CONFIG_RETRY_POLICY_KEY)) {
return null;
}
return getObject(methodConfig, METHOD_CONFIG_RETRY_POLICY_KEY);
}
@Nullable
static List<Map<String, Object>> getNameListFromMethodConfig(Map<String, Object> methodConfig) {
if (!methodConfig.containsKey(METHOD_CONFIG_NAME_KEY)) {
return null;
}
return checkObjectList(getList(methodConfig, METHOD_CONFIG_NAME_KEY));
}
/**
* Returns the number of nanoseconds of timeout for the given method config.
*
* @return duration nanoseconds, or {@code null} if it isn't present.
*/
@Nullable
static Long getTimeoutFromMethodConfig(Map<String, Object> methodConfig) {
if (!methodConfig.containsKey(METHOD_CONFIG_TIMEOUT_KEY)) {
return null;
}
String rawTimeout = getString(methodConfig, METHOD_CONFIG_TIMEOUT_KEY);
try {
return parseDuration(rawTimeout);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Nullable
static Boolean getWaitForReadyFromMethodConfig(Map<String, Object> methodConfig) {
if (!methodConfig.containsKey(METHOD_CONFIG_WAIT_FOR_READY_KEY)) {
return null;
}
return getBoolean(methodConfig, METHOD_CONFIG_WAIT_FOR_READY_KEY);
}
@Nullable
static Integer getMaxRequestMessageBytesFromMethodConfig(Map<String, Object> methodConfig) {
if (!methodConfig.containsKey(METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY)) {
return null;
}
return getDouble(methodConfig, METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY).intValue();
}
@Nullable
static Integer getMaxResponseMessageBytesFromMethodConfig(Map<String, Object> methodConfig) {
if (!methodConfig.containsKey(METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY)) {
return null;
}
return getDouble(methodConfig, METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY).intValue();
}
@Nullable
static List<Map<String, Object>> getMethodConfigFromServiceConfig(
Map<String, Object> serviceConfig) {
if (!serviceConfig.containsKey(SERVICE_CONFIG_METHOD_CONFIG_KEY)) {
return null;
}
return checkObjectList(getList(serviceConfig, SERVICE_CONFIG_METHOD_CONFIG_KEY));
}
@Nullable
static String getLoadBalancingPolicyFromServiceConfig(Map<String, Object> serviceConfig) {
if (!serviceConfig.containsKey(SERVICE_CONFIG_LOAD_BALANCING_POLICY_key)) {
return null;
}
return getString(serviceConfig, SERVICE_CONFIG_LOAD_BALANCING_POLICY_key);
}
/**
* Gets a list from an object for the given key.
*/
@SuppressWarnings("unchecked")
private static List<Object> getList(Map<String, Object> obj, String key) {
static List<Object> getList(Map<String, Object> obj, String key) {
assert obj.containsKey(key);
Object value = checkNotNull(obj.get(key), "no such key %s", key);
if (value instanceof List) {
@ -309,7 +362,7 @@ final class ServiceConfigUtil {
* Gets an object from an object for the given key.
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> getObject(Map<String, Object> obj, String key) {
static Map<String, Object> getObject(Map<String, Object> obj, String key) {
assert obj.containsKey(key);
Object value = checkNotNull(obj.get(key), "no such key %s", key);
if (value instanceof Map) {
@ -337,7 +390,7 @@ final class ServiceConfigUtil {
* Gets a double from an object for the given key.
*/
@SuppressWarnings("unchecked")
private static Double getDouble(Map<String, Object> obj, String key) {
static Double getDouble(Map<String, Object> obj, String key) {
assert obj.containsKey(key);
Object value = checkNotNull(obj.get(key), "no such key %s", key);
if (value instanceof Double) {
@ -351,7 +404,7 @@ final class ServiceConfigUtil {
* Gets a string from an object for the given key.
*/
@SuppressWarnings("unchecked")
private static String getString(Map<String, Object> obj, String key) {
static String getString(Map<String, Object> obj, String key) {
assert obj.containsKey(key);
Object value = checkNotNull(obj.get(key), "no such key %s", key);
if (value instanceof String) {
@ -365,7 +418,7 @@ final class ServiceConfigUtil {
* Gets a string from an object for the given index.
*/
@SuppressWarnings("unchecked")
private static String getString(List<Object> list, int i) {
static String getString(List<Object> list, int i) {
assert i >= 0 && i < list.size();
Object value = checkNotNull(list.get(i), "idx %s in %s is null", i, list);
if (value instanceof String) {
@ -374,4 +427,170 @@ final class ServiceConfigUtil {
throw new ClassCastException(
String.format("value %s for idx %d in %s is not String", value, i, list));
}
/**
* Gets a boolean from an object for the given key.
*/
static Boolean getBoolean(Map<String, Object> obj, String key) {
assert obj.containsKey(key);
Object value = checkNotNull(obj.get(key), "no such key %s", key);
if (value instanceof Boolean) {
return (Boolean) value;
}
throw new ClassCastException(
String.format("value %s for key %s in %s is not Boolean", value, key, obj));
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> checkObjectList(List<Object> rawList) {
for (int i = 0; i < rawList.size(); i++) {
if (!(rawList.get(i) instanceof Map)) {
throw new ClassCastException(
String.format("value %s for idx %d in %s is not object", rawList.get(i), i, rawList));
}
}
return (List<Map<String, Object>>) (List<?>) rawList;
}
@SuppressWarnings("unchecked")
static List<String> checkStringList(List<Object> rawList) {
for (int i = 0; i < rawList.size(); i++) {
if (!(rawList.get(i) instanceof String)) {
throw new ClassCastException(
String.format("value %s for idx %d in %s is not string", rawList.get(i), i, rawList));
}
}
return (List<String>) (List<?>) rawList;
}
/**
* Parse from a string to produce a duration. Copy of
* {@link com.google.protobuf.util.Durations#parse}.
*
* @return A Duration parsed from the string.
* @throws ParseException if parsing fails.
*/
private static long parseDuration(String value) throws ParseException {
// Must ended with "s".
if (value.isEmpty() || value.charAt(value.length() - 1) != 's') {
throw new ParseException("Invalid duration string: " + value, 0);
}
boolean negative = false;
if (value.charAt(0) == '-') {
negative = true;
value = value.substring(1);
}
String secondValue = value.substring(0, value.length() - 1);
String nanoValue = "";
int pointPosition = secondValue.indexOf('.');
if (pointPosition != -1) {
nanoValue = secondValue.substring(pointPosition + 1);
secondValue = secondValue.substring(0, pointPosition);
}
long seconds = Long.parseLong(secondValue);
int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
if (seconds < 0) {
throw new ParseException("Invalid duration string: " + value, 0);
}
if (negative) {
seconds = -seconds;
nanos = -nanos;
}
try {
return normalizedDuration(seconds, nanos);
} catch (IllegalArgumentException e) {
throw new ParseException("Duration value is out of range.", 0);
}
}
/**
* Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}.
*/
private static int parseNanos(String value) throws ParseException {
int result = 0;
for (int i = 0; i < 9; ++i) {
result = result * 10;
if (i < value.length()) {
if (value.charAt(i) < '0' || value.charAt(i) > '9') {
throw new ParseException("Invalid nanoseconds.", 0);
}
result += value.charAt(i) - '0';
}
}
return result;
}
private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
/**
* Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}.
*/
@SuppressWarnings("NarrowingCompoundAssignment")
private static long normalizedDuration(long seconds, int nanos) {
if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND);
nanos %= NANOS_PER_SECOND;
}
if (seconds > 0 && nanos < 0) {
nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding)
seconds--; // no overflow since seconds is positive (and we're decrementing)
}
if (seconds < 0 && nanos > 0) {
nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting)
seconds++; // no overflow since seconds is negative (and we're incrementing)
}
if (!durationIsValid(seconds, nanos)) {
throw new IllegalArgumentException(String.format(
"Duration is not valid. See proto definition for valid values. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds", seconds, nanos));
}
return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos);
}
/**
* Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*
* <p>Copy of {@link com.google.protobuf.util.Duration#isValid}.</p>
*/
private static boolean durationIsValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}
/**
* Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case
* {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively.
*
* <p>Copy of {@link com.google.common.math.LongMath#saturatedAdd}.</p>
*
*/
@SuppressWarnings("ShortCircuitBoolean")
private static long saturatedAdd(long a, long b) {
long naiveSum = a + b;
if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) {
// If a and b have different signs or a has the same sign as the result then there was no
// overflow, return.
return naiveSum;
}
// we did over/under flow, if the sign is negative we should return MAX otherwise MIN
return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1);
}
}

View File

@ -19,6 +19,7 @@ package io.grpc.internal;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -45,15 +46,19 @@ import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -68,6 +73,11 @@ public class DnsNameResolverTest {
@Rule public final Timeout globalTimeout = Timeout.seconds(10);
@Rule
public final ExpectedException thrown = ExpectedException.none();
private final Map<String, Object> serviceConfig = new LinkedHashMap<String, Object>();
private static final int DEFAULT_PORT = 887;
private static final Attributes NAME_RESOLVER_PARAMS =
Attributes.newBuilder().set(NameResolver.Factory.PARAMS_DEFAULT_PORT, DEFAULT_PORT).build();
@ -281,6 +291,239 @@ public class DnsNameResolverTest {
assertEquals("blah\\blah", DnsNameResolver.unquote("\"blah\\\\blah\""));
}
@Test
public void maybeChooseServiceConfig_failsOnMisspelling() {
Map<String, Object> bad = new LinkedHashMap<String, Object>();
bad.put("parcentage", 1.0);
thrown.expectMessage("Bad key");
DnsNameResolver.maybeChooseServiceConfig(bad, new Random(), "host");
}
@Test
public void maybeChooseServiceConfig_clientLanguageMatchesJava() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("java");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageDoesntMatchGo() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("go");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageCaseInsensitive() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("JAVA");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageMatchesEmtpy() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageMatchesMulti() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("go");
langs.add("java");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_percentageZeroAlwaysFails() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 0D);
choice.put("serviceConfig", serviceConfig);
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_percentageHundredAlwaysSucceeds() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 100D);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAboveMatches50() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 50D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 49;
}
};
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAtFails50() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 50D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 50;
}
};
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAboveMatches99() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 99D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 98;
}
};
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAtFails99() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 99D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 99;
}
};
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAboveMatches1() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 1D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 0;
}
};
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAtFails1() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 1D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 1;
}
};
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_hostnameMatches() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("localhost");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
@Test
public void maybeChooseServiceConfig_hostnameDoesntMatch() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("localhorse");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageCaseSensitive() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("LOCALHOST");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
@Test
public void maybeChooseServiceConfig_hostnameMatchesEmtpy() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_hostnameMatchesMulti() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("localhorse");
hosts.add("localhost");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
private void testInvalidUri(URI uri) {
try {
provider.newNameResolver(uri, NAME_RESOLVER_PARAMS);

View File

@ -0,0 +1,415 @@
/*
* Copyright 2018, gRPC Authors All rights reserved.
*
* 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.Matchers.eq;
import static org.mockito.Mockito.verify;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.Deadline;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.internal.ServiceConfigInterceptor.MethodInfo;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
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.MockitoAnnotations;
/**
* Unit tests for {@link ServiceConfigInterceptor}.
*/
@RunWith(JUnit4.class)
public class ServiceConfigInterceptorTest {
@Rule public final ExpectedException thrown = ExpectedException.none();
@Mock private Channel channel;
@Captor private ArgumentCaptor<CallOptions> callOptionsCap;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
private final ServiceConfigInterceptor interceptor = new ServiceConfigInterceptor();
private final String fullMethodName =
MethodDescriptor.generateFullMethodName("service", "method");
private final MethodDescriptor<Void, Void> methodDescriptor =
MethodDescriptor.newBuilder(new NoopMarshaller(), new NoopMarshaller())
.setType(MethodType.UNARY)
.setFullMethodName(fullMethodName)
.build();
private static final class JsonObj extends HashMap<String, Object> {
private JsonObj(Object ... kv) {
for (int i = 0; i < kv.length; i += 2) {
put((String) kv[i], kv[i + 1]);
}
}
}
private static final class JsonList extends ArrayList<Object> {
private JsonList(Object ... values) {
addAll(Arrays.asList(values));
}
}
@Test
public void withWaitForReady() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "waitForReady", true);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT.withoutWaitForReady(), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().isWaitForReady()).isTrue();
}
@Test
public void withMaxRequestSize() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", 1d);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(1);
}
@Test
public void withMaxRequestSize_pickSmallerExisting() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", 10d);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(
methodDescriptor, CallOptions.DEFAULT.withMaxOutboundMessageSize(5), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(5);
}
@Test
public void withMaxRequestSize_pickSmallerNew() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", 5d);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(
methodDescriptor, CallOptions.DEFAULT.withMaxOutboundMessageSize(10), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(5);
}
@Test
public void withMaxResponseSize() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", 1d);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getMaxInboundMessageSize()).isEqualTo(1);
}
@Test
public void withMaxResponseSize_pickSmallerExisting() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", 5d);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(
methodDescriptor, CallOptions.DEFAULT.withMaxInboundMessageSize(10), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getMaxInboundMessageSize()).isEqualTo(5);
}
@Test
public void withMaxResponseSize_pickSmallerNew() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", 10d);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(
methodDescriptor, CallOptions.DEFAULT.withMaxInboundMessageSize(5), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getMaxInboundMessageSize()).isEqualTo(5);
}
@Test
public void withoutWaitForReady() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "waitForReady", false);
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT.withWaitForReady(), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().isWaitForReady()).isFalse();
}
@Test
public void fullMethodMatched() {
// Put in service that matches, but has no deadline. It should be lower priority
JsonObj name1 = new JsonObj("service", "service");
JsonObj methodConfig1 = new JsonObj("name", new JsonList(name1));
JsonObj name2 = new JsonObj("service", "service", "method", "method");
JsonObj methodConfig2 = new JsonObj("name", new JsonList(name2), "timeout", "1s");
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig1, methodConfig2));
interceptor.handleUpdate(serviceConfig);
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getDeadline()).isNotNull();
}
@Test
public void nearerDeadlineKept_existing() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "100000s");
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
Deadline existingDeadline = Deadline.after(1000, TimeUnit.NANOSECONDS);
interceptor.interceptCall(
methodDescriptor, CallOptions.DEFAULT.withDeadline(existingDeadline), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getDeadline()).isEqualTo(existingDeadline);
}
@Test
public void nearerDeadlineKept_new() {
// TODO(carl-mastrangelo): the deadlines are very large because they change over time.
// This should be fixed, and is tracked in https://github.com/grpc/grpc-java/issues/2531
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "1s");
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
Deadline existingDeadline = Deadline.after(1234567890, TimeUnit.NANOSECONDS);
interceptor.interceptCall(
methodDescriptor, CallOptions.DEFAULT.withDeadline(existingDeadline), channel);
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
assertThat(callOptionsCap.getValue().getDeadline()).isNotEqualTo(existingDeadline);
}
@Test
public void handleUpdate_failsOnMissingServiceName() {
JsonObj name = new JsonObj("method", "method");
JsonObj methodConfig = new JsonObj("name", new JsonList(name));
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("missing service");
interceptor.handleUpdate(serviceConfig);
}
@Test
public void handleUpdate_failsOnDuplicateMethod() {
JsonObj name1 = new JsonObj("service", "service", "method", "method");
JsonObj name2 = new JsonObj("service", "service", "method", "method");
JsonObj methodConfig = new JsonObj("name", new JsonList(name1, name2));
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Duplicate method");
interceptor.handleUpdate(serviceConfig);
}
@Test
public void handleUpdate_failsOnEmptyName() {
JsonObj methodConfig = new JsonObj();
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("no names in method config");
interceptor.handleUpdate(serviceConfig);
}
@Test
public void handleUpdate_failsOnDuplicateService() {
JsonObj name1 = new JsonObj("service", "service");
JsonObj name2 = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name1, name2));
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Duplicate service");
interceptor.handleUpdate(serviceConfig);
}
@Test
public void handleUpdate_failsOnDuplicateServiceMultipleConfig() {
JsonObj name1 = new JsonObj("service", "service");
JsonObj name2 = new JsonObj("service", "service");
JsonObj methodConfig1 = new JsonObj("name", new JsonList(name1));
JsonObj methodConfig2 = new JsonObj("name", new JsonList(name2));
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig1, methodConfig2));
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Duplicate service");
interceptor.handleUpdate(serviceConfig);
}
@Test
public void handleUpdate_replaceExistingConfig() {
JsonObj name1 = new JsonObj("service", "service");
JsonObj methodConfig1 = new JsonObj("name", new JsonList(name1));
JsonObj serviceConfig1 = new JsonObj("methodConfig", new JsonList(methodConfig1));
JsonObj name2 = new JsonObj("service", "service", "method", "method");
JsonObj methodConfig2 = new JsonObj("name", new JsonList(name2));
JsonObj serviceConfig2 = new JsonObj("methodConfig", new JsonList(methodConfig2));
interceptor.handleUpdate(serviceConfig1);
assertThat(interceptor.serviceMap.get()).isNotEmpty();
assertThat(interceptor.serviceMethodMap.get()).isEmpty();
interceptor.handleUpdate(serviceConfig2);
assertThat(interceptor.serviceMap.get()).isEmpty();
assertThat(interceptor.serviceMethodMap.get()).isNotEmpty();
}
@Test
public void handleUpdate_matchNames() {
JsonObj name1 = new JsonObj("service", "service2");
JsonObj name2 = new JsonObj("service", "service", "method", "method");
JsonObj methodConfig = new JsonObj("name", new JsonList(name1, name2));
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
interceptor.handleUpdate(serviceConfig);
assertThat(interceptor.serviceMethodMap.get())
.containsExactly(
methodDescriptor.getFullMethodName(),
new MethodInfo(methodConfig));
assertThat(interceptor.serviceMap.get())
.containsExactly("service2", new MethodInfo(methodConfig));
}
@Test
public void methodInfo_validateDeadline() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "10000000000000000s");
thrown.expectMessage("Duration value is out of range");
new MethodInfo(methodConfig);
}
@Test
public void methodInfo_saturateDeadline() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "315576000000s");
MethodInfo info = new MethodInfo(methodConfig);
assertThat(info.timeoutNanos).isEqualTo(Long.MAX_VALUE);
}
@Test
public void methodInfo_badMaxRequestSize() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", -1d);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("exceeds bounds");
new MethodInfo(methodConfig);
}
@Test
public void methodInfo_badMaxResponseSize() {
JsonObj name = new JsonObj("service", "service");
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", -1d);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("exceeds bounds");
new MethodInfo(methodConfig);
}
private static final class NoopMarshaller implements MethodDescriptor.Marshaller<Void> {
@Override
public InputStream stream(Void value) {
return null;
}
@Override
public Void parse(InputStream stream) {
return null;
}
}
}

View File

@ -1,276 +0,0 @@
/*
* Copyright 2018, gRPC Authors All rights reserved.
*
* 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 org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link ServiceConfigUtil}.
*/
@RunWith(JUnit4.class)
public class ServiceConfigUtilTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private final Map<String, Object> serviceConfig = new LinkedHashMap<String, Object>();
@Test
public void maybeChooseServiceConfig_failsOnMisspelling() {
Map<String, Object> bad = new LinkedHashMap<String, Object>();
bad.put("parcentage", 1.0);
thrown.expectMessage("Bad key");
ServiceConfigUtil.maybeChooseServiceConfig(bad, new Random(), "host");
}
@Test
public void maybeChooseServiceConfig_clientLanguageMatchesJava() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("java");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageDoesntMatchGo() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("go");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageCaseInsensitive() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("JAVA");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageMatchesEmtpy() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageMatchesMulti() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> langs = new ArrayList<Object>();
langs.add("go");
langs.add("java");
choice.put("clientLanguage", langs);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_percentageZeroAlwaysFails() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 0D);
choice.put("serviceConfig", serviceConfig);
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_percentageHundredAlwaysSucceeds() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 100D);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAboveMatches50() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 50D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 49;
}
};
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAtFails50() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 50D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 50;
}
};
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAboveMatches99() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 99D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 98;
}
};
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAtFails99() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 99D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 99;
}
};
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAboveMatches1() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 1D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 0;
}
};
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_percentageAtFails1() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
choice.put("percentage", 1D);
choice.put("serviceConfig", serviceConfig);
Random r = new Random() {
@Override
public int nextInt(int bound) {
return 1;
}
};
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
}
@Test
public void maybeChooseServiceConfig_hostnameMatches() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("localhost");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
@Test
public void maybeChooseServiceConfig_hostnameDoesntMatch() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("localhorse");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
@Test
public void maybeChooseServiceConfig_clientLanguageCaseSensitive() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("LOCALHOST");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
@Test
public void maybeChooseServiceConfig_hostnameMatchesEmtpy() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
}
@Test
public void maybeChooseServiceConfig_hostnameMatchesMulti() {
Map<String, Object> choice = new LinkedHashMap<String, Object>();
List<Object> hosts = new ArrayList<Object>();
hosts.add("localhorse");
hosts.add("localhost");
choice.put("clientHostname", hosts);
choice.put("serviceConfig", serviceConfig);
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
}
}