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