Skip to content

Commit 0dec1bc

Browse files
fix(lineage): never cache RESTMapping failures
The apps.cozystack.io kinds resolved during the ownership walk are served by the aggregated cozystack-api, so discovery fails transiently while it rolls or before a new ApplicationDefinition is discovered. Caching such a failure marked every object admitted within the TTL window unmanaged — permanently, since the webhook's objectSelector never re-admits labeled objects. Return the error uncached, matching the treatment of transient GET errors; steady-state cost is nil since the mapper serves mappings from its own discovery cache. Also document the read-only contract of ObjectCache.Get and stop over-claiming that sampling eviction bounds the map size. Signed-off-by: Mattia Eleuteri <[email protected]>
1 parent 697d81c commit 0dec1bc

2 files changed

Lines changed: 16 additions & 7 deletions

File tree

‎pkg/lineage/cache.go‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func NewObjectCache(ttl time.Duration) *ObjectCache {
5555

5656
// Get returns a cached lookup, if any. The second return value reports whether
5757
// the entry was present and unexpired. A nil receiver is safe.
58+
//
59+
// The returned object is the stored pointer, not a copy: the cache is
60+
// process-wide and admissions run concurrently, so callers MUST treat it as
61+
// read-only and DeepCopy before any mutation.
5862
func (c *ObjectCache) Get(apiVersion, kind, namespace, name string) (*unstructured.Unstructured, error, bool) {
5963
if c == nil {
6064
return nil, nil, false
@@ -69,8 +73,8 @@ func (c *ObjectCache) Get(apiVersion, kind, namespace, name string) (*unstructur
6973
return entry.obj, entry.err, true
7074
}
7175

72-
// Set stores a lookup result. Both successful objects and errors are cached so
73-
// that repeated misses (NotFound, NoKindMatch) do not retry every admission.
76+
// Set stores a lookup result. Successful objects and permanent NotFound
77+
// errors are cached so that repeated misses do not retry every admission.
7478
// A nil receiver is safe.
7579
func (c *ObjectCache) Set(apiVersion, kind, namespace, name string, obj *unstructured.Unstructured, err error) {
7680
if c == nil {
@@ -117,9 +121,9 @@ func (c *ObjectCache) evictExpiredSampleLocked(now time.Time, n int) {
117121

118122
const (
119123
// evictionThreshold is the size at which Set begins sampling for expired
120-
// entries. Set high enough that the sweep stays infrequent under steady
121-
// load, low enough that the map cannot grow unbounded under pathological
122-
// admission patterns.
124+
// entries. The sampling bounds the write-lock hold time, not the map
125+
// size: the map remains bounded only by the number of distinct owners
126+
// looked up within one TTL window.
123127
evictionThreshold = 4096
124128
// evictionSampleSize caps how many entries each Set inspects when
125129
// evicting. With sampleSize ≪ threshold the work is amortised across many

‎pkg/lineage/lineage.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,13 @@ func getUnstructuredObject(
231231
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
232232
if err != nil {
233233
l.Error(err, "Could not map GVK "+gvk.String())
234-
// Cache the negative result: missing CRDs aren't a transient condition.
235-
cache.Set(apiVersion, kind, namespace, name, nil, err)
234+
// Never cache mapping failures: the apps.cozystack.io kinds walked here
235+
// are served by the aggregated cozystack-api, so discovery fails
236+
// transiently while it rolls (upgrade, drain) or before a new
237+
// ApplicationDefinition is discovered. Caching the failure would mark
238+
// every object admitted in the TTL window unmanaged — permanently,
239+
// since the webhook's objectSelector never re-admits them. In steady
240+
// state the mapper serves this from its own discovery cache anyway.
236241
return nil, err
237242
}
238243

0 commit comments

Comments
 (0)