-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
xds: HttpFilter support #7932
xds: HttpFilter support #7932
Conversation
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.
To be continued...
@Nullable // Can be null even if hasFaultInjection is true. | ||
final HttpFault httpFault; | ||
// Filter instance names. Null if HttpFilter support is not enabled. | ||
@Nullable final List<String> filterChain; |
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.
Why not have Filter
contains FilterConfig
? Then here you would only need a single list of Filter
s. And in the resolver, you do not need to look up the global registry again.
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.
Did you mean have FilterConfig
contain Filter
?
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.
See my comment for Filter below. I mean something like
interface Filter {
// Returns the canonical name of the filter.
String getName();
FilterConfig getConfig();
interface Parser {
// Returns the list of typeUrls for filter configs that are acceptable by this parser.
String[] getTypeUrls();
FilterConfig parseConfig(Any config);
}
...
}
Generics can probably make this better.
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.
No, the filter config override is finalized in XdsNameResolver's selectConfig()
. Way too late to instantiate such an object.
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.
At the minimum, I think you could make the FilterConfig
have a property for the name of filter it is associated with. Then you can eliminate this List + Map representation.
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.
As a ref, C-core's representation looks simpler to manage: https://github.com/grpc/grpc/blob/0937cb1249129a84159c639c59f49a451157edf7/src/core/ext/xds/xds_api.h#L231-L242
Note, don't be confused by C-core's FilterChain
, that's something used for server side. It's irrelevant.
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.
These two approaches are just preferences. It's not that one of them is significantly simpler than other. For the FilterConfig with name approach, it's inconvenient to query the name and override config.
import javax.annotation.Nullable; | ||
|
||
/** | ||
* Defines the parsing functionality of an HTTP filter. A Filter may optionally implement either |
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.
Such an interface looks weird. It seems to be really just a Parser, as it is documented as "Defines the parsing functionality of an HTTP filter...". Then you probably want some interface to represent Filter
objects (generics may help).
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 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 spec in the design doc
Each filter implementation will provide the following:
Method(s) for validating the config protos when parsing an xDS response.
An indication of whether it is supported on the gRPC client and/or gRPC server.
Methods to generate and configure the appropriate filters or interceptors in gRPC to perform the necessary functionality on the data plane.
/** Uses the FilterConfigs produced above to produce an HTTP filter interceptor for clients. */ | ||
interface ClientInterceptorBuilder { | ||
@Nullable | ||
ClientInterceptor buildClientInterceptor( |
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 interface is redundant. It looks you don't need to passing both the top-level filter config and the override config(s). It should be as simple as buildClientInterceptor(FilterConfig config, ...)
. The caller should just need to pass in the single filter config: the most fine-grained one (WeightedCluster > Route > VirtualHost > HttpConnectionManager). The existing interface is half-baked: you let the implementation decide whether to use config
or overrideConfig
, but let the caller decide which filter config to be passed in as the overrideConfig
.
Given that, it can be further changed to have this method on FilterConfig
interface:
interfacce FilterConfig {
String typeUrl();
ClientInterceptor getClientInterceptor(PickSubchannelArgs args, ScheduledExecutorService scheduler);
}
as the same type of FilterConfig always produces the same kind of ClientInterceptor.
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 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.
Fault filter uses just override config if it is provided, and that's just a special case, and for general HttpFilters that's not the case.
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 commenting from interface/class design's perspective. I don't think direct copying method signatures from other languages is appropriate, and IMO such an interface looks very awkward.
If you want a general purpose interface for merging the top-level config and its overrides (could be up to 3 levels of overrides), the method should take in a list of overriding configs, not just the most specific one. If it is well-defined that merging configs is just letting the most specific one take over, the method only needs to take in a single config argument where as the caller always passes in the most specific config. And the method of returning a ClientInterceptor can just be associated with the FilterConfig instance, as the implementation of converting a certain type of FilterConfig to a ClientInterceptor is specific to each type of config itself.
Anyway, I can just let you pass if you just want to do exactly the same thing as C-core and Go.
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 interface is for passing a top-level config and a merged override config (the 3 level of overrides merged as the finalized override), and in general, the top-level config and the merged override config are of different types, and both of them will be needed to generate ClientInterceptor. The interface is for a general HttpFilter. For the Fault Filter, the top-level and override configs are of the same type, and if override config is provided, the top-level will not be used, but that's a special implementation.
Any anyConfig = httpFilter.getTypedConfig(); | ||
Message rawConfig = anyConfig; | ||
String typeUrl = anyConfig.getTypeUrl(); | ||
if (anyConfig.getTypeUrl().equals(TYPE_URL_TYPED_STRUCT)) { |
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 this also be "type.googleapis.com/envoy.config.route.v3.FilterConfig"?
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.
No. The top-level config should not be envoy.config.route.v3.FilterConfig
, which is a wrapper of config plus a is_optional
flag that the top-level already has.
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.
Alright. My main point was to put the logic of flattening the config message in a helper method and reuse the code. Even if the top-level config will not be envoy.config.route.v3.FilterConfig
, your helper method can still be reused no matter it is top-level or override, right?
|
||
@Nullable | ||
@Override | ||
public ClientInterceptor buildClientInterceptor( |
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 very confused by how this method would be used. The LameFilter
does not implement config parsing. That means you do not get a FilterConfig
. In the xDS message parsing for top-level filters, if the config message doesn't exist, we just skip that filter entirely. That means, to use the LameFilter
, we must have a config. Parsing the LameFilter
's config throws UnsupportedOperationException
. I am deadlocked...
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.
LameFilter is not parsed from xDS message. It is added by grpc to the end of filter chain if RouterFilter is absent as in the spec :
if the router filter is not present, then gRPC will insert a special filter at the end of the filter chain that fails all RPCs.
parseFilterConfig()
will never be called.
There are many ways to do it equivalently. I chose a way that is closest to c-core.
|
||
@Override | ||
public StructOrError<? extends FilterConfig> parseFilterConfig(Message rawProtoMessage) { | ||
return StructOrError.fromStruct(ROUTER_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.
Why is this 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.
We don't parse any specific field in the raw config, so we just return a dummy "parsed" config.
filterConfig = | ||
anyConfig.unpack(io.envoyproxy.envoy.config.route.v3.FilterConfig.class); | ||
} catch (InvalidProtocolBufferException e) { | ||
return StructOrError.fromError("Invalid proto: " + 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.
Include the filter name in the error message now that you are completely relying on this method to give you an error message.
Or you could let the caller decorate the error message with filter name (then you do not need to pass in the filter name).
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.
Included the filter name in the error message now
private Result lameResult(RoutingConfig savedRoutingConfig, PickSubchannelArgs args) { | ||
Map<String, ?> rawServiceConfig = Collections.emptyMap(); | ||
if (enableTimeout) { | ||
Long timeoutNano = routingConfig.fallbackTimeoutNano; |
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.
Again, are we able to reorganize the code so that we do not need to repeat this logic (as well as the block for handling interceptor list)?
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.
* by any of the {@link Filter#typeUrls(), type URLs}. | ||
*/ | ||
final class Registry { | ||
static Registry GLOBAL_REGISTRY = |
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.
nit: GLOBAL_REGISTRY
. I am not a fan of such a name, it looks strange to me... To me, FilterRegistry.getDefaultRegistry()
looks much more comfortable than Filter.Registry.GLOBAL_REGISTRY
.
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
.build(); | ||
} | ||
|
||
private ConfigOrError generateServiceConfig( |
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 method is confusing in terms of its signature and its name. You only need the fallbackTimeoutNano
from the RoutingConfig
. This is like I want a banana, but you give me a monkey holding a banana with the entire forest behind...
Also, why do you need a timeout method config for lame?
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 method is confusing in terms of its signature and its name. You only need the fallbackTimeoutNano from the RoutingConfig. This is like I want a banana, but you give me a monkey holding a banana with the entire forest behind...
Well, the method name generateServiceConfigWithMethodTimeoutConfig()
is already taken. The two methods can be merged, but generateServiceConfigWithMethodTimeoutConfig()
is used for testing. That's why I was originally keeping a few lines of duplicate code.
I can change the RoutingConfig
arg to fallbackTimeoutNano
in signature.
Also, why do you need a timeout method config for lame?
If there is a delay injection filter ahead of lame, the fallback timeout can still be needed.
private final boolean applyFaultInjection; | ||
@Nullable | ||
private final HttpFault faultConfig; | ||
// Null if HttpFilter support is enabled but RouterFilter is absent in the filter chain. |
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 this implementation is unnecessarily complicated with many small logic branchings. A very straightforward approach is just having a list of FilterConfig
(not null, but can be empty). At the beginning processing the chain, you just peek the last entry, if it's not a RouterFilter, append a LameFilter. That's it. Then you have simple and invariant representations for all cases.
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 very straightforward approach is just having a list of FilterConfig (not null, but can be empty).
Nullness of the list is needed for whether HttpFilter support enabled or not. That is simple and straightforward. Other tricks can be very misleading. Once HttpFilter support is no longer experimental, the list will be non-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.
I mean your code logic contains too many case-specific handlings that can be generalized. That's why you end up having so many if
s (and Null if ...
). That introduces many edge cases and is more likely to have bugs, either hidden somewhere already or a small patch could introduce one. And it also makes testing hard/complicated (you would have more code branches need to be covered).
That's why I was saying you could just simply append a lame filter if the last filter is not a router filter, no matter there is one in the middle of the chain or not. And you just have a general code block for processing filters, rather than getting into different codepaths based on if router filter exists or not. The general idea is have a clear/strong specification for each method (AKA do one thing well) with weak precondition/call assumption.
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 different codepath is to skip the routing logic if there is no rout filter. This branching is unavoidable even appending a lame filter ahead of time. I do agree that appending a lame filter ahead of time can handle all kinds of filters uniformly and reduce some duplicate code. However, to handle the lame filter in the same way as other filters, it need a synthetic filter name and type_url, which I don't like.
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.
Changed as suggested. PTAL again.
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.
Mostly that's the review. That's all my comments. If I cannot convince you, I will just accept it.
Btw, don't you think the tests are weak/test too litttle?
private final boolean applyFaultInjection; | ||
@Nullable | ||
private final HttpFault faultConfig; | ||
// Null if HttpFilter support is enabled but RouterFilter is absent in the filter chain. |
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 mean your code logic contains too many case-specific handlings that can be generalized. That's why you end up having so many if
s (and Null if ...
). That introduces many edge cases and is more likely to have bugs, either hidden somewhere already or a small patch could introduce one. And it also makes testing hard/complicated (you would have more code branches need to be covered).
That's why I was saying you could just simply append a lame filter if the last filter is not a router filter, no matter there is one in the middle of the chain or not. And you just have a general code block for processing filters, rather than getting into different codepaths based on if router filter exists or not. The general idea is have a clear/strong specification for each method (AKA do one thing well) with weak precondition/call assumption.
What (strong) tests do you think should be added? |
1c2e369
to
69356f1
Compare
Don't forget to make types of supported HttpFilter messages printable. |
} | ||
if (listener != null) { | ||
toStringHelper.add("listener", listener); | ||
} | ||
return toStringHelper.toString(); | ||
} | ||
|
||
static final class NamedFilter { |
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.
Why this is defined here? Instead of in Filter? Also, this makes the definitions of Filter and FilterConfig very confusing. Why not just add the name property to FilterConfig
?
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.
Why this is defined here? Instead of in Filter?
Because it's only used for constructing or accessing data of LdsUpdate
.
Why not just add the name property to FilterConfig?
The parsed config returned by the API StructOrError<FilterConfig> parseFilterConfig(Message rawProtoMessage)
is not tied to a filter name. Actually it's possible that two filter instances with different names can have the same FilterConfig. It's just the pure config data parsed from envoy proto.
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.
Actually it's possible that two filter instances with different names can have the same FilterConfig.
That's just two separate FilterConfig instances, I don't see the difference between putting the name property on FilterConfig vs wrapping with another layer. Anyway, I accept this approach, but NamedFilter
shouldn't be defined here.
Because it's only used for constructing or accessing data of LdsUpdate.
That's not a valid reason, NamedFilter
is useless without the feature of HttpFilter, it's HttpFilter-specific. Putting it here is anti-modularity (Filter and other Filter-related classes are defined elsewhere while this one is defined on XdsClient surface). I don't think this is just a style issue...
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.
this.virtualHostOverrideConfig = Collections.unmodifiableMap(virtualHostOverrideConfig); | ||
} | ||
|
||
private boolean hasLameFilter() { |
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 see any usefulness of this method, especially compared with what you previously had (aka, a local variable when populating the filter chain). What I meant was, you do not need to populate the filter chain to see if a router filter exists (aka, code in updateRoutes()
), just peek the last one. If it's not a router filter, insert a LAME.
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.
Peeking the last one is not sufficient as the router may be present but is not the last one:
(https://github.com/grpc/proposal/blob/master/A39-xds-http-filters.md#the-router-filter)
In addition, if the router filter is present, any filters in the list after the router filter will not actually be used, although their configurations will still be validated when the config is received from the xDS server.
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.
any filters in the list after the router filter will not actually be used
If it's a router filter presents in the middle, anything after it doesn't matter.
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.
Not sure I understood what you mean correctly, removed the method.
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 do not agree with just creating another StructOrError
(Filter.StructOrError
), just because the existing one is not accessible outside ClientXdsClient
. Before creating this PR, you should have solved that problem already. If not, that should be a blocker for making this change.
I do not agree with the development process that first check in code with poor integrity and then fix it later (This is like "currently I do not depend on gRPC so let's implement my own gRPC, then next PR will delete my own implementation and pull in a gRPC dependency"). That's how things become messy. Anyway, you need to coordinate with Sergii for resolving this.
/cc @sergiitk
See for
Design doc:
https://github.com/grpc/proposal/blob/master/A39-xds-http-filters.md
C core impl:
grpc/grpc#25558
Go impl:
grpc/grpc-go#4206
The java approach is similar to Go because they both use FilterConfigs to generate interceptor rather than method config in ConfigSelector.
The new API
io.xds.Filter
is equivalent tohttpfilter.go
.