github.com/zorawar87/trillian@v1.2.1/quota/cacheqm/cache.go (about) 1 // Copyright 2017 Google Inc. All Rights Reserved. 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 cacheqm contains a caching quota.Manager implementation. 16 package cacheqm 17 18 import ( 19 "context" 20 "fmt" 21 "sort" 22 "sync" 23 "time" 24 25 "github.com/golang/glog" 26 "github.com/google/trillian/quota" 27 ) 28 29 const ( 30 // DefaultMinBatchSize is the suggested default for minBatchSize. 31 DefaultMinBatchSize = 100 32 33 // DefaultMaxCacheEntries is the suggested default for maxEntries. 34 DefaultMaxCacheEntries = 1000 35 ) 36 37 // now is used in place of time.Now to allow tests to take control of time. 38 var now = time.Now 39 40 type manager struct { 41 qm quota.Manager 42 minBatchSize, maxEntries int 43 44 // mu guards cache 45 mu sync.Mutex 46 cache map[quota.Spec]*bucket 47 48 // evictWg tracks evict() goroutines. 49 evictWg sync.WaitGroup 50 } 51 52 type bucket struct { 53 tokens int 54 lastModified time.Time 55 } 56 57 // NewCachedManager wraps a quota.Manager with an implementation that caches tokens locally. 58 // 59 // minBatchSize determines the minimum number of tokens requested from qm for each GetTokens() 60 // request. 61 // 62 // maxEntries determines the maximum number of cache entries, apart from global quotas. The oldest 63 // entries are evicted as necessary, their tokens replenished via PutTokens() to avoid excessive 64 // leakage. 65 func NewCachedManager(qm quota.Manager, minBatchSize, maxEntries int) (quota.Manager, error) { 66 switch { 67 case minBatchSize <= 0: 68 return nil, fmt.Errorf("invalid minBatchSize: %v", minBatchSize) 69 case maxEntries <= 0: 70 return nil, fmt.Errorf("invalid maxEntries: %v", minBatchSize) 71 } 72 return &manager{ 73 qm: qm, 74 minBatchSize: minBatchSize, 75 maxEntries: maxEntries, 76 cache: make(map[quota.Spec]*bucket), 77 }, nil 78 } 79 80 func (m *manager) PeekTokens(ctx context.Context, specs []quota.Spec) (map[quota.Spec]int, error) { 81 return m.qm.PeekTokens(ctx, specs) 82 } 83 84 func (m *manager) PutTokens(ctx context.Context, numTokens int, specs []quota.Spec) error { 85 return m.qm.PutTokens(ctx, numTokens, specs) 86 } 87 88 func (m *manager) ResetQuota(ctx context.Context, specs []quota.Spec) error { 89 return m.qm.ResetQuota(ctx, specs) 90 } 91 92 func (m *manager) GetTokens(ctx context.Context, numTokens int, specs []quota.Spec) error { 93 m.mu.Lock() 94 defer m.mu.Unlock() 95 96 // Verify which buckets need more tokens, if any 97 specsToRefill := []quota.Spec{} 98 for _, spec := range specs { 99 bucket, ok := m.cache[spec] 100 if !ok || bucket.tokens < numTokens { 101 specsToRefill = append(specsToRefill, spec) 102 } 103 } 104 105 // Request the required number of tokens and add them to buckets 106 if len(specsToRefill) != 0 { 107 defer func() { 108 // Do not hold GetTokens on eviction, it won't change the result. 109 m.evictWg.Add(1) 110 go func() { 111 m.evict(ctx) 112 m.evictWg.Done() 113 }() 114 }() 115 116 // A more accurate count would be numTokens+m.minBatchSize-bucket.tokens, but that might 117 // force us to make a GetTokens call for each spec. A single call is likely to be more 118 // efficient. 119 tokens := numTokens + m.minBatchSize 120 if err := m.qm.GetTokens(ctx, tokens, specsToRefill); err != nil { 121 return err 122 } 123 for _, spec := range specsToRefill { 124 b, ok := m.cache[spec] 125 if !ok { 126 b = &bucket{} 127 m.cache[spec] = b 128 } 129 b.tokens += tokens 130 } 131 } 132 133 // Subtract tokens from cache 134 lastModified := now() 135 for _, spec := range specs { 136 bucket, ok := m.cache[spec] 137 // Sanity check 138 if !ok || bucket.tokens < 0 || bucket.tokens < numTokens { 139 glog.Errorf("Bucket invariants failed for spec %+v: ok = %v, bucket = %+v", spec, ok, bucket) 140 return nil // Something is wrong with the implementation, let requests go through. 141 } 142 bucket.tokens -= numTokens 143 bucket.lastModified = lastModified 144 } 145 return nil 146 } 147 148 func (m *manager) evict(ctx context.Context) { 149 m.mu.Lock() 150 // m.mu is explicitly unlocked, so we don't have to hold it while we wait for goroutines to 151 // complete. 152 153 if len(m.cache) <= m.maxEntries { 154 m.mu.Unlock() 155 return 156 } 157 158 // Find and evict the oldest entries. To avoid excessive token leakage, let's try and 159 // replenish the tokens held for the evicted entries. 160 var buckets bucketsByTime = make([]specBucket, 0, len(m.cache)) 161 for spec, b := range m.cache { 162 if spec.Group != quota.Global { 163 buckets = append(buckets, specBucket{bucket: b, spec: spec}) 164 } 165 } 166 sort.Sort(buckets) 167 168 wg := sync.WaitGroup{} 169 evicts := len(m.cache) - m.maxEntries 170 for i := 0; i < evicts; i++ { 171 b := buckets[i] 172 glog.V(1).Infof("Too many tokens cached, returning least recently used (%v tokens for %+v)", b.tokens, b.spec) 173 delete(m.cache, b.spec) 174 175 // goroutines must not access the cache, the lock is released before they complete. 176 wg.Add(1) 177 go func() { 178 if err := m.qm.PutTokens(ctx, b.tokens, []quota.Spec{b.spec}); err != nil { 179 glog.Warningf("Error replenishing tokens from evicted bucket (spec = %+v, bucket = %+v): %v", b.spec, b.bucket, err) 180 } 181 wg.Done() 182 }() 183 } 184 185 m.mu.Unlock() 186 wg.Wait() 187 } 188 189 // wait waits for spawned goroutines to complete. Used by eviction tests. 190 func (m *manager) wait() { 191 m.evictWg.Wait() 192 } 193 194 // specBucket is a bucket with the corresponding spec. 195 type specBucket struct { 196 *bucket 197 spec quota.Spec 198 } 199 200 // bucketsByTime is a sortable slice of specBuckets. 201 type bucketsByTime []specBucket 202 203 func (b bucketsByTime) Len() int { 204 return len(b) 205 } 206 207 func (b bucketsByTime) Less(i, j int) bool { 208 return b[i].lastModified.Before(b[j].lastModified) 209 } 210 211 func (b bucketsByTime) Swap(i, j int) { 212 b[i], b[j] = b[j], b[i] 213 }