1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package cache 18 19import ( 20 "sync" 21 "time" 22 23 "k8s.io/apimachinery/pkg/util/clock" 24 "k8s.io/klog/v2" 25) 26 27// ExpirationCache implements the store interface 28// 1. All entries are automatically time stamped on insert 29// a. The key is computed based off the original item/keyFunc 30// b. The value inserted under that key is the timestamped item 31// 2. Expiration happens lazily on read based on the expiration policy 32// a. No item can be inserted into the store while we're expiring 33// *any* item in the cache. 34// 3. Time-stamps are stripped off unexpired entries before return 35// Note that the ExpirationCache is inherently slower than a normal 36// threadSafeStore because it takes a write lock every time it checks if 37// an item has expired. 38type ExpirationCache struct { 39 cacheStorage ThreadSafeStore 40 keyFunc KeyFunc 41 clock clock.Clock 42 expirationPolicy ExpirationPolicy 43 // expirationLock is a write lock used to guarantee that we don't clobber 44 // newly inserted objects because of a stale expiration timestamp comparison 45 expirationLock sync.Mutex 46} 47 48// ExpirationPolicy dictates when an object expires. Currently only abstracted out 49// so unittests don't rely on the system clock. 50type ExpirationPolicy interface { 51 IsExpired(obj *TimestampedEntry) bool 52} 53 54// TTLPolicy implements a ttl based ExpirationPolicy. 55type TTLPolicy struct { 56 // >0: Expire entries with an age > ttl 57 // <=0: Don't expire any entry 58 TTL time.Duration 59 60 // Clock used to calculate ttl expiration 61 Clock clock.Clock 62} 63 64// IsExpired returns true if the given object is older than the ttl, or it can't 65// determine its age. 66func (p *TTLPolicy) IsExpired(obj *TimestampedEntry) bool { 67 return p.TTL > 0 && p.Clock.Since(obj.Timestamp) > p.TTL 68} 69 70// TimestampedEntry is the only type allowed in a ExpirationCache. 71// Keep in mind that it is not safe to share timestamps between computers. 72// Behavior may be inconsistent if you get a timestamp from the API Server and 73// use it on the client machine as part of your ExpirationCache. 74type TimestampedEntry struct { 75 Obj interface{} 76 Timestamp time.Time 77 key string 78} 79 80// getTimestampedEntry returns the TimestampedEntry stored under the given key. 81func (c *ExpirationCache) getTimestampedEntry(key string) (*TimestampedEntry, bool) { 82 item, _ := c.cacheStorage.Get(key) 83 if tsEntry, ok := item.(*TimestampedEntry); ok { 84 return tsEntry, true 85 } 86 return nil, false 87} 88 89// getOrExpire retrieves the object from the TimestampedEntry if and only if it hasn't 90// already expired. It holds a write lock across deletion. 91func (c *ExpirationCache) getOrExpire(key string) (interface{}, bool) { 92 // Prevent all inserts from the time we deem an item as "expired" to when we 93 // delete it, so an un-expired item doesn't sneak in under the same key, just 94 // before the Delete. 95 c.expirationLock.Lock() 96 defer c.expirationLock.Unlock() 97 timestampedItem, exists := c.getTimestampedEntry(key) 98 if !exists { 99 return nil, false 100 } 101 if c.expirationPolicy.IsExpired(timestampedItem) { 102 klog.V(4).Infof("Entry %v: %+v has expired", key, timestampedItem.Obj) 103 c.cacheStorage.Delete(key) 104 return nil, false 105 } 106 return timestampedItem.Obj, true 107} 108 109// GetByKey returns the item stored under the key, or sets exists=false. 110func (c *ExpirationCache) GetByKey(key string) (interface{}, bool, error) { 111 obj, exists := c.getOrExpire(key) 112 return obj, exists, nil 113} 114 115// Get returns unexpired items. It purges the cache of expired items in the 116// process. 117func (c *ExpirationCache) Get(obj interface{}) (interface{}, bool, error) { 118 key, err := c.keyFunc(obj) 119 if err != nil { 120 return nil, false, KeyError{obj, err} 121 } 122 obj, exists := c.getOrExpire(key) 123 return obj, exists, nil 124} 125 126// List retrieves a list of unexpired items. It purges the cache of expired 127// items in the process. 128func (c *ExpirationCache) List() []interface{} { 129 items := c.cacheStorage.List() 130 131 list := make([]interface{}, 0, len(items)) 132 for _, item := range items { 133 key := item.(*TimestampedEntry).key 134 if obj, exists := c.getOrExpire(key); exists { 135 list = append(list, obj) 136 } 137 } 138 return list 139} 140 141// ListKeys returns a list of all keys in the expiration cache. 142func (c *ExpirationCache) ListKeys() []string { 143 return c.cacheStorage.ListKeys() 144} 145 146// Add timestamps an item and inserts it into the cache, overwriting entries 147// that might exist under the same key. 148func (c *ExpirationCache) Add(obj interface{}) error { 149 key, err := c.keyFunc(obj) 150 if err != nil { 151 return KeyError{obj, err} 152 } 153 c.expirationLock.Lock() 154 defer c.expirationLock.Unlock() 155 156 c.cacheStorage.Add(key, &TimestampedEntry{obj, c.clock.Now(), key}) 157 return nil 158} 159 160// Update has not been implemented yet for lack of a use case, so this method 161// simply calls `Add`. This effectively refreshes the timestamp. 162func (c *ExpirationCache) Update(obj interface{}) error { 163 return c.Add(obj) 164} 165 166// Delete removes an item from the cache. 167func (c *ExpirationCache) Delete(obj interface{}) error { 168 key, err := c.keyFunc(obj) 169 if err != nil { 170 return KeyError{obj, err} 171 } 172 c.expirationLock.Lock() 173 defer c.expirationLock.Unlock() 174 c.cacheStorage.Delete(key) 175 return nil 176} 177 178// Replace will convert all items in the given list to TimestampedEntries 179// before attempting the replace operation. The replace operation will 180// delete the contents of the ExpirationCache `c`. 181func (c *ExpirationCache) Replace(list []interface{}, resourceVersion string) error { 182 items := make(map[string]interface{}, len(list)) 183 ts := c.clock.Now() 184 for _, item := range list { 185 key, err := c.keyFunc(item) 186 if err != nil { 187 return KeyError{item, err} 188 } 189 items[key] = &TimestampedEntry{item, ts, key} 190 } 191 c.expirationLock.Lock() 192 defer c.expirationLock.Unlock() 193 c.cacheStorage.Replace(items, resourceVersion) 194 return nil 195} 196 197// Resync is a no-op for one of these 198func (c *ExpirationCache) Resync() error { 199 return nil 200} 201 202// NewTTLStore creates and returns a ExpirationCache with a TTLPolicy 203func NewTTLStore(keyFunc KeyFunc, ttl time.Duration) Store { 204 return NewExpirationStore(keyFunc, &TTLPolicy{ttl, clock.RealClock{}}) 205} 206 207// NewExpirationStore creates and returns a ExpirationCache for a given policy 208func NewExpirationStore(keyFunc KeyFunc, expirationPolicy ExpirationPolicy) Store { 209 return &ExpirationCache{ 210 cacheStorage: NewThreadSafeStore(Indexers{}, Indices{}), 211 keyFunc: keyFunc, 212 clock: clock.RealClock{}, 213 expirationPolicy: expirationPolicy, 214 } 215} 216