Skip to content

Conversation

@dapengzhang0
Copy link
Contributor

Note that the default config follows the spec that

4. If the service config is invalid, the clients should reject the received
config in its entirety and do different things depending on its state and
configuration.
	1. If it previously had selected a valid service config or no service
	config, it should continue using that previous configuration from that
	config. An empty service config is valid.
	2. If the client has not previously selected a valid service config or no
	service config, for example a new client, then:
		1. It uses the default service config, if configured and valid. Note
		that an empty default service config is a valid service config.
		2. If it does not have a default service config, it should treat the
		resolution attempt as having failed (e.g., for a polling-based
		resolver, it should retry the query after appropriate backoff). In
		other words,the channel will remain in state TRANSIENT_FAILURE until a
		valid service config is received.

}

@Override
public T discardServiceConfigFromNameResolver() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disableServiceConfigLookup() is shorter, and it's in line with the plan to expose this option to NameResolver so that it can skip the lookup. We also planned on logging a warning if the channel says don't lookup but the resolver did anyway. This means the channel and the resolver should work together to not look up, not just that the resolver keeps doing it while the channel will discard it.

If you go with disable, there should be enable for completeness. Or you can have lookupServiceConfig(boolean) like keepAliveWithoutCalls(boolean).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* If null is passed, then there will be no default service config.
*
* @throws UnsupportedOperationException If not implemented.
* @throws BuilderException When the given serviceConfig is invalid or the current version of grpc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want an exception that's specific to service config parsing, or just StatusException which is in line with parse() returning Status in the tuple.

@carl-mastrangelo @ejona86 WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing IllegalArgumentException instead of a checked Exception now.


Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
if (!(rawChoices instanceof List)) {
throw new IOException("wrong type " + rawChoices);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention from ServiceConfigUtil is to throw ClassCastException. We'd better follow suit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to minimize the change. So I would leave it unchanged in this PR.

}

/**
* Provides a service config to the channel.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also document the cases this default config will be used. My understanding of the spec is the default config is used only if the Channel has not seen any config from NameResolver, including the case config lookup is disabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Map<String, ?> effectiveServiceConfig =
config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG);
if (effectiveServiceConfig != null
&& effectiveServiceConfig.containsKey(DnsNameResolver.SERVICE_CONFIG_ERROR)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carl-mastrangelo is implementing the error handling in the proper way. Whatever hack you put here for error-handling will be thrown away. The code as of now will just ignore the update if the config parsing failed, or the resolver didn't return a config. You may want to just keep it that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So can I just assume the attributes passed here is with either no config or a valid config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted the hack.

Map<String, Object> parsedMap = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!(entry.getKey() instanceof String)) {
throw new BuilderException(null); // TODO(zdapeng): cause
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassCastException?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassCastException will be wrapped. Let's wait for the decision on what Exception class should be thrown.

}

private static List<?> parseList(List<?> list) throws BuilderException {
List<Object> parsedList = new ArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new ArrayList<>(list.size());

}

@Override
public T discardServiceConfigFromNameResolver() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should consider making this take boolean parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with lookupServiceConfig(boolean) as suggested by @zhangkun83 , let me know if any objection.

} catch (RuntimeException e) {
} catch (IOException | RuntimeException e) {
logger.log(Level.WARNING, "Can't parse service Configs", e);
serviceConfig = Collections.singletonMap(SERVICE_CONFIG_ERROR, (Object) e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am in the process of modifying this, you should consider keeping the old behavior, if possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to distinguish empty config (valid) and error config. In error config case, will try to use the default config; and if the default config is missing, then go to transient failure. Are you also modifying the exception handling at the original line#294?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted.

@dapengzhang0
Copy link
Contributor Author

I reverted service config resolution error handling part, and for now assume receiving null config is always a non-error case.

Copy link
Member

@ejona86 ejona86 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API looks fine to me. I didn't really look at ManagedChannelImpl to verify its correctness.

}

@Override
public T defaultServiceConfig(@Nullable Map<String, ?> serviceConfig) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be Map<String, Object>. Given the structure of serviceConfig, it's unlikely that any other value type would be useful.

Copy link
Contributor Author

@dapengzhang0 dapengzhang0 Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem of Map<String, Object> might be: If the user has a Map<String, Map> object at hand, they have to cast it to Map<String, Object> first and suppress unchecked and then pass it to the API. Actually I'm not too strong about it. @carl-mastrangelo you want to debate about it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree, but given the service config options, what is the likelihood that they set values that are the same type? It seems they would create the object just for passing to this call, so it would also be trivial for them to use Object instead of Map<String, Object> as the value.

I could accept having it, it just seems like it would be more confusing to users without much benefit.

Copy link
Contributor

@carl-mastrangelo carl-mastrangelo Mar 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that Map<String, ?> involved less internal casting, and was easier to pass in. It also means that it isn't possible to write to the map since no type matches the wildcard, implying it is immutable. (this isn't hard and fast, but it makes it hard to call put).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could Map<String, ?> involve less internal casting? Can you give an example?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a similar case with List<Object>: https://github.com/grpc/grpc-java/blob/v1.19.x/core/src/main/java/io/grpc/internal/ServiceConfigUtil.java#L501

It's not always going to happen that way, but still it was nice to avoid that.

One other thing, when passing in this type, it's nice to pass values into Map<String, ?>, especially in unit tests where the type comes from an inlined value:

defaultServiceConfig(ImmutableMap.of("methodConfig", myTestMethodConfig));

It involves a cast to the Map<String, Object>. I think we should try it as Map<String, ?> and consider if it should be changed in API review.

*
* If null is passed, then there will be no default service config.
*
* @throws UnsupportedOperationException If not implemented.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't tend to include this exception, since our implementations implement it and it can be confusing to users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

/**
* Lets the channel look up or not look up service config from the name resolver.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify what the default value is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/**
* Provides a service config to the channel.
*
* @param serviceConfig A nested map representing a Json object in the most natural way:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't include the table in the @param. Move that up to the general description. @param should be short, like 2 sentences max, and ideally just a sentence fragment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

@Nullable
private static Map<String, ?> parseMap(@Nullable Map<?, ?> map) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look to be parsing anything. It seems to just be making an immutable copy. Name it as such?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to checkMapEntryTypes()

}

@Nullable
private static Map<String, ?> parseMap(@Nullable Map<?, ?> map) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: if you instead had parseObject, this would be much shorter as it would have less duplication between map and list. But it seems this will be deleted almost immediately, so it is fine as-is.

channelLogger.log(ChannelLogLevel.INFO, "Service config changed to null");
}

if (!Objects.equal(defaultServiceConfig, serviceConfig)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't bother with this condition. The equals check could be pretty expensive itself and there's virtually no chance the configs match (unless they are empty, I guess). If you want, you could check to see if they are both null (which is basically equivalent to reference equality, but more clear).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

if (!Objects.equal(defaultServiceConfig, serviceConfig)) {
effectiveConfig = Attributes.newBuilder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.toBuilder() is more efficient than newBuilder().setAll(config) and more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
lastServiceConfig = defaultServiceConfig;
} else {
if (!serviceConfig.equals(lastServiceConfig)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use reference equality.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks this is preserving existing behavior. It can stay as-is. (Although in the future I expect it will be changed.)

@dapengzhang0 dapengzhang0 changed the title core: channelBuilder.defaulServiceConfig() and discardServiceConfigFromNameResolver() core: channelBuilder.defaulServiceConfig() and lookUpServiceConfig() Mar 12, 2019
/**
* Provides a service config to the channel. The channel will use the default service config when
* the name resolver provides no service config or if the channel disables lookup service config
* from name resolver. The argument {@code serviceConfig} is a nested map representing a Json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a link to lookUpServiceConfig().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

/**
* Lets the channel look up or not look up service config from the name resolver. The look-up is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service config is not looked up from name resolver per se. It controls both whether the name resolver will look up the config, and whether the channel will use the config from name resolver.
Maybe rephrase it to: "Enable or disable service config look-up from the naming system. Enabled by default."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@Nullable
private Map<String, ?> lastServiceConfig; // used for channel tracing when value changed
@Nullable
private Map<String, ?> defaultServiceConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Must be mutated and read from constructor or syncContext
// See service config error handling spec for reference.
// TODO: check this value when error in service config resolution
private boolean selectedServiceConfigOrNoServiceConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more comprehensible name for this variable may be "waitingForServiceConfig" which defaults to true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (serviceConfig != null && !serviceConfig.equals(lastServiceConfig)) {
channelLogger.log(ChannelLogLevel.INFO, "Service config changed");
Attributes effectiveConfig = config;
if (!lookUpServiceConfig || serviceConfig == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe simplify the conditions and make it flow better:

Map<String, ?> effectiveConfig = null;

// Try to use config if returned from name resolver and allowed
if (serviceConfig != null) {
  if (lookUpServiceConfig) {
    effectiveConfig = serviceConfig;
  } else {
    channelLogger.log(ChannelLogLevel.INFO, "Service config from name resolver discarded by channel settings");
  }
}

// Otherwise, try to use the default config if available
if (effectiveConfig == null && defaultServiceConfig != null) {
  channelLogger.log(ChannelLogLevel.INFO, "Using default service config");
  effectiveConfig = defaultServiceConfig;
}

if (effectiveConfig != lastServiceConfig) {
  channelLogger.log(ChannelLogLevel.INFO,
      "Service config changed" + (effectiveConfig == null ? " to null" : ""));
  lastServiceConfig = effectiveConfig;
}

config = config.toBuilder()
    .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, effectiveConfig)
    .build();

waitingForServiceConfig = false;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

if (!(defaultServiceConfig == null && serviceConfig == null)) {
effectiveConfig = config.toBuilder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even service config is already null in the attributes, it would make the implementation simpler to always update the attributes with the selected config (possibly null), because performance/allocation is not a concern for this code path. Please see my suggestion above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will break an existing test (Attributes.EMPTY is not the same as attributes with a key and a null value), I might need to change the test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's just one test, please go ahead and update that test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 6 tests, each will need introduce an ArgumentCapture, so added a check if (effectiveServiceConfig != serviceConfig) instead.

serviceConfigInterceptor.handleUpdate(lastServiceConfig);
if (retryEnabled) {
throttle = ServiceConfigUtil.getThrottlePolicy(lastServiceConfig);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above 4 lines are repeated below. They need to be put in a separate method.

nameResolverBackoffPolicy = null;

if (serviceConfig != null) {
if (lookUpServiceConfig) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this, I think the whole service config handling logic above should also be under an if (lookUpServiceConfig), and this if-block should be grouped with that.

}

@Test
public void disableNameResolverConfig_noDefaultConfig() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disableServiceConfigLookUp_

}

@Test
public void disableNameResolverConfig_withDefaultConfig() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}

@Test
public void noDiscardNameResolverConfig_noDefaultConfig() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enableServiceConfigLookUp_

}

@Test
public void noDiscardNameResolverConfig_withDefaultConfig() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should also be enableServiceConfigLookUp_resolverReturnsNoConfig_withDefaultConfig and enableServiceConfigLookUp_resolverReturnsNoConfig_noDefaultConfig


waitingForServiceConfig = false;
} else {
// Try to use config if returned from name resolver and allowed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"and allowed" should be removed now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@zhangkun83 zhangkun83 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

* <td>Json entry</td><td>Java Type</td>
* </tr>
* <tr>
* <td>object</td><td>{@link Map}</td>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map<String, ?>

Copy link
Contributor Author

@dapengzhang0 dapengzhang0 Mar 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic types, including wildcards sometimes are very inconvenient to use. I would let the users be flexible to choose whatever generic type parameter that's most convenient for them, (either the users want to minimize the times to cast and suppress unchecked, or want to minimize errorprone at compile time, or both), as long as it follows the table as a whole.

* <td>object</td><td>{@link Map}</td>
* </tr>
* <tr>
* <td>array</td><td>{@link List}</td>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List<?>

// Not using ImmutableMap.Builder because of extra guava dependency for Android.
Map<String, Object> parsedMap = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!(entry.getKey() instanceof String)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a checkArgument

} else if (value instanceof Boolean) {
parsedMap.put(key, value);
} else {
throw new IllegalArgumentException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include the type name and key

} else if (value instanceof Map) {
parsedMap.put(key, checkMapEntryTypes((Map<?, ?>) value));
} else if (value instanceof List) {
parsedMap.put(key, checkListEnttryTypes((List<?>) value));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkListEnttryTypes is misspelled

return null;
}
// Not using ImmutableMap.Builder because of extra guava dependency for Android.
Map<String, Object> parsedMap = new LinkedHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new LinkedHashMap<>(map.size());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default load factor is 0.75, it will be rehashed.

private Boolean haveBackends; // a flag for doing channel tracing when flipped
// Must be mutated and read from syncContext
// Must be mutated and read from constructor or syncContext
// TODO: check this value when error in service config resolution
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add my name for this todo?

@dapengzhang0
Copy link
Contributor Author

@carl-mastrangelo PTALA, hope this PR will not introduce too many merge conflicts for yours.

for (Map.Entry<?, ?> entry : map.entrySet()) {
checkArgument(
entry.getKey() instanceof String,
"The key of the entry '" + entry + "' is not of String type");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkArgument(entry.getKey() instanceof String, "The key of the entry '%s' is not of String type", entry);

Copy link
Contributor

@carl-mastrangelo carl-mastrangelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dapengzhang0
Copy link
Contributor Author

Thank you @zhangkun83 @carl-mastrangelo @ejona86 for the review!

@dapengzhang0 dapengzhang0 merged commit 1735adc into grpc:master Mar 14, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Jun 13, 2019
@dapengzhang0 dapengzhang0 deleted the channelbuilder-service-config branch January 16, 2022 18:24
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants