Skip to content

Commit 921ec94

Browse files
authored
Merge pull request #690 from a-hilaly/resolver/ttl
feat: Add TTL/LRU based caching to schema resolver
2 parents f07d106 + 1a8094b commit 921ec94

File tree

7 files changed

+400
-31
lines changed

7 files changed

+400
-31
lines changed

go.mod

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/gobuffalo/flect v1.0.2
99
github.com/google/cel-go v0.24.1
1010
github.com/google/go-cmp v0.6.0
11+
github.com/hashicorp/golang-lru/v2 v2.0.7
1112
github.com/onsi/ginkgo/v2 v2.20.0
1213
github.com/onsi/gomega v1.34.1
1314
github.com/prometheus/client_golang v1.19.1
@@ -18,6 +19,7 @@ require (
1819
golang.org/x/sync v0.12.0
1920
golang.org/x/time v0.3.0
2021
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7
22+
gopkg.in/yaml.v2 v2.4.0
2123
k8s.io/api v0.31.0
2224
k8s.io/apiextensions-apiserver v0.31.0
2325
k8s.io/apimachinery v0.31.0
@@ -54,7 +56,6 @@ require (
5456
github.com/golang/protobuf v1.5.4 // indirect
5557
github.com/google/gnostic-models v0.6.8 // indirect
5658
github.com/google/gofuzz v1.2.0 // indirect
57-
github.com/google/licenseclassifier/v2 v2.0.0 // indirect
5859
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
5960
github.com/google/uuid v1.6.0 // indirect
6061
github.com/hashicorp/hcl v1.0.0 // indirect
@@ -74,8 +75,6 @@ require (
7475
github.com/prometheus/client_model v0.6.1 // indirect
7576
github.com/prometheus/common v0.55.0 // indirect
7677
github.com/prometheus/procfs v0.15.1 // indirect
77-
github.com/sergi/go-diff v1.3.1 // indirect
78-
github.com/sirupsen/logrus v1.9.3 // indirect
7978
github.com/spf13/afero v1.9.5 // indirect
8079
github.com/spf13/cast v1.5.1 // indirect
8180
github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -84,10 +83,8 @@ require (
8483
github.com/stoewer/go-strcase v1.2.0 // indirect
8584
github.com/subosito/gotenv v1.4.2 // indirect
8685
github.com/x448/float16 v0.8.4 // indirect
87-
github.com/xlab/treeprint v1.2.0 // indirect
8886
go.uber.org/multierr v1.11.0 // indirect
8987
go.yaml.in/yaml/v2 v2.4.2 // indirect
90-
golang.org/x/mod v0.22.0 // indirect
9188
golang.org/x/net v0.38.0 // indirect
9289
golang.org/x/oauth2 v0.28.0 // indirect
9390
golang.org/x/sys v0.31.0 // indirect
@@ -100,13 +97,10 @@ require (
10097
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
10198
gopkg.in/inf.v0 v0.9.1 // indirect
10299
gopkg.in/ini.v1 v1.67.0 // indirect
103-
gopkg.in/yaml.v2 v2.4.0 // indirect
104100
gopkg.in/yaml.v3 v3.0.1 // indirect
105101
k8s.io/klog/v2 v2.130.1 // indirect
106102
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
107103
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
108104
)
109105

110-
tool (
111-
github.com/B1NARY-GR0UP/nwa
112-
)
106+
tool github.com/B1NARY-GR0UP/nwa

go.sum

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
4444
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
4545
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
4646
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
47-
github.com/awslabs/attribution-gen v0.0.4 h1:sG1PKMEn+XB/8e9Y38wox3+ucdioAI+mn5BkXz6faBI=
48-
github.com/awslabs/attribution-gen v0.0.4/go.mod h1:RFlz2/p2wAbXEFWe20sF4DufDfTZ133nX9x7ECuhZS4=
4947
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
5048
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
5149
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
@@ -156,8 +154,6 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
156154
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
157155
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
158156
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
159-
github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA=
160-
github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM=
161157
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
162158
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
163159
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -182,6 +178,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
182178
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
183179
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
184180
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
181+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
182+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
185183
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
186184
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
187185
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -244,11 +242,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
244242
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
245243
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
246244
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
247-
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
248-
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
249-
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
250-
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
251-
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
252245
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
253246
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
254247
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
@@ -281,8 +274,6 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
281274
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
282275
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
283276
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
284-
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
285-
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
286277
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
287278
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
288279
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -345,8 +336,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
345336
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
346337
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
347338
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
348-
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
349-
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
350339
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
351340
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
352341
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -439,7 +428,6 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
439428
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
440429
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
441430
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
442-
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
443431
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
444432
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
445433
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -613,7 +601,6 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h
613601
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
614602
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
615603
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
616-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
617604
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
618605
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
619606
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -624,7 +611,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
624611
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
625612
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
626613
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
627-
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
628614
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
629615
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
630616
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

pkg/graph/builder.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/kubernetes-sigs/kro/pkg/graph/emulator"
3838
"github.com/kubernetes-sigs/kro/pkg/graph/parser"
3939
"github.com/kubernetes-sigs/kro/pkg/graph/schema"
40+
schemaresolver "github.com/kubernetes-sigs/kro/pkg/graph/schema/resolver"
4041
"github.com/kubernetes-sigs/kro/pkg/graph/variable"
4142
"github.com/kubernetes-sigs/kro/pkg/metadata"
4243
"github.com/kubernetes-sigs/kro/pkg/simpleschema"
@@ -46,7 +47,7 @@ import (
4647
func NewBuilder(
4748
clientConfig *rest.Config,
4849
) (*Builder, error) {
49-
schemaResolver, dc, err := schema.NewCombinedResolver(clientConfig)
50+
schemaResolver, dc, err := schemaresolver.NewCombinedResolver(clientConfig)
5051
if err != nil {
5152
return nil, fmt.Errorf("failed to create schema resolver: %w", err)
5253
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2025 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resolver
16+
17+
import (
18+
"github.com/prometheus/client_golang/prometheus"
19+
)
20+
21+
var (
22+
cacheHitsTotal = prometheus.NewCounter(prometheus.CounterOpts{
23+
Name: "schema_resolver_cache_hits_total",
24+
Help: "Total number of schema resolver cache hits",
25+
})
26+
cacheMissesTotal = prometheus.NewCounter(prometheus.CounterOpts{
27+
Name: "schema_resolver_cache_misses_total",
28+
Help: "Total number of schema resolver cache misses",
29+
})
30+
31+
cacheSize = prometheus.NewGauge(prometheus.GaugeOpts{
32+
Name: "schema_resolver_cache_size",
33+
Help: "Current number of entries in the schema resolver cache",
34+
})
35+
36+
cacheEvictionsTotal = prometheus.NewCounter(prometheus.CounterOpts{
37+
Name: "schema_resolver_cache_evictions_total",
38+
Help: "Total number of entries evicted from the schema resolver cache",
39+
})
40+
41+
apiCallDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
42+
Name: "schema_resolver_api_call_duration_seconds",
43+
Help: "Duration of API calls to fetch schemas",
44+
Buckets: prometheus.ExponentialBuckets(0.01, 2, 10), // 10ms to ~10s
45+
})
46+
47+
singleflightDeduplicatedTotal = prometheus.NewCounter(prometheus.CounterOpts{
48+
Name: "schema_resolver_singleflight_deduplicated_total",
49+
Help: "Total number of requests that were deduplicated by singleflight",
50+
})
51+
52+
schemaResolutionErrorsTotal = prometheus.NewCounter(prometheus.CounterOpts{
53+
Name: "schema_resolver_errors_total",
54+
Help: "Total number of schema resolution errors",
55+
})
56+
)
57+
58+
// MustRegister registers the metrics with the given Prometheus registry
59+
func MustRegister(registry prometheus.Registerer) {
60+
registry.MustRegister(
61+
cacheHitsTotal,
62+
cacheMissesTotal,
63+
cacheSize,
64+
cacheEvictionsTotal,
65+
apiCallDuration,
66+
singleflightDeduplicatedTotal,
67+
schemaResolutionErrorsTotal,
68+
)
69+
}
70+
71+
// For now, register with the default registry
72+
//
73+
// TODO(a-hilaly): rework all kro custom metrics to use a custom registry, and
74+
// register them all somewhere central.
75+
func init() {
76+
MustRegister(prometheus.DefaultRegisterer)
77+
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package schema
15+
package resolver
1616

1717
import (
18+
"time"
19+
1820
"k8s.io/apiextensions-apiserver/pkg/generated/openapi"
1921
"k8s.io/apiserver/pkg/cel/openapi/resolver"
2022
"k8s.io/client-go/discovery"
@@ -23,7 +25,8 @@ import (
2325
)
2426

2527
// NewCombinedResolver creates a new schema resolver that can resolve both core and client types.
26-
func NewCombinedResolver(clientConfig *rest.Config) (resolver.SchemaResolver, *discovery.DiscoveryClient, error) {
28+
func NewCombinedResolver(clientConfig *rest.Config) (resolver.SchemaResolver, discovery.DiscoveryInterface, error) {
29+
// Create a regular discovery client first
2730
discoveryClient, err := discovery.NewDiscoveryClientForConfig(clientConfig)
2831
if err != nil {
2932
return nil, nil, err
@@ -37,15 +40,19 @@ func NewCombinedResolver(clientConfig *rest.Config) (resolver.SchemaResolver, *d
3740
Discovery: discoveryClient,
3841
}
3942

43+
cachedResolver := NewTTLCachedSchemaResolver(
44+
clientResolver,
45+
500, // maxSize: enough for 200 CRDs × 2-3 versions
46+
5*time.Minute, // TTL: balance between freshness and performance
47+
)
48+
4049
// CoreResolver is a resolver that uses the OpenAPI definitions to resolve
4150
// core types. It is used to resolve types that are known at compile time.
4251
coreResolver := resolver.NewDefinitionsSchemaResolver(
4352
openapi.GetOpenAPIDefinitions,
4453
scheme.Scheme,
4554
)
4655

47-
// Combine the two resolvers to create a single resolver that can resolve
48-
// both core and client types.
49-
combinedResolver := coreResolver.Combine(clientResolver)
56+
combinedResolver := coreResolver.Combine(cachedResolver)
5057
return combinedResolver, discoveryClient, nil
5158
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2025 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resolver
16+
17+
import (
18+
"time"
19+
20+
lru "github.com/hashicorp/golang-lru/v2/expirable"
21+
"golang.org/x/sync/singleflight"
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"k8s.io/apiserver/pkg/cel/openapi/resolver"
24+
"k8s.io/kube-openapi/pkg/validation/spec"
25+
)
26+
27+
// TTLCachedSchemaResolver caches schemas with LRU eviction and time-based expiration
28+
type TTLCachedSchemaResolver struct {
29+
// delegate is the underlying resolver to fetch schemas from when not in cache
30+
delegate resolver.SchemaResolver
31+
32+
// cache with LRU + TTL
33+
cache *lru.LRU[schema.GroupVersionKind, *spec.Schema]
34+
35+
// Deduplicate concurrent requests for the same GVK
36+
sf singleflight.Group
37+
}
38+
39+
// NewTTLCachedSchemaResolver creates a new TTLCachedSchemaResolver with LRU+TTL caching
40+
func NewTTLCachedSchemaResolver(
41+
delegate resolver.SchemaResolver,
42+
maxSize int,
43+
ttl time.Duration,
44+
) *TTLCachedSchemaResolver {
45+
// Create LRU cache with TTL and eviction callback for metrics
46+
cache := lru.NewLRU(
47+
maxSize,
48+
func(key schema.GroupVersionKind, value *spec.Schema) {
49+
cacheEvictionsTotal.Inc()
50+
},
51+
ttl,
52+
)
53+
54+
return &TTLCachedSchemaResolver{
55+
delegate: delegate,
56+
cache: cache,
57+
}
58+
}
59+
60+
// ResolveSchema resolves the schema for the given GroupVersionKind, using the cache if possible.
61+
// If multiple concurrent requests for the same GVK are made, they will be deduplicated.
62+
func (c *TTLCachedSchemaResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) {
63+
// Check cache first
64+
if schema, ok := c.cache.Get(gvk); ok {
65+
cacheHitsTotal.Inc()
66+
return schema, nil
67+
}
68+
69+
cacheMissesTotal.Inc()
70+
71+
// Use singleflight to ensure only one API call per GVK
72+
key := gvk.String()
73+
result, err, shared := c.sf.Do(key, func() (interface{}, error) {
74+
// Double-check cache inside singleflight (another goroutine might have populated it)
75+
if schema, ok := c.cache.Get(gvk); ok {
76+
return schema, nil
77+
}
78+
79+
// Actually fetch from delegate
80+
start := time.Now()
81+
schema, err := c.delegate.ResolveSchema(gvk)
82+
apiCallDuration.Observe(time.Since(start).Seconds())
83+
if err != nil {
84+
schemaResolutionErrorsTotal.Inc()
85+
return nil, err
86+
}
87+
88+
// Store in cache (LRU handles eviction automatically)
89+
c.cache.Add(gvk, schema)
90+
cacheSize.Set(float64(c.cache.Len()))
91+
92+
return schema, nil
93+
})
94+
95+
if shared {
96+
singleflightDeduplicatedTotal.Inc()
97+
}
98+
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return result.(*spec.Schema), nil
104+
}

0 commit comments

Comments
 (0)