Skip to content

client-go/codegen: cache only the group clients in use per LC - #4526

Merged
kcp-ci-bot merged 1 commit into
kcp-dev:mainfrom
neolit123:4401-cache-only-clients-in-use
Nov 5, 2026
Merged

kcp-ci-bot merged 1 commit into
kcp-dev:mainfrom
neolit123:4401-cache-only-clients-in-use

Conversation

@neolit123

@neolit123 neolit123 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ClusterClientset.Cluster() now returns a lightweight wrapper that delegates to the per-group cluster clients instead of caching a full k8s clientset. (EDIT: i also added an interface for Group). WSs now only retain clients for the API groups they actually use.

some stats for 3000 WS, one group only (rbacv1)
before: ~90mb
after: ~2mb (98% reducation)

if all WS uses all groups (50/60 or so), the reducation is 0%.

What Type of PR Is This?

/kind feature

Related Issue(s)

Fixes #4401

NOTE: the ticket proposed two options. i tried both. this one ended up being simpler and less diff but scales poorly if all WS use many API groups. the other one was about sharing the same clientset for all WS, big diff, and IMO that introduced some potential for rate limiting problems. despite that, if it's still preferred i can go that route instead.

Release Notes

reduce the kcp memory usage per workspace, by introducing a client cache only for the API groups in use, instead of a caching the entire k8s clientset per workspace.

@kcp-ci-bot kcp-ci-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. kind/feature Categorizes issue or PR as related to a new feature. dco-signoff: yes Indicates the PR's author has signed the DCO. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Oct 30, 2026
@neolit123

neolit123 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

/retest

CACHE W1030 12:46:27.701400 25562 watcher.go:350] watch chan error: etcdserver: mvcc: required revision has been compacted
time=2026-10-30T12:46:30.875Z level=ERROR source=manager.go:259 msg="error reloading target set" component="scrape manager" err="invalid config id:kcp-main-TestInactiveLogicalClusterBlocksRequests-1793364383"

unsure if related.

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.

🟡 Changes recommended

Automatically created rate limiters are now shared across logical clusters, allowing a busy workspace to exhaust unrelated workspaces’ request budgets; per-cluster isolation must be restored and affected generated clients updated.

1 open issue
6 files not reviewed
  • staging/src/github.com/kcp-dev/client-go/apiextensions/client/clientset.go: Generated file
  • staging/src/github.com/kcp-dev/client-go/kubernetes/clientset.go: Generated file
  • staging/src/github.com/kcp-dev/code-generator/examples/pkg/kcp/clients/clientset/versioned/clientset.go: Generated file
  • staging/src/github.com/kcp-dev/code-generator/examples/pkg/kcpexisting/clients/clientset/versioned/clientset.go: Generated file
  • staging/src/github.com/kcp-dev/sdk/client/clientset/versioned/cluster/clientset.go: Generated file
  • test/e2e/fixtures/wildwest/client/clientset/versioned/cluster/clientset.go: Generated file
What changed in this PR

Reduces per-workspace client memory usage by caching only API-group clients in use rather than complete clientsets, addressing #4401.

Changes:

  • Returns lightweight scoped wrappers that delegate to existing per-group client caches.
  • Replaces full-clientset caches with dedicated discovery-client caches.
  • Updates the generator and regenerates clientsets, examples, and the Wildwest fixture.
File Description
test/​e2e/​fixtures/​wildwest/​client/​clientset/​versioned/​cluster/​clientset.go Applies lazy group-client delegation to the Wildwest fixture.
staging/​src/​github.com/​kcp-dev/​sdk/​client/​clientset/​versioned/​cluster/​clientset.go Adds scoped wrappers and discovery caching for SDK clients.
staging/​src/​github.com/​kcp-dev/​code-generator/​examples/​pkg/​kcpexisting/​clients/​clientset/​versioned/​clientset.go Updates the existing-client generation example.
staging/​src/​github.com/​kcp-dev/​code-generator/​examples/​pkg/​kcp/​clients/​clientset/​versioned/​clientset.go Updates the standard generation example.
staging/​src/​github.com/​kcp-dev/​code-generator/​cmd/​cluster-client-gen/​generators/​generator_for_clientset.go Generates scoped accessors, typed imports, and discovery caches.
staging/​src/​github.com/​kcp-dev/​client-go/​kubernetes/​clientset.go Uses lazy per-group clients for Kubernetes APIs.
staging/​src/​github.com/​kcp-dev/​client-go/​apiextensions/​client/​clientset.go Uses lazy per-group clients for API extensions.

Review effort: Balanced

@neolit123

Copy link
Copy Markdown
Contributor Author

/retest

@neolit123 neolit123 changed the title client-go/codegen: cache only the group clients in use per LC WIP: client-go/codegen: cache only the group clients in use per LC Oct 30, 2026
@kcp-ci-bot kcp-ci-bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 30, 2026
@neolit123
neolit123 force-pushed the 4401-cache-only-clients-in-use branch 2 times, most recently from 8e68bae to cb9aed7 Compare October 30, 2026 14:38
@neolit123 neolit123 changed the title WIP: client-go/codegen: cache only the group clients in use per LC client-go/codegen: cache only the group clients in use per LC Oct 30, 2026
@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 Oct 30, 2026
@mjudeikis

Copy link
Copy Markdown
Contributor

@mjudeikis-bot revliew this please

})
}

//nolint:paralleltest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

any reason these dont have parallel test?

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.

copied from constructor_test.go. can be dropped and add t.Parallel()

@mjudeikis-bot mjudeikis-bot 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.

Two Notes, no Critical or Warning findings. The per-logical-cluster rate-limit regression that Copilot raised is already addressed by ClusterRateLimiter on this head, so I did not re-raise it.

Not anchored

  • Note: the generated scopedClientset wrapper (the core behaviour of this PR) has no test. Nothing checks that Cluster(p).RbacV1() creates only the rbac group cache entry for p, or that it returns the same client as ClusterClientset.RbacV1().Cluster(p). The existing e2e tests exercise the wrapper but would pass equally with the old full-clientset cache. A small test in staging/src/github.com/kcp-dev/client-go/kubernetes or in the code-generator examples would pin the memory property the PR claims.

Checked

  • Read beyond the diff: all of apimachinery/pkg/client/constructor.go (clientCache.Cluster, Evict, the weak evictor registry), pkg/server/clientcache_evictor.go callers of EvictCluster, and the typed group client constructors (apiextensions/v1 NewForConfigAndClient, setConfigDefaults).
  • Correctness: NewForConfigAndClient now builds the discovery cache from &configShallowCopy. The old code used c, so the shared ClusterRateLimiter reaches the discovery cache and the group caches. The group config copies keep the limiter pointer, and clientCache.Cluster swaps in the per-cluster limiter. New(c) adds an HTTPClientFor call that panics only on a config that NewForConfigOrDie would already have rejected. No problem found.
  • Concurrency: ClusterRateLimiter guards its map with a mutex, and the evictor registration follows the existing weak-ref pattern. The one issue is the evict/re-create ordering (inline Note).
  • kcp specifics: logical-cluster scoping is preserved through the group caches, and the wildcard still panics in Cluster(). Per-cluster limiters match the previous behaviour, because the old cache was built from the unmodified c and each cluster got its own limiter. Shard and cache-server paths are not touched.
  • API and storage: no CRD or stored-format changes. The generated clientsets in client-go, sdk, code-generator examples and the e2e fixtures all show the same pattern. The ClusterInterface return type client.Interface is unchanged.
  • Security: no request parsing, credentials or RBAC changes.
  • Tests: rate_limiter_test.go covers per-cluster isolation, sharing across caches, eviction and user-limiter passthrough. TestClusterRateLimiter_EvictCluster passes even if a later Cluster() call re-creates the entry (inline Note). The wrapper itself has no unit test (Not anchored Note).
  • Commands run:
    • go build ./... at the repo root: ok.
    • go build ./... and go vet ./... in staging/.../sdk and staging/.../code-generator: clean.
    • go build in staging/.../client-go: only the main package error from a root package main file, which this PR does not touch.
    • go test -race ./pkg/client/... in apimachinery: ok.
    • go test ./cmd/... in code-generator: no test files.
    • I did not re-run make codegen to confirm the generated output matches the generator.
  • e2e: not run. Running it needs a full kcp server plus multi-shard setup, which I did not attempt here, so the memory claim in the PR body (~90MB to ~2MB) is unverified by me.

limiter, ok := l.limiters[clusterPath]
if !ok {
limiter = flowcontrol.NewTokenBucketRateLimiter(l.qps, l.burst)
l.limiters[clusterPath] = limiter

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note: Cluster() re-creates a limiter after Evict, leaking one entry per deleted workspace that is touched later
clientCache deliberately tombstones evicted paths (c.evicted) so that a late Cluster() for a deleted workspace does not re-cache state. ClusterRateLimiter has no tombstone. After EvictCluster(p) deletes limiters[p], a late in-flight clientCache.Cluster(p) (the evicted branch still builds an instance) calls limiter.Cluster(p) and inserts a new entry that is never removed. The cost is small, a token bucket per touched deleted workspace, but it is the same leak class the evictor exists to prevent, and TestClusterRateLimiter_EvictCluster does not catch it. Fix: keep an evicted set like clientCache and return an uncached limiter for evicted paths.

limiter, ok := l.limiters[clusterPath]
if !ok {
	limiter = flowcontrol.NewTokenBucketRateLimiter(l.qps, l.burst)
	if _, evicted := l.evicted[clusterPath]; !evicted {
		l.limiters[clusterPath] = limiter
	}
}
return limiter

@mjudeikis mjudeikis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall this is close, just 1 comment from my bot AND lets add public interface

"github.com/kcp-dev/logicalcluster/v3"
)

type ClusterRateLimiter struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we need to add explisit interface here:

type ClusterAwareRateLimiter interface {
	flowcontrol.RateLimiter
	Cluster(clusterPath logicalcluster.Path) flowcontrol.RateLimiter
}

}

cfg := SetCluster(rest.CopyConfig(c.cfg), clusterPath)
if limiter, ok := cfg.RateLimiter.(*ClusterRateLimiter); ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These will become cast to interface cfg.RateLimiter.(ClusterAwareRateLimiter)

@neolit123
neolit123 force-pushed the 4401-cache-only-clients-in-use branch from cb9aed7 to 5f0eff9 Compare November 3, 2026 12:35
@neolit123

Copy link
Copy Markdown
Contributor Author

@neolit123

neolit123 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

need to rebase to main and re-run codegen

go: github.com/kcp-dev/kcp/cmd/virtual-workspaces/command imports
github.com/kcp-dev/client-go/kubernetes imports
k8s.io/client-go/kubernetes/typed/scheduling/v1alpha2: module k8s.io/client-go@latest found (v0.37.2, replaced by github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go@v0.0.0-20261030071914-98714686dd41), but does not contain package k8s.io/client-go/kubernetes/typed/scheduling/v1alpha2
make: *** [Makefile:539: modules] Error 1

@neolit123
neolit123 force-pushed the 4401-cache-only-clients-in-use branch 2 times, most recently from 1eef066 to 723f619 Compare November 3, 2026 12:57
"github.com/kcp-dev/logicalcluster/v3"
)

func TestClusterDelegatesToGroupClients(t *testing.T) {

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.

it was requested.

@neolit123
neolit123 force-pushed the 4401-cache-only-clients-in-use branch 2 times, most recently from 43a7850 to 0500b75 Compare November 3, 2026 13:43
defer l.lock.Unlock()
limiter, ok := l.limiters[clusterPath]
if !ok {
limiter = flowcontrol.NewTokenBucketRateLimiter(l.qps, l.burst)

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.

Doesn't that change the meaning of the QPS and burst since it applies to each workspace client by itself and make it technically unbounded?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it does. But sharing one bucket IMO is worse. Once workspace will take down all clients quota :/

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.

one bucket per WS makes more sense to me, but LMK if you think that this needs to change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WOnder how this would look like with P&F shape:

  1. Global bucket
  2. LC buckets
  3. Per APIGroups buckets.

Basically hierarchy of bucketing

@neolit123 neolit123 Nov 3, 2026 •

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.

this current one only tracks LCs with Cluster(...), but the implementors could track multi-LC that way (so global in some sense). maybe we can add another interface that works for groups too. i will see if that's doable in some reasonable form.

@neolit123 neolit123 Nov 3, 2026 •

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.

https://github.com/kcp-dev/kcp/compare/c49933fa3e838c8322cc25b48ea97d4758681261..9719335771d08cba49ca133709436317afdc17e8

is one idea (latest force push here)

so if the user implements also

type GroupAwareRateLimiter interface {
	flowcontrol.RateLimiter
	Group(groupVersion schema.GroupVersion) flowcontrol.RateLimiter
}

then from Cluster() we can call Group() too if that iface is implemented. then the implementor could choose how to orchestrate it on t he global/lc/group level.

	cfg := SetCluster(rest.CopyConfig(c.cfg), clusterPath)
	if limiter, ok := cfg.RateLimiter.(ClusterAwareRateLimiter); ok {
		cfg.RateLimiter = limiter.Cluster(clusterPath)
		if groupLimiter, ok := cfg.RateLimiter.(GroupAwareRateLimiter); ok {
			groupVersion, err := c.groupVersion()
			if err != nil {
				var result R
				return result, err
			}
			cfg.RateLimiter = groupLimiter.Group(groupVersion)
		}
	}

this test shows the example
https://github.com/kcp-dev/kcp/compare/c49933fa3e838c8322cc25b48ea97d4758681261..9719335771d08cba49ca133709436317afdc17e8#diff-0d12ea246682986f9e4e269d72a8a781cab8f856a1f1bd7205280e1e0d12df2fR157

EDIT: overall maybe not the best solution but it allows the low memory + also allows for implementors to rate per group.

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.

one bucket per WS makes more sense to me, but LMK if you think that this needs to change.

I don't mind either way, I just wanted to mention it. Because technically that would make qps/burst unbounded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think its fine to merge this as is, but we need to think bigger picture - P&F cluster aware

@neolit123
neolit123 force-pushed the 4401-cache-only-clients-in-use branch from 0500b75 to c49933f Compare November 3, 2026 15:32
ClusterClientset.Cluster() now returns a lightweight wrapper
that delegates to the per-group cluster clients instead
of caching a full k8s clientset. WSs now only retain
clients for the API groups they actually use.

When no RateLimiter is supplied, the ClusterClientset now uses
a ClusterRateLimiter that hands out a token bucket per LC,
shared by all group clients of that LC, so one busy WS cannot
exhaust the request budget of the others.

Signed-off-by: Lubomir I. Ivanov <neolit123@gmail.com>
@neolit123
neolit123 force-pushed the 4401-cache-only-clients-in-use branch from c49933f to 9719335 Compare November 3, 2026 17:53
@kcp-ci-bot kcp-ci-bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Nov 3, 2026

@mjudeikis mjudeikis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/lgtm
/approve

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

Copy link
Copy Markdown
Contributor

LGTM label has been added.

DetailsGit tree hash: bd4b3e17655228068b1d34b436d2f19ab0f0f8a1

@kcp-ci-bot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: mjudeikis

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 Nov 5, 2026
@kcp-ci-bot
kcp-ci-bot merged commit 2e4bfd8 into kcp-dev:main Nov 5, 2026
19 checks passed
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/feature Categorizes issue or PR as related to a new feature. 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A full clientset is cached per logical cluster

6 participants