[YUNIKORN-3357] Add goroutine leak detection to tests via goleak - #1124
[YUNIKORN-3357] Add goroutine leak detection to tests via goleak#1124tigerquoll wants to merge 3 commits into
Conversation
Wire uber-go/goleak into the test suite of every test package (20 of them; pkg/rmproxy has no test functions) through a shared pkg/common/leakcheck helper. All shared exemptions live in a single documented baseline list: it blocks new kinds of goroutine leaks from being introduced while the seven pre-existing leaked shapes are fixed and removed from the list in follow-up changes. VerifyTestMain accepts per-package extra options so the shared list never needs widening. No existing test or production code is modified. goleak was already a transitive dependency and becomes a direct test-only dependency.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1124 +/- ##
==========================================
- Coverage 81.62% 81.55% -0.07%
==========================================
Files 104 105 +1
Lines 14251 14262 +11
==========================================
Hits 11632 11632
- Misses 2330 2341 +11
Partials 289 289 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
chenyulin0719
left a comment
There was a problem hiding this comment.
+1 LGTM
- It's a good start to cleaning up goroutine leaks. Exemptions should be tracked later.
- funcN closure names should be tracked later too — positional naming is fragile.
- Comments in leakcheck.go are verbose but clear.
wilfred-s
left a comment
There was a problem hiding this comment.
Instead of the large comments for the exclusions, point to the jira logged for the fix and use the comment as the jira description.
if we're not fixing it we should have a comment.
Missing: the go.sum update
| // defect; delete this entry once Stop() deregisters the callback. | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/events.(*eventPublisher).start.func1"), |
There was a problem hiding this comment.
This is done on purpose we need that routine to keep running after a stop to make sure the system can be turned on and off via the config update.
It is also a false positive as the function exits directly after calling. The function is registered but the call only happens on change, no leaks here.
| // replies. Note notifyRMAllocationReleased has the same unbuffered-reply | ||
| // shape and would leak under a different top frame; it has not been seen | ||
| // yet, so it is deliberately not exempted here. |
There was a problem hiding this comment.
Can be added as there is no reason to fix either of these: the events are dropped either way and the app is exiting so nothing to be handled.
| // Two distinct causes share this top frame. One is a test that creates a | ||
| // stream and never calls RemoveStream. The other is a consumer that stops | ||
| // reading, which wedges the forwarder on the "consumer <- event" send | ||
| // outside its select, where neither stop channel can reach it; that one is |
There was a problem hiding this comment.
The push outside of the select is the first batch of records during the opening of the stream. This would mean that the requestor closed the connection even before it was really started and they did request a non 0 history (not the default).
This is a defect and needs a jira to fix the push of the history.
| // Tests across events, scheduler and objects call events.Init() followed by | ||
| // StartService()/StartServiceWithPublisher() without a matching Stop(). | ||
| // Stoppable: delete this entry once those tests defer Stop(). | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/events.(*EventSystemImpl).StartServiceWithPublisher.func1"), |
There was a problem hiding this comment.
This should be fixed on the core side before we turn this check on
| // configmap callback that Stop() never removes, so a reload after Stop() | ||
| // resurrects a system nobody holds a reference to. Suspected production | ||
| // defect; delete this entry once Stop() deregisters the callback. | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/events.(*eventPublisher).start.func1"), |
There was a problem hiding this comment.
false positive, the function to update from config cannot be called after shutdown as the system is exiting.
|
|
||
| // Partition expired application cleaner, the second goroutine started by | ||
| // partitionManager.Run. Same cause and same fix as cleanRoot above. | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/scheduler.(*partitionManager).cleanExpiredApps"), |
| // a partition resolves users. Stoppable: UserGroupCache.Stop() closes the | ||
| // cleaner and resets the singleton, and ClusterContext.Stop() calls it. | ||
| // Delete this entry once those tests defer Stop(). | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/common/security.(*UserGroupCache).run"), |
| // Tests across events, scheduler and objects call events.Init() followed by | ||
| // StartService()/StartServiceWithPublisher() without a matching Stop(). | ||
| // Stoppable: delete this entry once those tests defer Stop(). | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/events.(*EventSystemImpl).StartServiceWithPublisher.func1"), |
There was a problem hiding this comment.
this should get a follow up jira
|
|
||
| // Event stream forwarder, started by EventStreaming.CreateEventStream. | ||
| // Two distinct causes share this top frame. One is a test that creates a | ||
| // stream and never calls RemoveStream. The other is a consumer that stops |
There was a problem hiding this comment.
that is a test breakage and needs a follow up jira
| // a partition resolves users. Stoppable: UserGroupCache.Stop() closes the | ||
| // cleaner and resets the singleton, and ClusterContext.Stop() calls it. | ||
| // Delete this entry once those tests defer Stop(). | ||
| goleak.IgnoreTopFunction("github.com/apache/yunikorn-core/pkg/common/security.(*UserGroupCache).run"), |
There was a problem hiding this comment.
one follow up jira for all 3 cases in one jira: a call to create a new cluster context should have a defer to Stop()
Replace the per-exemption prose with a one-line identifier and the JIRA that tracks removing it, and condense the header to the mechanism note. The exemption list itself is unchanged: comments only.
The two IgnoreTopFunction matching gotchas are properties of the matching mechanism rather than of any one exemption, so they are not covered by the follow-up JIRAs and belong in the file. Comments only.
@wilfred-s go.sum update not required - this module was previously brought in as a secondary dependency |
|
JIRA Issues added to https://issues.apache.org/jira/browse/YUNIKORN-3357 , with comments updated to point to the JIRA |
What is this PR for?
Adds goroutine leak detection to the test suite using uber-go/goleak, wired into every test package (20 of them) via a shared
pkg/common/leakcheckhelper:TestMainper package; all shared exemptions live in a single documented list inleakcheck.options();VerifyTestMainaccepts per-package extra options so the shared list never needs wideningpkg/rmproxyis the one deliberate exception: it has no test functions, so a hook there would guard nothing (documented in the package doc)No existing test or production code is modified.
goleakwas already a transitive dependency (go.sumunchanged); it is promoted to a direct test-only dependency.The baseline exemption list is a burn-down list
Switching detection on surfaced seven pre-existing leaked-goroutine occurrences. Each has a documented exemption so this change lands green and blocks new kinds of leaks immediately; each exemption is to be removed by its own follow-up fix:
EventSystemImpl.StartServiceWithPublisher.func1Stop()eventPublisher.start.func1Init()registers a configmap callback thatStop()never removes — a config update after shutdown restarts the event systemEventStreaming.CreateEventStream.func1RemoveStream; (b) forwarder blocks on a bareconsumer <- eventsend, so slow-consumer eviction cannot release itpartitionManager.cleanRootClusterContextwithoutStop()partitionManager.cleanExpiredAppssecurity.UserGroupCache.runClusterContext.Stop()already stops itClusterContext.notifyRMNewAllocationStopAllstops the scheduler before the RM proxy; a queued allocation event can win the select over the closed stop channel, then the notify blocks forever on its unbuffered reply becausehandleRMEventsexits without draining (reproduced ~43% of fullpkg/scheduler/testsruns before the exemption)Items 2, 3(b) and 7 are production shutdown gaps rather than test hygiene; they will be filed as separate JIRAs.
Known limitation, stated in the code: the exemptions match on top stack frame, so the baseline is a ratchet against new leak kinds, not an instance count; three entries key on positional
.funcNclosure names that the follow-up fixes should replace by hoisting the goroutine bodies to named methods.What type of PR is it?
What is the Jira issue?
https://issues.apache.org/jira/browse/YUNIKORN-3357
How should this be tested?
go test -count=1 ./pkg/scheduler/tests/×5: all green (this package's leak is the intermittent Changes to README to clarify build process #7 above — before its exemption it failed ~43% of full runs)go test -count=3on the other instrumented packages: no goleak failuresgo test -race ./pkg/...and the CI-style-race -tags deadlockrun with deadlock detection enabled: all 20 packages okmake lint,make license-check,go vet ./pkg/...: cleanNote: four packages (
common/security,entrypoint,metrics,scheduler/objects) fail undergo test -count=3on unmodified master as well — pre-existing non-idempotent tests over process-global state, unrelated to this change and not goleak failures.Generated by the Author with assistance from Claude Code