Skip to content

xds: Add composite filter#12646

Open
AgraVator wants to merge 13 commits into
grpc:masterfrom
AgraVator:xds-composite-filter
Open

xds: Add composite filter#12646
AgraVator wants to merge 13 commits into
grpc:masterfrom
AgraVator:xds-composite-filter

Conversation

@AgraVator
Copy link
Copy Markdown
Contributor

@AgraVator AgraVator commented Feb 5, 2026

Implementation of A103

@AgraVator AgraVator force-pushed the xds-composite-filter branch from 6bf8474 to 603d4fe Compare February 5, 2026 16:44
Comment on lines 517 to 519
if (proto.getHttpFiltersList().isEmpty()) {
throw new ResourceInvalidException("Missing HttpFilter in HttpConnectionManager.");
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping this the same, assuming we will still be getting Filters here and not all through ExtensionWithMatcherPerRoute

@AgraVator AgraVator changed the title composite filter init composite filter Feb 9, 2026
@AgraVator AgraVator marked this pull request as ready for review February 9, 2026 18:07
@sauravzg
Copy link
Copy Markdown
Collaborator

@AgraVator Can you split this into two PRs ? 1 for importing protos and another for business logic changes for composite filters? go/small-cls

Comment thread xds/src/main/java/io/grpc/xds/XdsNameResolver.java
Comment thread xds/src/main/java/io/grpc/xds/XdsListenerResource.java
Comment thread xds/src/main/java/io/grpc/xds/XdsAttributes.java
abstract ImmutableMap<String, FilterConfig> filterConfigOverrides();

@Nullable
abstract ImmutableMap<String, com.google.protobuf.Struct> filterMetadata();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

go/java-practices/null#collection

Is there a semantic difference between a null filterMetadata and empty filterMetadata ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the current implementation, a null filterMetadata represents the absence of metadata (as per the proto not having it set), while an empty map would represent present but empty metadata. I followed the existing pattern in the codebase for optional maps. However, I agree that using non-null empty collections is generally preferred in Java to avoid null checks. We can consider a refactor to use empty maps consistently in a separate cleanup PR to keep this PR focused on the core feature ?

public String[] typeUrls() {
return new String[] {
TYPE_URL_EXTENSION_WITH_MATCHER,
TYPE_URL_COMPOSITE,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need this ?
I'd assume that TypeUrls represent the entry points which in case should be ...matcher or ..matcherperroute . Then we construct the composite filter as needed from the config proto.
Are there cases where we might need this at the top level?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The gRFC A103 specifies that the Composite Filter can be configured as an HTTP filter directly. In Envoy, it can appear at the top level in the http_filters list of the HttpConnectionManager. While it is more commonly used inside a matcher to combine filters, we need to support it at the top level to be compliant with the spec and support cases where a user wants to apply a composite filter to all routes by default.

Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
}

if (matcherProto == null) {
return ConfigOrError.fromConfig(new CompositeFilterConfig(null));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What does this case cover? any is an arbitrary proto, so we return a default configerror? What does a CompositeFilterConfig with null semantically mean?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A CompositeFilterConfig with a null matcher represents a configuration where the message is just Composite without any specific matcher configured. Semantically, it means a default or passthrough configuration when no matching logic is specified. If an arbitrary proto is received that we cannot unpack or handle, we return a config error to fail fast, as per standard xDS parsing rules.

throw new IllegalArgumentException("Failed to unpack TypedStruct", e);
}

Filter.Provider provider = FilterRegistry.getDefaultRegistry().get(typeUrl);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Usually, depending on a global registry may make testing difficult and brittle, which is somewhat evident by the need to add a visiblefortesting deletion method needed to be added in the FilterRegistry.

My recommendation here would be to follow "program to interface instead of implementation" , by creating an interface and inject it as a dependency into the CompositeFilter .
Then production can use an implementation which can adapt the DefaultFilterRegistry, while the unit test can use a mock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a package-private static registryLookup function in CompositeFilter.Provider that defaults to using FilterRegistry.getDefaultRegistry(). This allows injecting a custom lookup function in CompositeFilterTest to return the mock provider without having to modify the global registry state during tests.


@Override
public String typeUrl() {
return TYPE_URL_COMPOSITE;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this the reason for having three entries in the filter provider ? So, that we can correlate the config type to the provider?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, returning these three type URLs in typeUrls() allows the FilterRegistry to route all three configuration types to this CompositeFilter.Provider. This is necessary because CompositeFilter handles all three of these related proto types (ExtensionWithMatcher, Composite, and ExtensionWithMatcherPerRoute) to implement the feature, and returning them in typeUrls() ensures that the xDS config parser dispatches them to this provider.

Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated

return new ServerInterceptor() {
@Override
public <ReqT, RespT> io.grpc.ServerCall.Listener<ReqT> interceptCall(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Any particular reason for the assymetry? ClientInterceptor is abstracted into a helper class while ServerInterceptor is inline?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The asymmetry is due to the different timing of when headers are available. On the client side, headers are only available when ClientCall.start() is called, so we need a helper class (CompositeClientCall) to delay the matching and interceptor creation until then. On the server side, headers are available immediately in ServerInterceptor.interceptCall(), so we can perform matching and create interceptors inline without needing a separate helper class.

Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java
UnifiedMatcher.MatchingData data = new MatchingDataImpl(headers, null,
call.getAttributes());

List<FilterDelegate> delegates = matcher.match(data);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we use non null empty containers only in the matchers API contract?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will be replaced by the work being done for A106

}
} catch (Throwable t) {
for (Filter f : filters) {
f.close();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am not sure of the solution here due to my unfamiliarity with the language , but we have repeated usages for "close all filters on error" . Is there an alternate design pattern that can do some form of RAII like try-with-resources (or something that allows autocolose on error /scope exit) ?

Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
@ejona86
Copy link
Copy Markdown
Member

ejona86 commented Feb 10, 2026

Can we use a typical title that includes a verb and specify the module? Like "xds: Add composite filter".

Definitely split out the protos upgrade. If you just added protos in this PR it wouldn't be as hard to look at and as likely to conflict with other changes. But given we're implementing multiple parts of this simultaneously, just adding protos probably should be separate to avoid drift while reviewing (you add a proto here, but someone else does an upgrade, but wouldn't upgrade your new proto).

Right now I'm left wondering why you needed to do the upgrade: what field/change are you pulling in? That's the sort of thing useful to describe in the PR/commit description, because there's no way I'll be able to find the specific piece you're wanting.

@AgraVator AgraVator changed the title composite filter xds: Add composite filter Feb 11, 2026
@AgraVator
Copy link
Copy Markdown
Contributor Author

f you just added protos in this PR i

xds: add protos for composite filter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All of UnifiedMatcher should be gone with #12640 as per A106.

}

/** Translate StringMatcher xDS proto to internal StringMatcher. */
public static Matchers.StringMatcher parseStringMatcher(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The stringMatcher parsing logic should not be in this PR. It will be covered by A106. You then need to just modify add the CUSTOM case.

Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Copy link
Copy Markdown
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.

I just glanced around a bit.

Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
Comment thread xds/src/main/java/io/grpc/xds/CompositeFilter.java Outdated
@ejona86
Copy link
Copy Markdown
Member

ejona86 commented May 8, 2026

@AgraVator, did you want to "Re-request review" from anyone?

}

static final class Provider implements Filter.Provider {
private static final ThreadLocal<Integer> recursionDepth = ThreadLocal.withInitial(() -> 0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A ThreadLocal would be quite unfortunate. I strongly doubt that is necessary. The gRFC says:

Note that in order to avoid potential stack overflows, we will impose a maximum recursion depth of 8 when parsing HTTP filter configs.

That doesn't say a maximum recursion depth for composite filter configs, it says all HTTP filter configs. That would mean it'd be a change to parseFilterConfig()/parseFilterConfigOverride()

@AgraVator AgraVator force-pushed the xds-composite-filter branch from f5d7b03 to 3d98fb9 Compare May 11, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants