-
Notifications
You must be signed in to change notification settings - Fork 4k
core: channelBuilder.defaulServiceConfig() and lookUpServiceConfig() #5463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core: channelBuilder.defaulServiceConfig() and lookUpServiceConfig() #5463
Conversation
…elbuilder-service-config
| } | ||
|
|
||
| @Override | ||
| public T discardServiceConfigFromNameResolver() { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClassCastException?
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reverted.
|
I reverted service config resolution error handling part, and for now assume receiving null config is always a non-error case. |
ejona86
left a comment
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just use reference equality.
There was a problem hiding this comment.
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.)
| /** | ||
| * 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 |
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
core/src/main/java/io/grpc/internal/AbstractManagedChannelImplBuilder.java
Show resolved
Hide resolved
| @Nullable | ||
| private Map<String, ?> lastServiceConfig; // used for channel tracing when value changed | ||
| @Nullable | ||
| private Map<String, ?> defaultServiceConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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;There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); | ||
| } |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
zhangkun83
left a comment
There was a problem hiding this 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Map<String, ?>
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new LinkedHashMap<>(map.size());
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
|
@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"); |
There was a problem hiding this comment.
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);
carl-mastrangelo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
Thank you @zhangkun83 @carl-mastrangelo @ejona86 for the review! |
Note that the default config follows the spec that