Skip to content

cache: add prefix/glob watch support to LinearCache for LEDS#1411

Merged
valerian-roche merged 7 commits into
envoyproxy:mainfrom
DataDog:wdauchy/linear-cache-prefix-watches
Mar 30, 2026
Merged

cache: add prefix/glob watch support to LinearCache for LEDS#1411
valerian-roche merged 7 commits into
envoyproxy:mainfrom
DataDog:wdauchy/linear-cache-prefix-watches

Conversation

@wdauchy

@wdauchy wdauchy commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Envoy's LEDS (Locality Endpoint Discovery Service) uses delta xDS with
glob collection subscriptions. Clients subscribe to resource names ending
in /* (e.g. ns_svc_8080/us-east-1a/*) via resource_names_subscribe
in DeltaDiscoveryRequest, and expect to receive all individual resources
whose name starts with that prefix.

Currently LinearCache only supports exact-name watches (resourceWatches)
and full wildcard (wildcardWatches). A subscription to collection/* is
stored as a literal key in resourceWatches and never matches individual
resources under that prefix.

This PR adds prefix/glob watch support to LinearCache:

Subscription layer (subscription.go):

  • Prefix globs are detected and split at subscription time into a separate
    subscribedPrefixes map (keyed by the prefix, e.g. "collection/")
  • SubscribedPrefixes() is added to the Subscription interface so the
    cache never has to check for globbing at operation time

Cache layer (linear.go):

  • New prefixWatches map handles glob subscriptions
  • trackWatch/removeWatch: use SubscribedPrefixes() directly to
    register/clean up prefix watches — no glob parsing at watch time
  • notifyAll: iterates prefixWatches for each modified resource and
    triggers matching prefix watches
  • computeResourceChange: single pass over the cache for prefix
    expansion (all prefixes checked per resource). Consolidated removal loop
    handles both unsubscription and cache deletion in one place
  • computeResponse: single cache pass for prefix expansion in the
    full-state non-wildcard branch
  • isSubscribedTo: O(1) map lookup when no prefix subscriptions are
    active, zero overhead for users not using the feature

Existing exact-name and wildcard behavior is unchanged.

@wdauchy wdauchy force-pushed the wdauchy/linear-cache-prefix-watches branch from aefeae0 to 441aa0e Compare February 20, 2026 09:44
@wdauchy wdauchy marked this pull request as ready for review February 20, 2026 09:46

@wdauchy wdauchy left a comment

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.

fyi I started to merge this on Datadog production to see how far we can go with it

@valerian-roche valerian-roche left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Logic looks okay, but it should not impact users not using the feature.
I'm also concerned on all those full cache iterations which can be very costly on endpoints cache (where wildcards are not used hence never happen today).
As xds TP1 mandates a path to be along the line of xds:://<type>/<some>/<segments>/..., we should potentially build a tree matcher to guarantee O(segments) instead of O(entries)

Comment thread pkg/cache/v3/linear.go Outdated
return true
}
for sub := range subscribedResources {
if prefix, ok := isPrefixGlob(sub); ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's a lot of matching everyone will pay even if not using the feature, changing the check from O(1) to O(subscribed).
Could it be changed to be stored in a different set?
TP1 does not define rules on the use of / within resources so I don't think O(1) is achievable, but the cost should at least be restricted to collections and not all resources

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.

agreed, fixed. isSubscribedTo now does a single O(1) map lookup on SubscribedResources() first. the prefix iteration only kicks in when SubscribedPrefixes() is non-empty, so users not using the feature pay zero extra cost

Comment thread pkg/cache/v3/linear.go Outdated
}
} else {
for resourceName := range sub.SubscribedResources() {
if prefix, ok := isPrefixGlob(resourceName); ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this be reverted? Identify all prefixes then iterate once above the cache? Iterating over a large map is slow and thrash caches

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.

done, prefix expansion now does a single pass over the cache checking all prefixes per resource (inner loop over prefixes, outer loop over cache entries). the whole block is also guarded by len(subscribedPrefixes) > 0 so it's completely skipped when no prefix subscriptions exist

Comment thread pkg/cache/v3/linear.go
Comment thread pkg/cache/v3/linear.go Outdated
}
}
// Detect resources previously returned under this prefix that were deleted.
for knownName := range knownVersions {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you clarify this one? A resource can still be directly requested or match another collection (theoretically). We should not check for negative this way, but by checking what's still matching at the end compared to previously known
Your check at line 270 should be handling this

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.

good point, removed the inline deletion check entirely. the consolidated loop over knownVersions now handles both cases in one place: unsubscription (!isSubscribedTo) and cache deletion (still subscribed but !inCache). this also correctly handles a resource that matches both an exact subscription and a prefix

@wdauchy wdauchy force-pushed the wdauchy/linear-cache-prefix-watches branch from 76e794f to 307eaa3 Compare March 10, 2026 09:31
@wdauchy

wdauchy commented Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

addressed the feedback.

re: tree matcher for O(segments), makes sense as a follow-up, the current approach is O(prefixes) per resource which should be fine for now to start with something

@wdauchy wdauchy requested a review from valerian-roche March 10, 2026 09:56
@wdauchy

wdauchy commented Mar 22, 2026

Copy link
Copy Markdown
Contributor Author

@valerian-roche if you have some time to have a look

valerian-roche
valerian-roche previously approved these changes Mar 23, 2026

@valerian-roche valerian-roche left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One minor comment left, otherwise lgtm.
Thanks for rewriting the PR in a way that avoids most impact for non-prefix users

Comment thread pkg/cache/v3/linear.go Outdated
if _, ok := sub.SubscribedResources()[resourceName]; !ok {
if !isSubscribedTo(sub, resourceName) {
removedResources = append(removedResources, resourceName)
} else if _, inCache := cache.resources[resourceName]; !inCache {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: I'd expect cache evolution to be more common than watch removal, and isSubscribedTo is more expensive when prefixes are used, so might be better to revert evaluation order

@wdauchy wdauchy Mar 23, 2026

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.

good point, swapped the order, cache deletion check (O(1) map lookup) now comes first, isSubscribedTo only runs if the resource is still in cache

wdauchy added 6 commits March 23, 2026 10:32
Envoy's LEDS uses delta xDS with glob collection subscriptions where
clients subscribe to resource names ending in /* (e.g.
ns_svc_8080/us-east-1a/*) and expect to receive all individual resources
whose name starts with that prefix.

Currently LinearCache only supports exact-name watches and full wildcard
watches. A subscription to collection/* is stored as a literal key in
resourceWatches and never matches individual resources under that prefix.

Add a new prefixWatches map to LinearCache that is keyed by the prefix
(with trailing * stripped, keeping the separator). When a subscribed
resource name ends with /*, the watch is registered in prefixWatches
instead of resourceWatches. notifyAll iterates prefixWatches for each
modified resource and triggers matching prefix watches.
computeResourceChange expands prefix subscriptions to all matching
resources in the cache, detecting new, updated, and deleted resources
under the prefix.

Signed-off-by: William Dauchy <william.dauchy@datadoghq.com>
Address review feedback:
- Move glob detection into Subscription (SubscribedPrefixes) so prefix
  sets are precomputed once at subscription time instead of checked on
  every cache operation.
- Single pass over cache.resources for prefix expansion instead of one
  iteration per prefix subscription.
- Consolidated removal check: detect both unsubscription and cache
  deletion in a single loop over knownVersions.
- isSubscribedTo is O(1) when no prefix subscriptions are active.
- Remove isPrefixGlob and isResourceMatchingSubscription helpers from
  the cache package.

Signed-off-by: William Dauchy <william.dauchy@datadoghq.com>
Signed-off-by: William Dauchy <william.dauchy@datadoghq.com>
Signed-off-by: William Dauchy <william.dauchy@datadoghq.com>
Signed-off-by: William Dauchy <william.dauchy@datadoghq.com>
@wdauchy wdauchy force-pushed the wdauchy/linear-cache-prefix-watches branch from d0daea0 to 1c25d95 Compare March 23, 2026 09:34
@wdauchy wdauchy requested a review from valerian-roche March 23, 2026 09:35
@wdauchy

wdauchy commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

golangci-lint are failing but I don't think it is related to my PR

@wdauchy

wdauchy commented Mar 26, 2026

Copy link
Copy Markdown
Contributor Author

@valerian-roche is it ready to merge?

@valerian-roche

Copy link
Copy Markdown
Contributor

@valerian-roche is it ready to merge?

@wdauchy Can you rebase? The linter issue has been addressed on main

@wdauchy

wdauchy commented Mar 29, 2026

Copy link
Copy Markdown
Contributor Author

@wdauchy Can you rebase? The linter issue has been addressed on main

should be good now!

@valerian-roche valerian-roche merged commit 7d94a89 into envoyproxy:main Mar 30, 2026
5 checks passed
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.

2 participants