Skip to content

Commit cf096e6

Browse files
Joeavaikathclaude
andcommitted
Optimize Kubernetes scheme creation with configuration-based caching
Add sync.Map-based cache for runtime.Scheme objects indexed by ClientOptions configuration. This eliminates repeated scheme creation across different commands using the same configuration. Changes: - NewSchemeWithTypes() now caches schemes by configuration key - Add schemeKey type to uniquely identify scheme configurations - Add TestNewSchemeWithTypesCaching tests to verify caching Impact: Reduces allocations for ~19 client creation calls across different commands that use the same scheme configuration. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent af14144 commit cf096e6

2 files changed

Lines changed: 144 additions & 1 deletion

File tree

cmd/shared/client.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"net"
23+
"sync"
2324
"time"
2425

2526
nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1"
@@ -31,6 +32,18 @@ import (
3132
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
3233
)
3334

35+
// schemeKey uniquely identifies a scheme configuration
36+
type schemeKey struct {
37+
hasNonAdmin bool
38+
hasVelero bool
39+
hasCore bool
40+
}
41+
42+
var (
43+
// Cache schemes by configuration
44+
schemeCache sync.Map // map[schemeKey]*runtime.Scheme
45+
)
46+
3447
// ClientOptions holds configuration for creating Kubernetes clients
3548
type ClientOptions struct {
3649
// IncludeNonAdminTypes adds OADP NonAdmin CRD types to the scheme
@@ -122,6 +135,19 @@ func NewClientWithFullScheme(f client.Factory) (kbclient.WithWatch, error) {
122135

123136
// NewSchemeWithTypes creates a new runtime scheme with the specified types
124137
func NewSchemeWithTypes(opts ClientOptions) (*runtime.Scheme, error) {
138+
// Create cache key from options
139+
key := schemeKey{
140+
hasNonAdmin: opts.IncludeNonAdminTypes,
141+
hasVelero: opts.IncludeVeleroTypes,
142+
hasCore: opts.IncludeCoreTypes,
143+
}
144+
145+
// Try cache first
146+
if cached, ok := schemeCache.Load(key); ok {
147+
return cached.(*runtime.Scheme), nil
148+
}
149+
150+
// Create new scheme
125151
scheme := runtime.NewScheme()
126152

127153
if opts.IncludeNonAdminTypes {
@@ -142,7 +168,9 @@ func NewSchemeWithTypes(opts ClientOptions) (*runtime.Scheme, error) {
142168
}
143169
}
144170

145-
return scheme, nil
171+
// Store in cache (handles race conditions)
172+
actual, _ := schemeCache.LoadOrStore(key, scheme)
173+
return actual.(*runtime.Scheme), nil
146174
}
147175

148176
// GetCurrentNamespace gets the current namespace from the kubeconfig context

cmd/shared/client_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2025 The OADP CLI Contributors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package shared
18+
19+
import (
20+
"testing"
21+
)
22+
23+
// TestNewSchemeWithTypesCaching verifies that schemes are cached by configuration
24+
func TestNewSchemeWithTypesCaching(t *testing.T) {
25+
opts := ClientOptions{
26+
IncludeNonAdminTypes: true,
27+
IncludeVeleroTypes: true,
28+
}
29+
30+
scheme1, err1 := NewSchemeWithTypes(opts)
31+
scheme2, err2 := NewSchemeWithTypes(opts)
32+
33+
if err1 != nil || err2 != nil {
34+
t.Fatal("Unexpected error creating schemes")
35+
}
36+
37+
if scheme1 != scheme2 {
38+
t.Error("Expected same scheme instance for same options")
39+
}
40+
}
41+
42+
// TestNewSchemeWithTypesCachingDifferentOptions verifies different options yield different schemes
43+
func TestNewSchemeWithTypesCachingDifferentOptions(t *testing.T) {
44+
opts1 := ClientOptions{
45+
IncludeNonAdminTypes: true,
46+
IncludeVeleroTypes: true,
47+
}
48+
49+
opts2 := ClientOptions{
50+
IncludeNonAdminTypes: true,
51+
IncludeVeleroTypes: false,
52+
}
53+
54+
scheme1, err1 := NewSchemeWithTypes(opts1)
55+
scheme2, err2 := NewSchemeWithTypes(opts2)
56+
57+
if err1 != nil || err2 != nil {
58+
t.Fatal("Unexpected error creating schemes")
59+
}
60+
61+
if scheme1 == scheme2 {
62+
t.Error("Expected different scheme instances for different options")
63+
}
64+
}
65+
66+
// TestNewSchemeWithTypes verifies that the function creates schemes with correct types
67+
func TestNewSchemeWithTypes(t *testing.T) {
68+
tests := []struct {
69+
name string
70+
opts ClientOptions
71+
}{
72+
{
73+
name: "NonAdmin types only",
74+
opts: ClientOptions{
75+
IncludeNonAdminTypes: true,
76+
},
77+
},
78+
{
79+
name: "Velero types only",
80+
opts: ClientOptions{
81+
IncludeVeleroTypes: true,
82+
},
83+
},
84+
{
85+
name: "Core types only",
86+
opts: ClientOptions{
87+
IncludeCoreTypes: true,
88+
},
89+
},
90+
{
91+
name: "All types",
92+
opts: ClientOptions{
93+
IncludeNonAdminTypes: true,
94+
IncludeVeleroTypes: true,
95+
IncludeCoreTypes: true,
96+
},
97+
},
98+
{
99+
name: "No types",
100+
opts: ClientOptions{},
101+
},
102+
}
103+
104+
for _, tt := range tests {
105+
t.Run(tt.name, func(t *testing.T) {
106+
scheme, err := NewSchemeWithTypes(tt.opts)
107+
if err != nil {
108+
t.Errorf("NewSchemeWithTypes() unexpected error: %v", err)
109+
}
110+
if scheme == nil {
111+
t.Error("NewSchemeWithTypes() returned nil scheme")
112+
}
113+
})
114+
}
115+
}

0 commit comments

Comments
 (0)