Skip to content

Commit 36142e4

Browse files
authored
Implement caching for download of virtual machine images (vmware-tanzu#47)
This patch introduces an in-memory cache to store and manage downloaded OVF for virtual machine images. It also cleans up stale cached items after a specified time (currently set to 30 minutes). This avoids unnecessary requests to vCenter when the same content library is associated with multiple namespaces within a cluster and speeds up vm image reconciliation time. Testing Done: - Internal kind-based e2e tests (both fast and full) passed - Manually deployed this patch in a WCP testbed (with Image-Registry FSS enabled) and verified the expected workflow by associating a content library to multiple namespaces simultaneously. Logs from VM-Operator (in verbose mode): ``` I0207 09:15:21.266601 1 vmprovider.go:248] vsphere "msg"="Cache item miss, downloading OVF from vCenter" "contentVersion"="2" "itemID"="732f3e46-4699-4b66-aa6c-2740b0bfe6ed" ... I0207 09:15:29.134953 1 vmprovider.go:264] vsphere "msg"="Cache item put" "contentVersion"="2" "itemID"="732f3e46-4699-4b66-aa6c-2740b0bfe6ed" "putResult"=1 I0207 09:15:29.135280 1 vmprovider.go:246] vsphere "msg"="Cache item hit, using cached OVF" "contentVersion"="2" "itemID"="732f3e46-4699-4b66-aa6c-2740b0bfe6ed" I0207 09:15:29.136055 1 vmprovider.go:246] vsphere "msg"="Cache item hit, using cached OVF" "contentVersion"="2" "itemID"="732f3e46-4699-4b66-aa6c-2740b0bfe6ed" ```
1 parent fe9331c commit 36142e4

18 files changed

+632
-96
lines changed

controllers/contentlibrary/clustercontentlibraryitem/clustercontentlibraryitem_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package clustercontentlibraryitem
@@ -260,7 +260,7 @@ func (r *Reconciler) syncImageContent(ctx goctx.Context,
260260
return nil
261261
}
262262

263-
err := r.VMProvider.SyncVirtualMachineImage(ctx, cclItem.Spec.UUID, cvmi)
263+
err := r.VMProvider.SyncVirtualMachineImage(ctx, cclItem, cvmi)
264264
if err != nil {
265265
conditions.MarkFalse(cvmi,
266266
vmopv1a1.VirtualMachineImageSyncedCondition,

controllers/contentlibrary/clustercontentlibraryitem/clustercontentlibraryitem_controller_intg_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package clustercontentlibraryitem_test
@@ -65,8 +65,8 @@ func cclItemReconcile() {
6565
ctx = suite.NewIntegrationTestContext()
6666
intgFakeVMProvider.Lock()
6767
defer intgFakeVMProvider.Unlock()
68-
intgFakeVMProvider.SyncVirtualMachineImageFn = func(ctx context.Context,
69-
itemID string, cvmiObj client.Object) error {
68+
intgFakeVMProvider.SyncVirtualMachineImageFn = func(
69+
_ context.Context, _, cvmiObj client.Object) error {
7070
// Change a random spec and status field to verify the provider function is called.
7171
cvmi := cvmiObj.(*vmopv1a1.ClusterVirtualMachineImage)
7272
cvmi.Spec.HardwareVersion = 123

controllers/contentlibrary/clustercontentlibraryitem/clustercontentlibraryitem_controller_unit_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package clustercontentlibraryitem_test
@@ -56,8 +56,8 @@ func unitTestsReconcile() {
5656
ctx.VMProvider,
5757
)
5858
fakeVMProvider = ctx.VMProvider.(*providerfake.VMProvider)
59-
fakeVMProvider.SyncVirtualMachineImageFn = func(_ context.Context, _ string,
60-
cvmiObj client.Object) error {
59+
fakeVMProvider.SyncVirtualMachineImageFn = func(
60+
_ context.Context, _, cvmiObj client.Object) error {
6161
cvmi := cvmiObj.(*vmopv1a1.ClusterVirtualMachineImage)
6262
// Change a random spec and status field to verify the provider function is called.
6363
cvmi.Spec.HardwareVersion = 123

controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package contentlibraryitem
@@ -255,10 +255,7 @@ func (r *Reconciler) syncImageContent(ctx goctx.Context,
255255
return nil
256256
}
257257

258-
// TODO: .
259-
// We should use cache here to avoid downloading from VC for every single VM image under every namespaces and
260-
// cluster VMI with the same item ID.
261-
err := r.VMProvider.SyncVirtualMachineImage(ctx, clItem.Spec.UUID, vmi)
258+
err := r.VMProvider.SyncVirtualMachineImage(ctx, clItem, vmi)
262259
if err != nil {
263260
conditions.MarkFalse(vmi,
264261
vmopv1alpha1.VirtualMachineImageSyncedCondition,

controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller_intg_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package contentlibraryitem_test
@@ -60,8 +60,8 @@ func clItemReconcile() {
6060

6161
intgFakeVMProvider.Lock()
6262
defer intgFakeVMProvider.Unlock()
63-
intgFakeVMProvider.SyncVirtualMachineImageFn = func(ctx context.Context,
64-
itemID string, vmiObj client.Object) error {
63+
intgFakeVMProvider.SyncVirtualMachineImageFn = func(
64+
_ context.Context, _, vmiObj client.Object) error {
6565
vmi := vmiObj.(*vmopv1a1.VirtualMachineImage)
6666
// Change a random spec and status field to verify the provider function is called.
6767
vmi.Spec.HardwareVersion = 123

controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller_unit_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package contentlibraryitem_test
@@ -55,8 +55,7 @@ func unitTestsReconcile() {
5555
ctx.VMProvider,
5656
)
5757
fakeVMProvider = ctx.VMProvider.(*providerfake.VMProvider)
58-
fakeVMProvider.SyncVirtualMachineImageFn = func(_ context.Context, _ string,
59-
vmi client.Object) error {
58+
fakeVMProvider.SyncVirtualMachineImageFn = func(_ context.Context, _, vmi client.Object) error {
6059
vmiObj := vmi.(*vmopv1a1.VirtualMachineImage)
6160
// Change a random spec and status field to verify the provider function is called.
6261
vmiObj.Spec.HardwareVersion = 123

controllers/contentlibrary/utils/test_utils.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022 VMware, Inc. All Rights Reserved.
1+
// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package utils
@@ -105,7 +105,7 @@ func GetServiceTypeLabels(labels map[string]string) map[string]string {
105105
}
106106

107107
func GetExpectedCVMIFrom(cclItem imgregv1a1.ClusterContentLibraryItem,
108-
providerFunc func(context.Context, string, crtlclient.Object) error) *vmopv1a1.ClusterVirtualMachineImage {
108+
providerFunc func(context.Context, crtlclient.Object, crtlclient.Object) error) *vmopv1a1.ClusterVirtualMachineImage {
109109

110110
cvmi := &vmopv1a1.ClusterVirtualMachineImage{
111111
ObjectMeta: metav1.ObjectMeta{
@@ -152,14 +152,14 @@ func GetExpectedCVMIFrom(cclItem imgregv1a1.ClusterContentLibraryItem,
152152
}
153153

154154
if providerFunc != nil {
155-
_ = providerFunc(nil, "", cvmi)
155+
_ = providerFunc(nil, nil, cvmi)
156156
}
157157

158158
return cvmi
159159
}
160160

161161
func GetExpectedVMIFrom(clItem imgregv1a1.ContentLibraryItem,
162-
providerFunc func(context.Context, string, crtlclient.Object) error) *vmopv1a1.VirtualMachineImage {
162+
providerFunc func(context.Context, crtlclient.Object, crtlclient.Object) error) *vmopv1a1.VirtualMachineImage {
163163

164164
vmi := &vmopv1a1.VirtualMachineImage{
165165
ObjectMeta: metav1.ObjectMeta{
@@ -206,7 +206,7 @@ func GetExpectedVMIFrom(clItem imgregv1a1.ContentLibraryItem,
206206
}
207207

208208
if providerFunc != nil {
209-
_ = providerFunc(nil, "", vmi)
209+
_ = providerFunc(nil, nil, vmi)
210210
}
211211

212212
return vmi

pkg/util/cache.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright (c) 2023 VMware, Inc. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package util
5+
6+
import (
7+
"sync"
8+
"time"
9+
)
10+
11+
// CacheItem wraps T so it has a LastUpdated field and can be used with Cache.
12+
type CacheItem[T any] struct {
13+
// Item is the cached item.
14+
Item T
15+
16+
// LastUpdated is the last time the item was updated.
17+
LastUpdated time.Time
18+
}
19+
20+
// Cache is a generic implementation of a cache that can be configured with a
21+
// maximum number of items and can evict items after a certain amount of time.
22+
type Cache[T any] struct {
23+
mu sync.Mutex
24+
items map[string]CacheItem[T]
25+
maxItems int
26+
done chan struct{}
27+
donce sync.Once
28+
29+
// expiredChan returns a channel on which the IDs of items that are expired
30+
// are sent. This channel is closed when the cache is closed, but only after
31+
// all, pending eviction notices are received.
32+
expiredChan chan string
33+
expiringNum sync.WaitGroup
34+
}
35+
36+
// NewCache initializes a new cache with the provided expiration options.
37+
func NewCache[T any](
38+
expireAfter, checkExpireInterval time.Duration,
39+
maxItems int) *Cache[T] {
40+
41+
c := &Cache[T]{
42+
expiredChan: make(chan string),
43+
done: make(chan struct{}),
44+
items: map[string]CacheItem[T]{},
45+
maxItems: maxItems,
46+
}
47+
48+
// Start a goroutine to expire items from the cache.
49+
go func() {
50+
ticker := time.NewTicker(checkExpireInterval)
51+
52+
// evictFn iterates over the cached items and evicts expired items
53+
evictFn := func() {
54+
c.mu.Lock()
55+
defer c.mu.Unlock()
56+
for k, v := range c.items {
57+
if time.Since(v.LastUpdated) > expireAfter {
58+
// Delete the item from the cache.
59+
delete(c.items, k)
60+
61+
// Notify subscribers that the item with the ID of k has
62+
// been evicted if the cache is not closed. Otherwise we
63+
// add one to the number of items being expired. This
64+
// ensures the channel cannot be closed until all, pending
65+
// notifications are sent.
66+
select {
67+
case <-c.done:
68+
default:
69+
c.expiringNum.Add(1)
70+
go func(k string) {
71+
c.expiredChan <- k
72+
c.expiringNum.Done()
73+
}(k)
74+
}
75+
}
76+
}
77+
}
78+
for {
79+
select {
80+
case <-c.done:
81+
ticker.Stop()
82+
return
83+
case <-ticker.C:
84+
evictFn()
85+
}
86+
}
87+
}()
88+
89+
return c
90+
}
91+
92+
// ExpiredChan returns a channel on which the IDs of items that are expired are
93+
// sent. This channel is closed when the cache is closed, but only after all,
94+
// pending eviction notices are received.
95+
func (c *Cache[T]) ExpiredChan() <-chan string {
96+
return c.expiredChan
97+
}
98+
99+
// Close shuts down the cache. It is safe to call this function more than once.
100+
func (c *Cache[T]) Close() {
101+
c.donce.Do(func() {
102+
close(c.done)
103+
104+
// Wait until all pending, expiry notifications are received until the
105+
// expired channel is closed.
106+
go func() {
107+
c.expiringNum.Wait()
108+
close(c.expiredChan)
109+
}()
110+
})
111+
}
112+
113+
// Delete removes the specified item from the cache.
114+
func (c *Cache[T]) Delete(id string) {
115+
c.mu.Lock()
116+
defer c.mu.Unlock()
117+
delete(c.items, id)
118+
}
119+
120+
// Get returns the cached item with the provided ID. The function isHit may be
121+
// optionally provided to further determine whether a cached item should hit
122+
// or miss.
123+
func (c *Cache[T]) Get(id string, isHit func(t T) bool) (T, bool) {
124+
c.mu.Lock()
125+
defer c.mu.Unlock()
126+
127+
var emptyT T
128+
129+
v, ok := c.items[id]
130+
if !ok {
131+
return emptyT, false
132+
}
133+
134+
if isHit != nil && !isHit(v.Item) {
135+
return emptyT, false
136+
}
137+
138+
return v.Item, true
139+
}
140+
141+
// CachePutResult describes the result of putting an item into the cache.
142+
type CachePutResult uint
143+
144+
const (
145+
// CachePutResultCreate indicates the item was created in the cache.
146+
CachePutResultCreate CachePutResult = iota + 1
147+
148+
// CachePutResultUpdate indicates an existing, cached item was updated.
149+
CachePutResultUpdate
150+
151+
// CachePutResultMaxItemsExceeded indicates the item was not stored because
152+
// the cache's maximum number of items would have been exceeded.
153+
CachePutResultMaxItemsExceeded
154+
)
155+
156+
// Put stores the provided item in the cache, updating existing items.
157+
func (c *Cache[T]) Put(id string, t T) CachePutResult {
158+
c.mu.Lock()
159+
defer c.mu.Unlock()
160+
161+
v, ok := c.items[id]
162+
163+
if ok {
164+
// Update existing item.
165+
v.Item = t
166+
v.LastUpdated = time.Now()
167+
c.items[id] = v
168+
return CachePutResultUpdate
169+
}
170+
171+
// Store the new item in the cache as long as the maximum number of items
172+
// was note exceeded.
173+
if len(c.items) < c.maxItems {
174+
// Store new item in the cache.
175+
c.items[id] = CacheItem[T]{
176+
Item: t,
177+
LastUpdated: time.Now(),
178+
}
179+
return CachePutResultCreate
180+
}
181+
182+
return CachePutResultMaxItemsExceeded
183+
}

0 commit comments

Comments
 (0)