119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
package wfe2
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/golang/groupcache/lru"
|
|
"github.com/jmhodges/clock"
|
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// AccountGetter represents the ability to get an account by ID - either from the SA
|
|
// or from a cache.
|
|
type AccountGetter interface {
|
|
GetRegistration(ctx context.Context, regID *sapb.RegistrationID, opts ...grpc.CallOption) (*corepb.Registration, error)
|
|
}
|
|
|
|
// accountCache is an implementation of AccountGetter that first tries a local
|
|
// in-memory cache, and if the account is not there, calls out to an underlying
|
|
// AccountGetter. It is safe for concurrent access so long as the underlying
|
|
// AccountGetter is.
|
|
type accountCache struct {
|
|
// Note: This must be a regular mutex, not an RWMutex, because cache.Get()
|
|
// actually mutates the lru.Cache (by updating the last-used info).
|
|
sync.Mutex
|
|
under AccountGetter
|
|
ttl time.Duration
|
|
cache *lru.Cache
|
|
clk clock.Clock
|
|
requests *prometheus.CounterVec
|
|
}
|
|
|
|
func NewAccountCache(
|
|
under AccountGetter,
|
|
maxEntries int,
|
|
ttl time.Duration,
|
|
clk clock.Clock,
|
|
stats prometheus.Registerer,
|
|
) *accountCache {
|
|
requestsCount := prometheus.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "cache_requests",
|
|
}, []string{"status"})
|
|
stats.MustRegister(requestsCount)
|
|
return &accountCache{
|
|
under: under,
|
|
ttl: ttl,
|
|
cache: lru.New(maxEntries),
|
|
clk: clk,
|
|
requests: requestsCount,
|
|
}
|
|
}
|
|
|
|
type accountEntry struct {
|
|
account *corepb.Registration
|
|
expires time.Time
|
|
}
|
|
|
|
func (ac *accountCache) GetRegistration(ctx context.Context, regID *sapb.RegistrationID, opts ...grpc.CallOption) (*corepb.Registration, error) {
|
|
ac.Lock()
|
|
val, ok := ac.cache.Get(regID.Id)
|
|
ac.Unlock()
|
|
if !ok {
|
|
ac.requests.WithLabelValues("miss").Inc()
|
|
return ac.queryAndStore(ctx, regID)
|
|
}
|
|
entry, ok := val.(accountEntry)
|
|
if !ok {
|
|
ac.requests.WithLabelValues("wrongtype").Inc()
|
|
return nil, fmt.Errorf("shouldn't happen: wrong type %T for cache entry", entry)
|
|
}
|
|
if entry.expires.Before(ac.clk.Now()) {
|
|
// Note: this has a slight TOCTOU issue but it's benign. If the entry for this account
|
|
// was expired off by some other goroutine and then a fresh one added, removing it a second
|
|
// time will just cause a slightly lower cache rate.
|
|
// We have to actively remove expired entries, because otherwise each retrieval counts as
|
|
// a "use" and they won't exit the cache on their own.
|
|
ac.Lock()
|
|
ac.cache.Remove(regID.Id)
|
|
ac.Unlock()
|
|
ac.requests.WithLabelValues("expired").Inc()
|
|
return ac.queryAndStore(ctx, regID)
|
|
}
|
|
if entry.account.Id != regID.Id {
|
|
ac.requests.WithLabelValues("wrong id from cache").Inc()
|
|
return nil, fmt.Errorf("shouldn't happen: wrong account ID. expected %d, got %d", regID.Id, entry.account.Id)
|
|
}
|
|
copied := new(corepb.Registration)
|
|
proto.Merge(copied, entry.account)
|
|
ac.requests.WithLabelValues("hit").Inc()
|
|
return copied, nil
|
|
}
|
|
|
|
func (ac *accountCache) queryAndStore(ctx context.Context, regID *sapb.RegistrationID) (*corepb.Registration, error) {
|
|
account, err := ac.under.GetRegistration(ctx, regID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if account.Id != regID.Id {
|
|
ac.requests.WithLabelValues("wrong id from SA").Inc()
|
|
return nil, fmt.Errorf("shouldn't happen: wrong account ID from backend. expected %d, got %d", regID.Id, account.Id)
|
|
}
|
|
// Make sure we have our own copy that no one has a pointer to.
|
|
copied := new(corepb.Registration)
|
|
proto.Merge(copied, account)
|
|
ac.Lock()
|
|
ac.cache.Add(regID.Id, accountEntry{
|
|
account: copied,
|
|
expires: ac.clk.Now().Add(ac.ttl),
|
|
})
|
|
ac.Unlock()
|
|
return account, nil
|
|
}
|