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  }