github.com/mailgun/holster/v4@v4.20.0/collections/expire_cache.go (about) 1 /* 2 Copyright 2017 Mailgun Technologies Inc 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 package collections 17 18 import ( 19 "sync" 20 21 "github.com/mailgun/holster/v4/clock" 22 "github.com/mailgun/holster/v4/errors" 23 "github.com/mailgun/holster/v4/syncutil" 24 ) 25 26 type ExpireCacheStats struct { 27 Size int64 28 Miss int64 29 Hit int64 30 } 31 32 // ExpireCache is a cache which expires entries only after 2 conditions are met 33 // 1. The Specified TTL has expired 34 // 2. The item has been processed with ExpireCache.Each() 35 // 36 // This is an unbounded cache which guaranties each item in the cache 37 // has been processed before removal. This is different from a LRU 38 // cache, as the cache might decide an item needs to be removed 39 // (because we hit the cache limit) before the item has been processed. 40 // 41 // Every time an item is touched by `Get()` or `Add()` the duration is 42 // updated which ensures items in frequent use stay in the cache 43 // 44 // Processing can modify the item in the cache without updating the 45 // expiration time by using the `Update()` method 46 // 47 // The cache can also return statistics which can be used to graph track 48 // the size of the cache 49 // 50 // NOTE: Because this is an unbounded cache, the user MUST process the cache 51 // with `Each()` regularly! Else the cache items will never expire and the cache 52 // will eventually eat all the memory on the system 53 type ExpireCache struct { 54 cache map[interface{}]*expireRecord 55 mutex sync.Mutex 56 ttl clock.Duration 57 stats ExpireCacheStats 58 } 59 60 type expireRecord struct { 61 Value interface{} 62 ExpireAt clock.Time 63 } 64 65 // New creates a new ExpireCache. 66 func NewExpireCache(ttl clock.Duration) *ExpireCache { 67 return &ExpireCache{ 68 cache: make(map[interface{}]*expireRecord), 69 ttl: ttl, 70 } 71 } 72 73 // Retrieves a key's value from the cache 74 func (c *ExpireCache) Get(key interface{}) (interface{}, bool) { 75 c.mutex.Lock() 76 defer c.mutex.Unlock() 77 78 record, ok := c.cache[key] 79 if !ok { 80 c.stats.Miss++ 81 return nil, ok 82 } 83 84 // Since this was recently accessed, keep it in 85 // the cache by resetting the expire time 86 record.ExpireAt = clock.Now().UTC().Add(c.ttl) 87 88 c.stats.Hit++ 89 return record.Value, ok 90 } 91 92 // Put the key, value and TTL in the cache 93 func (c *ExpireCache) Add(key, value interface{}) { 94 c.mutex.Lock() 95 defer c.mutex.Unlock() 96 97 record := expireRecord{ 98 Value: value, 99 ExpireAt: clock.Now().UTC().Add(c.ttl), 100 } 101 // Add the record to the cache 102 c.cache[key] = &record 103 } 104 105 // Update the value in the cache without updating the TTL 106 func (c *ExpireCache) Update(key, value interface{}) error { 107 c.mutex.Lock() 108 defer c.mutex.Unlock() 109 110 record, ok := c.cache[key] 111 if !ok { 112 return errors.Errorf("ExpoireCache() - No record found for '%+v'", key) 113 } 114 record.Value = value 115 return nil 116 } 117 118 // Get a list of keys at this point in time 119 func (c *ExpireCache) Keys() (keys []interface{}) { 120 defer c.mutex.Unlock() 121 c.mutex.Lock() 122 123 for key := range c.cache { 124 keys = append(keys, key) 125 } 126 return 127 } 128 129 // Get the value without updating the expiration 130 func (c *ExpireCache) Peek(key interface{}) (value interface{}, ok bool) { 131 defer c.mutex.Unlock() 132 c.mutex.Lock() 133 134 if record, hit := c.cache[key]; hit { 135 return record.Value, true 136 } 137 return nil, false 138 } 139 140 // Processes each item in the cache in a thread safe way, such that the cache can be in use 141 // while processing items in the cache 142 func (c *ExpireCache) Each(concurrent int, callBack func(key interface{}, value interface{}) error) []error { 143 fanOut := syncutil.NewFanOut(concurrent) 144 keys := c.Keys() 145 146 for _, key := range keys { 147 fanOut.Run(func(key interface{}) error { 148 c.mutex.Lock() 149 record, ok := c.cache[key] 150 c.mutex.Unlock() 151 if !ok { 152 return errors.Errorf("Each() - key '%+v' disapeared "+ 153 "from cache during iteration", key) 154 } 155 156 err := callBack(key, record.Value) 157 if err != nil { 158 return err 159 } 160 161 c.mutex.Lock() 162 if record.ExpireAt.Before(clock.Now().UTC()) { 163 delete(c.cache, key) 164 } 165 c.mutex.Unlock() 166 return nil 167 }, key) 168 } 169 170 // Wait for all the routines to complete 171 errs := fanOut.Wait() 172 if errs != nil { 173 return errs 174 } 175 176 return nil 177 } 178 179 // Retrieve stats about the cache 180 func (c *ExpireCache) GetStats() ExpireCacheStats { 181 c.mutex.Lock() 182 c.stats.Size = int64(len(c.cache)) 183 defer func() { 184 c.stats = ExpireCacheStats{} 185 c.mutex.Unlock() 186 }() 187 return c.stats 188 } 189 190 // Returns the number of items in the cache. 191 func (c *ExpireCache) Size() int64 { 192 defer c.mutex.Unlock() 193 c.mutex.Lock() 194 return int64(len(c.cache)) 195 }