Skip to content

Add client evictor & investigastion docs - #4138

Merged
kcp-ci-bot merged 8 commits into
kcp-dev:mainfrom
mjudeikis:leaking.pipes
May 28, 2026
Merged

kcp-ci-bot merged 8 commits into
kcp-dev:mainfrom
mjudeikis:leaking.pipes

Conversation

@mjudeikis

Copy link
Copy Markdown
Contributor

Summary

Whats in this PR:

  • pkg/pproflabels/labels.go — pprof label helper, wired into kubequota controllers
  • staging/.../apimachinery/pkg/client/constructor.go — Evict + global EvictCluster registry
  • pkg/server/clientcache_evictor.go + pkg/server/server.go — LogicalCluster delete → EvictCluster
  • Makefile — fix prometheus target so UGET_PRINT_PATH=absolute works when binary cached (needed for ./hack/run-with-prometheus.sh ./bin/kcp start --debug-socket-path=/tmp/kcp-debug.sock to work locally on mac)
  • docs/content/developers/investigations/memory-leak-attribution.md — full investigation guide

Impact:

Targeted bug (clientCache retention): 213 MB → 3.7 MB cum (98% recovery)
Total leak per workspace: 745 KB → 562 KB (-183 KB)
Goroutine leak class from #3350: already mostly fixed, wiring confirms attribution works

Next investigations identified (separate PRs):

  • 200 MB cum on structToUnstructured — likely OpenAPI v3 controller's per-cluster spec cache
  • 99 MB on json.objectInterface — JSON-decoded objects still pinned by something
  • pkg/index/index.go DeleteLogicalCluster incomplete (5 of 8 per-cluster maps not cleaned)

What Type of PR Is This?

/kind bug

Related Issue(s)

Fixes #4071 (one of few fixes, more will come)

Release Notes

Add client evictor to release memory on LogicalCluster deletes

mjudeikis added 3 commits May 21, 2026 08:33
Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
On-behalf-of: SAP <mangirdas.judeikis@sap.com>
@kcp-ci-bot kcp-ci-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. kind/bug Categorizes issue or PR as related to a bug. labels May 21, 2026
@kcp-ci-bot

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@kcp-ci-bot kcp-ci-bot added dco-signoff: yes Indicates the PR's author has signed the DCO. kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels May 21, 2026
@mjudeikis

Copy link
Copy Markdown
Contributor Author

/test all

@mjudeikis
mjudeikis marked this pull request as ready for review May 21, 2026 08:40
@kcp-ci-bot kcp-ci-bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces mechanisms and tooling to better attribute and reduce memory/goroutine retention across logical cluster lifecycles, and adds a new quickstart scenario for workspace-churn/performance testing.

Changes:

  • Add a global per-cluster client-cache eviction fan-out (EvictCluster) and wire it to LogicalCluster deletions to release cached per-workspace client state.
  • Introduce pkg/pproflabels and apply labels to kubequota controller/admission per-cluster initialization paths for improved pprof attribution.
  • Add a new quickstart “workspaces” scenario (plus flags/tests) and add investigation documentation for leak attribution.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
staging/src/github.com/kcp-dev/cli/pkg/quickstart/scenarios/workspaces.go New quickstart scenario that creates a random tree of workspaces under an org workspace.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/scenarios/workspaces_test.go Unit tests for workspaces scenario validation and deterministic tree generation.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/scenarios/scenarios_test.go Adds coverage that “workspaces” is a valid scenario in the registry.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/scenarios/scenario.go Extends Scenario with Validate and registers the new “workspaces” scenario.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/scenarios/api_provider.go Adds scenario-specific Validate for workspace-name DNS-1123 constraints.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/plugin/quickstart_test.go Updates test mocks to satisfy the new Scenario.Validate interface method.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/plugin/options.go Adds workspaces scenario tuning flags and defers prefix validation to Scenario.Validate.
staging/src/github.com/kcp-dev/cli/pkg/quickstart/cmd/cmd.go Updates CLI help text to describe the new “workspaces” scenario.
staging/src/github.com/kcp-dev/apimachinery/pkg/client/constructor.go Adds Evict support, global evictor registry, and EvictCluster fan-out tied to caches created via NewCache.
pkg/server/server.go Wires LogicalCluster informer delete events to client-cache eviction.
pkg/server/clientcache_evictor.go Implements the LogicalCluster delete handler that calls apiclient.EvictCluster.
pkg/reconciler/kubequota/kubequota_controller.go Wraps per-cluster startup in pprof labels to keep leaked goroutines attributable.
pkg/pproflabels/labels.go New helper package for attaching stable pprof labels (controller, logicalcluster).
pkg/admission/kubequota/kubequota_admission.go Wraps per-cluster quota admission delegate creation in pprof labels for attribution.
docs/content/developers/investigations/memory-leak-attribution.md Adds a leak attribution guide using pprof labels and workspace churn.
Comments suppressed due to low confidence (1)

staging/src/github.com/kcp-dev/apimachinery/pkg/client/constructor.go:63

  • The package-level evictors slice strongly references every cache ever created via NewCache (because NewCache auto-calls RegisterEvictor). If consumers create many short-lived clientsets/caches (common in tests or CLIs), this global registry will grow monotonically and prevent those caches from being garbage-collected. Consider adding an UnregisterEvictor/ResetEvictorsForTest hook, or avoiding auto-registration in NewCache in favor of explicit registration by long-lived processes (like the kcp server).
var (
	evictorsMu sync.RWMutex
	evictors   []Evictor
)

// RegisterEvictor adds e to the set of caches notified by EvictCluster.
// NewCache calls this automatically.
func RegisterEvictor(e Evictor) {
	evictorsMu.Lock()
	defer evictorsMu.Unlock()
	evictors = append(evictors, e)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 33 to +42
// Cache is a client factory that caches previous results.
type Cache[R any] interface {
ClusterOrDie(clusterPath logicalcluster.Path) R
Cluster(clusterPath logicalcluster.Path) (R, error)
// Evict drops the cached client for clusterPath, if any. Used to release
// per-cluster client state (REST clients, codec factories, parsed
// schemas) when a logical cluster is deleted. Safe to call concurrently
// with Cluster / ClusterOrDie. No-op if the path is not cached.
Evict(clusterPath logicalcluster.Path)
}

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.

I would like to make this breaking so downstream clients MUST provide eviction, else it just yet-another-leak

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

But is this something of interest for downstream? kcp-internal only has this problem because we have the shard-cluster relation and the internal view.
External clients should only see the clusters.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And since they only see the clusters they won't have this problem.

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.

I mean, the idea was that one could make this external, but I opted out of this. So we get compile errors :)

Comment thread pkg/server/server.go Resolved
@mjudeikis

Copy link
Copy Markdown
Contributor Author

/retest

@ntnn

ntnn commented May 21, 2026

Copy link
Copy Markdown
Member

/test pull-kcp-test-e2e-multiple-runs

@mjudeikis mjudeikis added the tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges. label May 25, 2026
Comment on lines +57 to +63
// RegisterEvictor adds e to the set of caches notified by EvictCluster.
// NewCache calls this automatically.
func RegisterEvictor(e Evictor) {
evictorsMu.Lock()
defer evictorsMu.Unlock()
evictors = append(evictors, e)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That also means that evictors are never deregistered.
If clients are repeatedly build, each with their own cache, this would lead to them never being GC'd because they are always referenced here.

Comment on lines +155 to +160
if c.evictGen[clusterPath] != genAtStart {
// An Evict raced with this build. Hand the freshly-built client to the
// caller so its in-flight request can complete, but do not cache it —
// the cluster has been signalled as gone.
return instance, nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're only skipping caching if the client is built during an eviction.
If a client is requested after the cluster has been evicted we'd still be caching the (dead) client and hand it back to the caller.

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.

and this is fine, we might be in the deletion flow. Client will just give error and reconciler will retry

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah the then dead client is fine because it's cache is empty?

Comment thread pkg/server/server.go Outdated
go s.CacheKcpSharedInformerFactory.Apis().V1alpha2().APIExports().Informer().Run(hookContext.Done())
go s.CacheKcpSharedInformerFactory.Cache().V1alpha1().CachedResources().Informer().Run(hookContext.Done())
go s.CacheKcpSharedInformerFactory.Cache().V1alpha1().CachedResourceEndpointSlices().Informer().Run(hookContext.Done())
installClientCacheEvictor(hookCtx, s.KcpSharedInformerFactory.Core().V1alpha1().LogicalClusters())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As in the other PR, I think this should be installed after the wait below to ensure the informer is synced before kicking off the reconciler.

- registry holds caches via weak.Pointer so dropped caches can be GC'd
- once a path is evicted the cache never re-caches for it
- install client cache evictor after informer sync
- drop named returns in kubequota controller/admission

Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
On-behalf-of: SAP <mangirdas.judeikis@sap.com>
c.Lock()
defer c.Unlock()
delete(c.clientsByClusterPath, clusterPath)
c.evicted[clusterPath] = struct{}{}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh... I don't know if this is a good idea. Technically someone could exhaust a shard by creating and deleting workspaces until the shard is restarted.
Maybe we just eat the possibility of storing dead clients.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Or we ttlcache it :D but that'd be a follow up

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.

100k churned workspaces ≈ ~6MB,

good luck doing this, for 6MB you will need 90h of create/delete :D I think this is fine :)

@ntnn ntnn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/lgtm
/approve

under duress and protest :D

@kcp-ci-bot kcp-ci-bot added the lgtm Indicates that a PR is ready to be merged. label May 28, 2026
@kcp-ci-bot

Copy link
Copy Markdown
Contributor

LGTM label has been added.

DetailsGit tree hash: 23fed95a1190b39c1b6c87848dd164bc67fc1a68

@xrstf

xrstf commented May 28, 2026

Copy link
Copy Markdown
Contributor

/approve

@kcp-ci-bot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ntnn, xrstf

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kcp-ci-bot kcp-ci-bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label May 28, 2026
@ntnn

ntnn commented May 28, 2026

Copy link
Copy Markdown
Member

/test pull-kcp-test-e2e

seems like ci infra failure?

@xrstf

xrstf commented May 28, 2026

Copy link
Copy Markdown
Contributor

/retest

@kcp-ci-bot
kcp-ci-bot merged commit 7573468 into kcp-dev:main May 28, 2026
14 checks passed
olamilekan000 pushed a commit to olamilekan000/kcp that referenced this pull request Oct 6, 2026
* wire in labels to pprof

* add workspace quickstart into cli for ease of testing

* Add client evictor to recover memory

Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
On-behalf-of: SAP <mangirdas.judeikis@sap.com>

* move to ctx logger

* update doc to use new quickstart

* address reviews

* add generation counter

* address review feedback

- registry holds caches via weak.Pointer so dropped caches can be GC'd
- once a path is evicted the cache never re-caches for it
- install client cache evictor after informer sync
- drop named returns in kubequota controller/admission

Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
On-behalf-of: SAP <mangirdas.judeikis@sap.com>

---------

Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has signed the DCO. kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API kind/bug Categorizes issue or PR as related to a bug. lgtm Indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. tide/merge-method-squash Denotes a PR that should be squashed by tide when it merges.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deleting logical clusters does not free up memory

5 participants