caching the call for GetNamespaceFilter

Introduces a concurrent-safe sync.Map cache to the backup Request struct
to memoize GetNamespaceFilter results. This avoids re-evaluating glob
patterns for every item, significantly improving backup performance while
preserving the original exact-match precedence logic.

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
This commit is contained in:
Adam Zhang
2026-06-11 15:21:50 +08:00
parent 89f18122c2
commit 13f06026c0
3 changed files with 52 additions and 4 deletions
@@ -0,0 +1 @@
Fix issue #9907, add cache for the GetNamespaceFilter call
+28
View File
@@ -6107,15 +6107,43 @@ func TestGetNamespaceFilter(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// First call (populates cache)
result := req.GetNamespaceFilter(tt.namespace)
if tt.expectNil {
assert.Nil(t, result)
// Verify negative cache
val, ok := req.NamespaceFilterCache.Load(tt.namespace)
assert.True(t, ok)
assert.Nil(t, val)
} else {
assert.NotNil(t, result)
// Ensure the returned filter points to the correct reference in our map
assert.Same(t, filterMap[tt.expectMatched], result)
// Verify positive cache
val, ok := req.NamespaceFilterCache.Load(tt.namespace)
assert.True(t, ok)
assert.Same(t, filterMap[tt.expectMatched], val)
}
// Second call (hits cache)
result2 := req.GetNamespaceFilter(tt.namespace)
assert.Same(t, result, result2)
})
}
}
func TestGetNamespaceFilter_CacheBypass(t *testing.T) {
req := &Request{
NamespacedFilterMap: make(map[string]*ResolvedNamespaceFilter),
}
cachedFilter := &ResolvedNamespaceFilter{}
req.NamespaceFilterCache.Store("cached-ns", cachedFilter)
// Since NamespacedFilterMap is empty, this would normally return nil,
// but the cache should return our cachedFilter.
assert.Same(t, cachedFilter, req.GetNamespaceFilter("cached-ns"))
}
+23 -4
View File
@@ -100,6 +100,10 @@ type Request struct {
// NamespacedFilterPatterns preserves the order of patterns for first-match semantics
// and caches pre-compiled globs to avoid repeated compilation in the hot path.
NamespacedFilterPatterns []NamespacedFilterPattern
// NamespaceFilterCache memoizes the resolved filter for a given namespace.
// sync.Map is used because item backuppers access this concurrently.
NamespaceFilterCache sync.Map
}
// NamespacedFilterPattern pairs a namespace pattern string with its pre-compiled
@@ -149,22 +153,37 @@ func (r *Request) StopWorkerPool() {
// GetNamespaceFilter returns the resolved filter for a namespace, or nil
// if the namespace should use global filters. Uses first-match semantics
// when multiple patterns could match the same namespace.
// when multiple patterns could match the same namespace, but exact matches
// always take precedence over glob patterns regardless of definition order.
func (r *Request) GetNamespaceFilter(namespace string) *ResolvedNamespaceFilter {
if r.NamespacedFilterMap == nil {
return nil
}
// First check for exact match
// 1. Check the concurrent cache first
if val, ok := r.NamespaceFilterCache.Load(namespace); ok {
if val == nil {
return nil
}
return val.(*ResolvedNamespaceFilter)
}
// 2. Check for exact match first
if f, ok := r.NamespacedFilterMap[namespace]; ok {
r.NamespaceFilterCache.Store(namespace, f)
return f
}
// Walk patterns in definition order using pre-compiled globs (no allocation per call)
// 3. Walk patterns in definition order using pre-compiled globs
for _, p := range r.NamespacedFilterPatterns {
if p.Compiled != nil && p.Compiled.Match(namespace) {
return r.NamespacedFilterMap[p.Pattern]
filter := r.NamespacedFilterMap[p.Pattern]
r.NamespaceFilterCache.Store(namespace, filter)
return filter
}
}
// 4. Cache the miss
r.NamespaceFilterCache.Store(namespace, nil)
return nil
}