fix(executor): drop event watcher cache buckets when their last event goes - #7993
Open
Daksha1611 wants to merge 1 commit into
Open
fix(executor): drop event watcher cache buckets when their last event goes#7993Daksha1611 wants to merge 1 commit into
Daksha1611 wants to merge 1 commit into
Conversation
… goes objectCache kept a bucket for every (namespace, name, kind) it ever saw an event for. OnDelete removed the event from the bucket but deliberately left the bucket itself behind, to avoid racing a concurrent store against the removal of the top-level map entry, and nothing else ever removed one. The map therefore grew for the life of the process, keyed by every object the cluster emitted an event about rather than by what the executor still cares about. Pod names are unique per attempt, so even the Flyte-only subset never reached a steady state. Driving store and OnDelete for 200k distinct objects and then deleting every event left all 200k buckets and ~116 MB resident with every inner map empty. The bucket is now dropped when its last event is deleted, which closes the race rather than avoiding it: the evicted flag and the map removal both happen under the bucket's own write lock, so a store that already loaded the bucket blocks, observes the flag and retries against a fresh one, while a store that has not loaded it yet cannot reach it once it is out of the map. The lock order is bucket-then-map here and map-then-bucket nowhere, since every other path releases the map's lock before taking a bucket's, so the two cannot deadlock. The same 200k probe now retains nothing. This bounds objectCache to objects with a live event. It does not address the other half of flyteorg#7992, that the Event informer itself is unscoped and caches every Event in the cluster; scoping it needs a decision about which events the executor should watch. Signed-off-by: Daksha1611 <mehtadaksha1611@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tracking issue
Related to #7992 (fixes the second of the two problems reported there)
Why are the changes needed?
controllerRuntimeEventWatcher.objectCachehad no eviction path at all.storecreates one bucket per(namespace, name, kind),OnDeleteremoves the event from the bucket but deliberately leaves the bucket behind, and nothing anywhere else removes one — noDelete, noLoadAndDelete, no reaper.The existing comment explains why the bucket was left in place:
That race is real, but avoiding it this way means the map grows for the entire life of the process. Kubernetes expires Events after ~1h so the inner maps drain, but the outer buckets never go. The key space is every object the cluster emitted an event about since the executor started, and because
buildGeneratedNamemakes pod names unique per attempt ({action}-{retry}), even the Flyte-only subset grows with cumulative attempts rather than with concurrency — it never reaches a steady state.Driving
storeandOnDeletefor 200k distinct objects, deleting every event, then forcing a GC:Every inner map is empty and all 200k buckets are still held. On a busy cluster this is a slow, unbounded climb toward the OOMKill that #7831 described.
What changes were proposed in this pull request?
The bucket is now dropped when its last event is deleted, which closes the race rather than avoiding it.
eventObjectsgains anevictedflag. Both setting the flag and removing the map entry happen under that bucket's own write lock:and
storeretries when it finds the bucket it loaded has been evicted:This is correct in both directions:
evictedand retries. Because the entry is already out of the map by then,LoadOrStoregives it a fresh bucket rather than the dead one.The loop cannot spin: it turns at most once per concurrent eviction of that object's bucket, and eviction only happens when the bucket is empty.
The lock order is bucket-then-map here, and map-then-bucket nowhere — every other path (
Load/LoadOrStoreinstore,OnDelete,List) releases the map's internal lock before taking a bucket's — so takingobjectCache.Deleteunder the bucket lock cannot deadlock.One incidental change:
storenow writes a copy of theeventInfoper attempt instead of mutating and inserting a single shared value, so a retry cannot carry over the merge it did against the bucket it lost.objectCacheis now bounded by objects with a live event instead of by every object seen since startup.Deliberately not included
This does not address the other half of #7992 — that the Event informer is registered with no
ByObjectentry and so does a full unfiltered LIST/WATCH of every Event in the cluster. Scoping it needs a maintainer decision about which events the executor should watch (aregarding.kind=Podfield selector, namespace scoping, or a transform), so I have left it to the issue rather than guessing here. Happy to follow up with a second PR once there is a direction.How was this patch tested?
Added to
event_watcher_test.go:TestEventWatcherDropsBucketWhenLastEventIsDeleted— the bucket survives while any event for the object remains and goes with the last one.TestEventWatcherDoesNotGrowAcrossObjectChurn— 1000 distinct objects added then deleted leaves 0 buckets.TestEventWatcherStoreSurvivesConcurrentEviction— 400 goroutines racing stores against evictions on the same object; asserts the surviving event is still reachable, i.e. an eviction that raced a store did not swallow it.Results:
Re-running the 200k probe from the issue against this branch:
All pre-existing tests in the package still pass unchanged.
Labels
fixed
Check all the applicable boxes