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