...

Source file src/github.com/letsencrypt/boulder/wfe2/cache.go

Documentation: github.com/letsencrypt/boulder/wfe2

     1  package wfe2
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/golang/groupcache/lru"
    10  	"github.com/jmhodges/clock"
    11  	corepb "github.com/letsencrypt/boulder/core/proto"
    12  	sapb "github.com/letsencrypt/boulder/sa/proto"
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	"google.golang.org/grpc"
    15  	"google.golang.org/protobuf/proto"
    16  )
    17  
    18  // AccountGetter represents the ability to get an account by ID - either from the SA
    19  // or from a cache.
    20  type AccountGetter interface {
    21  	GetRegistration(ctx context.Context, regID *sapb.RegistrationID, opts ...grpc.CallOption) (*corepb.Registration, error)
    22  }
    23  
    24  // accountCache is an implementation of AccountGetter that first tries a local
    25  // in-memory cache, and if the account is not there, calls out to an underlying
    26  // AccountGetter. It is safe for concurrent access so long as the underlying
    27  // AccountGetter is.
    28  type accountCache struct {
    29  	// Note: This must be a regular mutex, not an RWMutex, because cache.Get()
    30  	// actually mutates the lru.Cache (by updating the last-used info).
    31  	sync.Mutex
    32  	under    AccountGetter
    33  	ttl      time.Duration
    34  	cache    *lru.Cache
    35  	clk      clock.Clock
    36  	requests *prometheus.CounterVec
    37  }
    38  
    39  func NewAccountCache(
    40  	under AccountGetter,
    41  	maxEntries int,
    42  	ttl time.Duration,
    43  	clk clock.Clock,
    44  	stats prometheus.Registerer,
    45  ) *accountCache {
    46  	requestsCount := prometheus.NewCounterVec(prometheus.CounterOpts{
    47  		Name: "cache_requests",
    48  	}, []string{"status"})
    49  	stats.MustRegister(requestsCount)
    50  	return &accountCache{
    51  		under:    under,
    52  		ttl:      ttl,
    53  		cache:    lru.New(maxEntries),
    54  		clk:      clk,
    55  		requests: requestsCount,
    56  	}
    57  }
    58  
    59  type accountEntry struct {
    60  	account *corepb.Registration
    61  	expires time.Time
    62  }
    63  
    64  func (ac *accountCache) GetRegistration(ctx context.Context, regID *sapb.RegistrationID, opts ...grpc.CallOption) (*corepb.Registration, error) {
    65  	ac.Lock()
    66  	val, ok := ac.cache.Get(regID.Id)
    67  	ac.Unlock()
    68  	if !ok {
    69  		ac.requests.WithLabelValues("miss").Inc()
    70  		return ac.queryAndStore(ctx, regID)
    71  	}
    72  	entry, ok := val.(accountEntry)
    73  	if !ok {
    74  		ac.requests.WithLabelValues("wrongtype").Inc()
    75  		return nil, fmt.Errorf("shouldn't happen: wrong type %T for cache entry", entry)
    76  	}
    77  	if entry.expires.Before(ac.clk.Now()) {
    78  		// Note: this has a slight TOCTOU issue but it's benign. If the entry for this account
    79  		// was expired off by some other goroutine and then a fresh one added, removing it a second
    80  		// time will just cause a slightly lower cache rate.
    81  		// We have to actively remove expired entries, because otherwise each retrieval counts as
    82  		// a "use" and they won't exit the cache on their own.
    83  		ac.Lock()
    84  		ac.cache.Remove(regID.Id)
    85  		ac.Unlock()
    86  		ac.requests.WithLabelValues("expired").Inc()
    87  		return ac.queryAndStore(ctx, regID)
    88  	}
    89  	if entry.account.Id != regID.Id {
    90  		ac.requests.WithLabelValues("wrong id from cache").Inc()
    91  		return nil, fmt.Errorf("shouldn't happen: wrong account ID. expected %d, got %d", regID.Id, entry.account.Id)
    92  	}
    93  	copied := new(corepb.Registration)
    94  	proto.Merge(copied, entry.account)
    95  	ac.requests.WithLabelValues("hit").Inc()
    96  	return copied, nil
    97  }
    98  
    99  func (ac *accountCache) queryAndStore(ctx context.Context, regID *sapb.RegistrationID) (*corepb.Registration, error) {
   100  	account, err := ac.under.GetRegistration(ctx, regID)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	if account.Id != regID.Id {
   105  		ac.requests.WithLabelValues("wrong id from SA").Inc()
   106  		return nil, fmt.Errorf("shouldn't happen: wrong account ID from backend. expected %d, got %d", regID.Id, account.Id)
   107  	}
   108  	// Make sure we have our own copy that no one has a pointer to.
   109  	copied := new(corepb.Registration)
   110  	proto.Merge(copied, account)
   111  	ac.Lock()
   112  	ac.cache.Add(regID.Id, accountEntry{
   113  		account: copied,
   114  		expires: ac.clk.Now().Add(ac.ttl),
   115  	})
   116  	ac.Unlock()
   117  	return account, nil
   118  }
   119  

View as plain text