mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1018 lines
38 KiB
1018 lines
38 KiB
// Package cache provides caching features for data from a Consul server. |
|
// |
|
// While this is similar in some ways to the "agent/ae" package, a key |
|
// difference is that with anti-entropy, the agent is the authoritative |
|
// source so it resolves differences the server may have. With caching (this |
|
// package), the server is the authoritative source and we do our best to |
|
// balance performance and correctness, depending on the type of data being |
|
// requested. |
|
// |
|
// The types of data that can be cached is configurable via the Type interface. |
|
// This allows specialized behavior for certain types of data. Each type of |
|
// Consul data (CA roots, leaf certs, intentions, KV, catalog, etc.) will |
|
// have to be manually implemented. This usually is not much work, see |
|
// the "agent/cache-types" package. |
|
package cache |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"io" |
|
"strconv" |
|
"sync" |
|
"sync/atomic" |
|
"time" |
|
|
|
"github.com/armon/go-metrics" |
|
"github.com/armon/go-metrics/prometheus" |
|
"github.com/hashicorp/go-hclog" |
|
"golang.org/x/time/rate" |
|
|
|
"github.com/hashicorp/consul/acl" |
|
"github.com/hashicorp/consul/lib" |
|
"github.com/hashicorp/consul/lib/ttlcache" |
|
) |
|
|
|
// TODO(kit): remove the namespace from these once the metrics themselves change |
|
var Gauges = []prometheus.GaugeDefinition{ |
|
{ |
|
Name: []string{"consul", "cache", "entries_count"}, |
|
Help: "Deprecated - please use cache_entries_count instead.", |
|
}, |
|
{ |
|
Name: []string{"cache", "entries_count"}, |
|
Help: "Represents the number of entries in this cache.", |
|
}, |
|
} |
|
|
|
// TODO(kit): remove the namespace from these once the metrics themselves change |
|
var Counters = []prometheus.CounterDefinition{ |
|
{ |
|
Name: []string{"consul", "cache", "bypass"}, |
|
Help: "Deprecated - please use cache_bypass instead.", |
|
}, |
|
{ |
|
Name: []string{"cache", "bypass"}, |
|
Help: "Counts how many times a request bypassed the cache because no cache-key was provided.", |
|
}, |
|
{ |
|
Name: []string{"consul", "cache", "fetch_success"}, |
|
Help: "Deprecated - please use cache_fetch_success instead.", |
|
}, |
|
{ |
|
Name: []string{"cache", "fetch_success"}, |
|
Help: "Counts the number of successful fetches by the cache.", |
|
}, |
|
{ |
|
Name: []string{"consul", "cache", "fetch_error"}, |
|
Help: "Deprecated - please use cache_fetch_error instead.", |
|
}, |
|
{ |
|
Name: []string{"cache", "fetch_error"}, |
|
Help: "Counts the number of failed fetches by the cache.", |
|
}, |
|
{ |
|
Name: []string{"consul", "cache", "evict_expired"}, |
|
Help: "Deprecated - please use cache_evict_expired instead.", |
|
}, |
|
{ |
|
Name: []string{"cache", "evict_expired"}, |
|
Help: "Counts the number of expired entries that are evicted.", |
|
}, |
|
} |
|
|
|
// Constants related to refresh backoff. We probably don't ever need to |
|
// make these configurable knobs since they primarily exist to lower load. |
|
const ( |
|
DefaultCacheRefreshBackoffMin = 3 // 3 attempts before backing off |
|
DefaultCacheRefreshMaxWait = 1 * time.Minute // maximum backoff wait time |
|
|
|
// The following constants are default values for the cache entry |
|
// rate limiter settings. |
|
|
|
// DefaultEntryFetchRate is the default rate at which cache entries can |
|
// be fetch. This defaults to not being unlimited |
|
DefaultEntryFetchRate = rate.Inf |
|
|
|
// DefaultEntryFetchMaxBurst is the number of cache entry fetches that can |
|
// occur in a burst. |
|
DefaultEntryFetchMaxBurst = 2 |
|
) |
|
|
|
// Cache is a agent-local cache of Consul data. Create a Cache using the |
|
// New function. A zero-value Cache is not ready for usage and will result |
|
// in a panic. |
|
// |
|
// The types of data to be cached must be registered via RegisterType. Then, |
|
// calls to Get specify the type and a Request implementation. The |
|
// implementation of Request is usually done directly on the standard RPC |
|
// struct in agent/structs. This API makes cache usage a mostly drop-in |
|
// replacement for non-cached RPC calls. |
|
// |
|
// The cache is partitioned by ACL and datacenter/peer. This allows the cache |
|
// to be safe for multi-DC queries and for queries where the data is modified |
|
// due to ACLs all without the cache having to have any clever logic, at |
|
// the slight expense of a less perfect cache. |
|
// |
|
// The Cache exposes various metrics via go-metrics. Please view the source |
|
// searching for "metrics." to see the various metrics exposed. These can be |
|
// used to explore the performance of the cache. |
|
type Cache struct { |
|
// types stores the list of data types that the cache knows how to service. |
|
// These can be dynamically registered with RegisterType. |
|
typesLock sync.RWMutex |
|
types map[string]typeEntry |
|
|
|
// entries contains the actual cache data. Access to entries and |
|
// entriesExpiryHeap must be protected by entriesLock. |
|
// |
|
// entriesExpiryHeap is a heap of *cacheEntry values ordered by |
|
// expiry, with the soonest to expire being first in the list (index 0). |
|
// |
|
// NOTE(mitchellh): The entry map key is currently a string in the format |
|
// of "<DC>/<ACL token>/<Request key>" in order to properly partition |
|
// requests to different datacenters and ACL tokens. This format has some |
|
// big drawbacks: we can't evict by datacenter, ACL token, etc. For an |
|
// initial implementation this works and the tests are agnostic to the |
|
// internal storage format so changing this should be possible safely. |
|
entriesLock sync.RWMutex |
|
entries map[string]cacheEntry |
|
entriesExpiryHeap *ttlcache.ExpiryHeap |
|
lastGoroutineID uint64 |
|
|
|
// stopped is used as an atomic flag to signal that the Cache has been |
|
// discarded so background fetches and expiry processing should stop. |
|
stopped uint32 |
|
// stopCh is closed when Close is called |
|
stopCh chan struct{} |
|
// options includes a per Cache Rate limiter specification to avoid performing too many queries |
|
options Options |
|
rateLimitContext context.Context |
|
rateLimitCancel context.CancelFunc |
|
} |
|
|
|
// typeEntry is a single type that is registered with a Cache. |
|
type typeEntry struct { |
|
// Name that was used to register the Type |
|
Name string |
|
Type Type |
|
Opts *RegisterOptions |
|
} |
|
|
|
// ResultMeta is returned from Get calls along with the value and can be used |
|
// to expose information about the cache status for debugging or testing. |
|
type ResultMeta struct { |
|
// Hit indicates whether or not the request was a cache hit |
|
Hit bool |
|
|
|
// Age identifies how "stale" the result is. It's semantics differ based on |
|
// whether or not the cache type performs background refresh or not as defined |
|
// in https://www.consul.io/api/index.html#agent-caching. |
|
// |
|
// For background refresh types, Age is 0 unless the background blocking query |
|
// is currently in a failed state and so not keeping up with the server's |
|
// values. If it is non-zero it represents the time since the first failure to |
|
// connect during background refresh, and is reset after a background request |
|
// does manage to reconnect and either return successfully, or block for at |
|
// least the yamux keepalive timeout of 30 seconds (which indicates the |
|
// connection is OK but blocked as expected). |
|
// |
|
// For simple cache types, Age is the time since the result being returned was |
|
// fetched from the servers. |
|
Age time.Duration |
|
|
|
// Index is the internal ModifyIndex for the cache entry. Not all types |
|
// support blocking and all that do will likely have this in their result type |
|
// already but this allows generic code to reason about whether cache values |
|
// have changed. |
|
Index uint64 |
|
} |
|
|
|
// Options are options for the Cache. |
|
type Options struct { |
|
Logger hclog.Logger |
|
|
|
// EntryFetchMaxBurst max burst size of RateLimit for a single cache entry |
|
EntryFetchMaxBurst int |
|
// EntryFetchRate represents the max calls/sec for a single cache entry |
|
EntryFetchRate rate.Limit |
|
|
|
// CacheRefreshBackoffMin is the number of attempts to wait before backing off. |
|
// Mostly configurable just for testing. |
|
CacheRefreshBackoffMin uint |
|
// CacheRefreshMaxWait is the maximum backoff wait time. |
|
// Mostly configurable just for testing. |
|
CacheRefreshMaxWait time.Duration |
|
} |
|
|
|
// Equal return true if both options are equivalent |
|
func (o Options) Equal(other Options) bool { |
|
return o.EntryFetchMaxBurst == other.EntryFetchMaxBurst && o.EntryFetchRate == other.EntryFetchRate |
|
} |
|
|
|
// applyDefaultValuesOnOptions set default values on options and returned updated value |
|
func applyDefaultValuesOnOptions(options Options) Options { |
|
if options.EntryFetchRate == 0.0 { |
|
options.EntryFetchRate = DefaultEntryFetchRate |
|
} |
|
if options.EntryFetchMaxBurst == 0 { |
|
options.EntryFetchMaxBurst = DefaultEntryFetchMaxBurst |
|
} |
|
if options.CacheRefreshBackoffMin == 0 { |
|
options.CacheRefreshBackoffMin = DefaultCacheRefreshBackoffMin |
|
} |
|
if options.CacheRefreshMaxWait == 0 { |
|
options.CacheRefreshMaxWait = DefaultCacheRefreshMaxWait |
|
} |
|
if options.Logger == nil { |
|
options.Logger = hclog.New(nil) |
|
} |
|
return options |
|
} |
|
|
|
// New creates a new cache with the given RPC client and reasonable defaults. |
|
// Further settings can be tweaked on the returned value. |
|
func New(options Options) *Cache { |
|
options = applyDefaultValuesOnOptions(options) |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
c := &Cache{ |
|
types: make(map[string]typeEntry), |
|
entries: make(map[string]cacheEntry), |
|
entriesExpiryHeap: ttlcache.NewExpiryHeap(), |
|
stopCh: make(chan struct{}), |
|
options: options, |
|
rateLimitContext: ctx, |
|
rateLimitCancel: cancel, |
|
} |
|
|
|
// Start the expiry watcher |
|
go c.runExpiryLoop() |
|
|
|
return c |
|
} |
|
|
|
// RegisterOptions are options that can be associated with a type being |
|
// registered for the cache. This changes the behavior of the cache for |
|
// this type. |
|
type RegisterOptions struct { |
|
// LastGetTTL is the time that the values returned by this type remain |
|
// in the cache after the last get operation. If a value isn't accessed |
|
// within this duration, the value is purged from the cache and |
|
// background refreshing will cease. |
|
LastGetTTL time.Duration |
|
|
|
// Refresh configures whether the data is actively refreshed or if |
|
// the data is only refreshed on an explicit Get. The default (false) |
|
// is to only request data on explicit Get. |
|
Refresh bool |
|
|
|
// SupportsBlocking should be set to true if the type supports blocking queries. |
|
// Types that do not support blocking queries will not be able to use |
|
// background refresh nor will the cache attempt blocking fetches if the |
|
// client requests them with MinIndex. |
|
SupportsBlocking bool |
|
|
|
// RefreshTimer is the time to sleep between attempts to refresh data. |
|
// If this is zero, then data is refreshed immediately when a fetch |
|
// is returned. |
|
// |
|
// Using different values for RefreshTimer and QueryTimeout, various |
|
// "refresh" mechanisms can be implemented: |
|
// |
|
// * With a high timer duration and a low timeout, a timer-based |
|
// refresh can be set that minimizes load on the Consul servers. |
|
// |
|
// * With a low timer and high timeout duration, a blocking-query-based |
|
// refresh can be set so that changes in server data are recognized |
|
// within the cache very quickly. |
|
// |
|
RefreshTimer time.Duration |
|
|
|
// QueryTimeout is the default value for the maximum query time for a fetch |
|
// operation. It is set as FetchOptions.Timeout so that cache.Type |
|
// implementations can use it as the MaxQueryTime. |
|
QueryTimeout time.Duration |
|
} |
|
|
|
// RegisterType registers a cacheable type. |
|
// |
|
// This makes the type available for Get but does not automatically perform |
|
// any prefetching. In order to populate the cache, Get must be called. |
|
func (c *Cache) RegisterType(n string, typ Type) { |
|
opts := typ.RegisterOptions() |
|
if opts.LastGetTTL == 0 { |
|
opts.LastGetTTL = 72 * time.Hour // reasonable default is days |
|
} |
|
|
|
c.typesLock.Lock() |
|
defer c.typesLock.Unlock() |
|
c.types[n] = typeEntry{Name: n, Type: typ, Opts: &opts} |
|
} |
|
|
|
// ReloadOptions updates the cache with the new options |
|
// return true if Cache is updated, false if already up to date |
|
func (c *Cache) ReloadOptions(options Options) bool { |
|
options = applyDefaultValuesOnOptions(options) |
|
modified := !options.Equal(c.options) |
|
if modified { |
|
c.entriesLock.RLock() |
|
defer c.entriesLock.RUnlock() |
|
for _, entry := range c.entries { |
|
if c.options.EntryFetchRate != options.EntryFetchRate { |
|
entry.FetchRateLimiter.SetLimit(options.EntryFetchRate) |
|
} |
|
if c.options.EntryFetchMaxBurst != options.EntryFetchMaxBurst { |
|
entry.FetchRateLimiter.SetBurst(options.EntryFetchMaxBurst) |
|
} |
|
} |
|
c.options.EntryFetchRate = options.EntryFetchRate |
|
c.options.EntryFetchMaxBurst = options.EntryFetchMaxBurst |
|
} |
|
return modified |
|
} |
|
|
|
// Get loads the data for the given type and request. If data satisfying the |
|
// minimum index is present in the cache, it is returned immediately. Otherwise, |
|
// this will block until the data is available or the request timeout is |
|
// reached. |
|
// |
|
// Multiple Get calls for the same Request (matching CacheKey value) will |
|
// block on a single network request. |
|
// |
|
// The timeout specified by the Request will be the timeout on the cache |
|
// Get, and does not correspond to the timeout of any background data |
|
// fetching. If the timeout is reached before data satisfying the minimum |
|
// index is retrieved, the last known value (maybe nil) is returned. No |
|
// error is returned on timeout. This matches the behavior of Consul blocking |
|
// queries. |
|
func (c *Cache) Get(ctx context.Context, t string, r Request) (interface{}, ResultMeta, error) { |
|
c.typesLock.RLock() |
|
tEntry, ok := c.types[t] |
|
c.typesLock.RUnlock() |
|
if !ok { |
|
// Shouldn't happen given that we successfully fetched this at least |
|
// once. But be robust against panics. |
|
return nil, ResultMeta{}, fmt.Errorf("unknown type in cache: %s", t) |
|
} |
|
return c.getWithIndex(ctx, newGetOptions(tEntry, r)) |
|
} |
|
|
|
// getOptions contains the arguments for a Get request. It is used in place of |
|
// Request so that internal functions can modify Info without having to extract |
|
// it from the Request each time. |
|
type getOptions struct { |
|
// Fetch is a closure over tEntry.Type.Fetch which provides the original |
|
// Request from the caller. |
|
Fetch func(opts FetchOptions) (FetchResult, error) |
|
Info RequestInfo |
|
TypeEntry typeEntry |
|
} |
|
|
|
func newGetOptions(tEntry typeEntry, r Request) getOptions { |
|
return getOptions{ |
|
Fetch: func(opts FetchOptions) (FetchResult, error) { |
|
return tEntry.Type.Fetch(opts, r) |
|
}, |
|
Info: r.CacheInfo(), |
|
TypeEntry: tEntry, |
|
} |
|
} |
|
|
|
// getEntryLocked retrieves a cache entry and checks if it is ready to be |
|
// returned given the other parameters. It reads from entries and the caller |
|
// has to issue a read lock if necessary. |
|
func (c *Cache) getEntryLocked( |
|
tEntry typeEntry, |
|
key string, |
|
info RequestInfo, |
|
) (entryExists bool, entryValid bool, entry cacheEntry) { |
|
entry, ok := c.entries[key] |
|
if !entry.Valid { |
|
return ok, false, entry |
|
} |
|
|
|
// Check index is not specified or lower than value, or the type doesn't |
|
// support blocking. |
|
if tEntry.Opts.SupportsBlocking && info.MinIndex > 0 && info.MinIndex >= entry.Index { |
|
// MinIndex was given and matches or is higher than current value so we |
|
// ignore the cache and fallthrough to blocking on a new value below. |
|
return true, false, entry |
|
} |
|
|
|
// Check MaxAge is not exceeded if this is not a background refreshing type |
|
// and MaxAge was specified. |
|
if !tEntry.Opts.Refresh && info.MaxAge > 0 && entryExceedsMaxAge(info.MaxAge, entry) { |
|
return true, false, entry |
|
} |
|
|
|
// Check if re-validate is requested. If so the first time round the |
|
// loop is not a hit but subsequent ones should be treated normally. |
|
if !tEntry.Opts.Refresh && info.MustRevalidate { |
|
// It is important to note that this block ONLY applies when we are not |
|
// in indefinite refresh mode (where the underlying goroutine will |
|
// continue to re-query for data). |
|
// |
|
// In this mode goroutines have a 1:1 relationship to RPCs that get |
|
// executed, and importantly they DO NOT SLEEP after executing. |
|
// |
|
// This means that a running goroutine for this cache entry extremely |
|
// strongly implies that the RPC has not yet completed, which is why |
|
// this check works for the revalidation-avoidance optimization here. |
|
if entry.GoroutineID != 0 { |
|
// There is an active goroutine performing a blocking query for |
|
// this data, which has not returned. |
|
// |
|
// We can logically deduce that the contents of the cache are |
|
// actually current, and we can simply return this while leaving |
|
// the blocking query alone. |
|
return true, true, entry |
|
} |
|
return true, false, entry |
|
} |
|
|
|
return true, true, entry |
|
} |
|
|
|
func entryExceedsMaxAge(maxAge time.Duration, entry cacheEntry) bool { |
|
return !entry.FetchedAt.IsZero() && maxAge < time.Since(entry.FetchedAt) |
|
} |
|
|
|
// getWithIndex implements the main Get functionality but allows internal |
|
// callers (Watch) to manipulate the blocking index separately from the actual |
|
// request object. |
|
func (c *Cache) getWithIndex(ctx context.Context, r getOptions) (interface{}, ResultMeta, error) { |
|
if r.Info.Key == "" { |
|
metrics.IncrCounter([]string{"consul", "cache", "bypass"}, 1) |
|
metrics.IncrCounter([]string{"cache", "bypass"}, 1) |
|
|
|
// If no key is specified, then we do not cache this request. |
|
// Pass directly through to the backend. |
|
result, err := r.Fetch(FetchOptions{MinIndex: r.Info.MinIndex}) |
|
return result.Value, ResultMeta{}, err |
|
} |
|
|
|
key := makeEntryKey(r.TypeEntry.Name, r.Info.Datacenter, r.Info.PeerName, r.Info.Token, r.Info.Key) |
|
|
|
// First time through |
|
first := true |
|
|
|
// timeoutCh for watching our timeout |
|
var timeoutCh <-chan time.Time |
|
|
|
RETRY_GET: |
|
// Get the current value |
|
c.entriesLock.RLock() |
|
_, entryValid, entry := c.getEntryLocked(r.TypeEntry, key, r.Info) |
|
c.entriesLock.RUnlock() |
|
|
|
if entry.Expiry != nil { |
|
// The entry already exists in the TTL heap, touch it to keep it alive since |
|
// this Get is still interested in the value. Note that we used to only do |
|
// this in the `entryValid` block below but that means that a cache entry |
|
// will expire after it's TTL regardless of how many callers are waiting for |
|
// updates in this method in a couple of cases: |
|
// 1. If the agent is disconnected from servers for the TTL then the client |
|
// will be in backoff getting errors on each call to Get and since an |
|
// errored cache entry has Valid = false it won't be touching the TTL. |
|
// 2. If the value is just not changing then the client's current index |
|
// will be equal to the entry index and entryValid will be false. This |
|
// is a common case! |
|
// |
|
// But regardless of the state of the entry, assuming it's already in the |
|
// TTL heap, we should touch it every time around here since this caller at |
|
// least still cares about the value! |
|
c.entriesLock.Lock() |
|
c.entriesExpiryHeap.Update(entry.Expiry.Index(), r.TypeEntry.Opts.LastGetTTL) |
|
c.entriesLock.Unlock() |
|
} |
|
|
|
if entryValid { |
|
meta := ResultMeta{Index: entry.Index} |
|
if first { |
|
metrics.IncrCounter([]string{"consul", "cache", r.TypeEntry.Name, "hit"}, 1) |
|
metrics.IncrCounter([]string{"cache", r.TypeEntry.Name, "hit"}, 1) |
|
meta.Hit = true |
|
} |
|
|
|
// If refresh is enabled, calculate age based on whether the background |
|
// routine is still connected. |
|
if r.TypeEntry.Opts.Refresh { |
|
meta.Age = time.Duration(0) |
|
if !entry.RefreshLostContact.IsZero() { |
|
meta.Age = time.Since(entry.RefreshLostContact) |
|
} |
|
} else { |
|
// For non-background refresh types, the age is just how long since we |
|
// fetched it last. |
|
if !entry.FetchedAt.IsZero() { |
|
meta.Age = time.Since(entry.FetchedAt) |
|
} |
|
} |
|
|
|
// We purposely do not return an error here since the cache only works with |
|
// fetching values that either have a value or have an error, but not both. |
|
// The Error may be non-nil in the entry in the case that an error has |
|
// occurred _since_ the last good value, but we still want to return the |
|
// good value to clients that are not requesting a specific version. The |
|
// effect of this is that blocking clients will all see an error immediately |
|
// without waiting a whole timeout to see it, but clients that just look up |
|
// cache with an older index than the last valid result will still see the |
|
// result and not the error here. I.e. the error is not "cached" without a |
|
// new fetch attempt occurring, but the last good value can still be fetched |
|
// from cache. |
|
return entry.Value, meta, nil |
|
} |
|
|
|
// If this isn't our first time through and our last value has an error, then |
|
// we return the error. This has the behavior that we don't sit in a retry |
|
// loop getting the same error for the entire duration of the timeout. |
|
// Instead, we make one effort to fetch a new value, and if there was an |
|
// error, we return. Note that the invariant is that if both entry.Value AND |
|
// entry.Error are non-nil, the error _must_ be more recent than the Value. In |
|
// other words valid fetches should reset the error. See |
|
// https://github.com/hashicorp/consul/issues/4480. |
|
if !first && entry.Error != nil { |
|
return entry.Value, ResultMeta{Index: entry.Index}, entry.Error |
|
} |
|
|
|
if first { |
|
// We increment two different counters for cache misses depending on |
|
// whether we're missing because we didn't have the data at all, |
|
// or if we're missing because we're blocking on a set index. |
|
missKey := "miss_block" |
|
if r.Info.MinIndex == 0 { |
|
missKey = "miss_new" |
|
} |
|
metrics.IncrCounter([]string{"consul", "cache", r.TypeEntry.Name, missKey}, 1) |
|
metrics.IncrCounter([]string{"cache", r.TypeEntry.Name, missKey}, 1) |
|
} |
|
|
|
// Set our timeout channel if we must |
|
if r.Info.Timeout > 0 && timeoutCh == nil { |
|
timeoutCh = time.After(r.Info.Timeout) |
|
} |
|
|
|
// At this point, we know we either don't have a value at all or the |
|
// value we have is too old. We need to wait for new data. |
|
waiterCh := c.fetch(key, r) |
|
|
|
// No longer our first time through |
|
first = false |
|
|
|
select { |
|
case <-ctx.Done(): |
|
return nil, ResultMeta{}, ctx.Err() |
|
case <-waiterCh: |
|
// Our fetch returned, retry the get from the cache. |
|
r.Info.MustRevalidate = false |
|
goto RETRY_GET |
|
|
|
case <-timeoutCh: |
|
// Timeout on the cache read, just return whatever we have. |
|
return entry.Value, ResultMeta{Index: entry.Index}, nil |
|
} |
|
} |
|
|
|
func makeEntryKey(t, dc, peerName, token, key string) string { |
|
// TODO(peering): figure out if this is the desired format |
|
if peerName != "" { |
|
return fmt.Sprintf("%s/%s/%s/%s", t, "peer:"+peerName, token, key) |
|
} |
|
return fmt.Sprintf("%s/%s/%s/%s", t, dc, token, key) |
|
} |
|
|
|
// fetch triggers a new background fetch for the given Request. If a background |
|
// fetch is already running or a goroutine to manage that still exists for a |
|
// matching Request, the waiter channel for that request is returned. The |
|
// effect of this is that there is only ever one blocking query and goroutine |
|
// for any matching requests. |
|
func (c *Cache) fetch(key string, r getOptions) <-chan struct{} { |
|
c.entriesLock.Lock() |
|
defer c.entriesLock.Unlock() |
|
|
|
ok, entryValid, entry := c.getEntryLocked(r.TypeEntry, key, r.Info) |
|
|
|
switch { |
|
case ok && entryValid: |
|
// This handles the case where a fetch succeeded after checking for its |
|
// existence in getWithIndex. This ensures that we don't miss updates. |
|
ch := make(chan struct{}) |
|
close(ch) |
|
return ch |
|
|
|
case ok && entry.GoroutineID != 0: |
|
// If we already have an entry and there's a goroutine to keep it |
|
// refreshed then don't spawn another one to do the same work. |
|
// |
|
// Return the currently active waiter. |
|
return entry.Waiter |
|
|
|
case !ok: |
|
// If we don't have an entry, then create it. The entry must be marked |
|
// as invalid so that it isn't returned as a valid value for a zero |
|
// index. |
|
entry = cacheEntry{ |
|
Valid: false, |
|
Waiter: make(chan struct{}), |
|
FetchRateLimiter: rate.NewLimiter( |
|
c.options.EntryFetchRate, |
|
c.options.EntryFetchMaxBurst, |
|
), |
|
} |
|
} |
|
|
|
// Assign each background fetching goroutine a unique ID and fingerprint |
|
// the cache entry with the same ID. This way if the cache entry is ever |
|
// cleaned up due to expiry and later recreated the old goroutine can |
|
// detect that and terminate rather than leak and do double work. |
|
c.lastGoroutineID++ |
|
entry.GoroutineID = c.lastGoroutineID |
|
c.entries[key] = entry |
|
metrics.SetGauge([]string{"consul", "cache", "entries_count"}, float32(len(c.entries))) |
|
metrics.SetGauge([]string{"cache", "entries_count"}, float32(len(c.entries))) |
|
|
|
// The actual Fetch must be performed in a goroutine. |
|
go c.launchBackgroundFetcher(entry.GoroutineID, key, r) |
|
|
|
return entry.Waiter |
|
} |
|
|
|
func (c *Cache) launchBackgroundFetcher(goroutineID uint64, key string, r getOptions) { |
|
defer func() { |
|
c.entriesLock.Lock() |
|
defer c.entriesLock.Unlock() |
|
entry, ok := c.entries[key] |
|
if ok && entry.GoroutineID == goroutineID { |
|
entry.GoroutineID = 0 |
|
c.entries[key] = entry |
|
} |
|
}() |
|
|
|
var attempt uint |
|
for { |
|
shouldStop, shouldBackoff := c.runBackgroundFetcherOnce(goroutineID, key, r) |
|
if shouldStop { |
|
return |
|
} |
|
|
|
if shouldBackoff { |
|
attempt++ |
|
} else { |
|
attempt = 0 |
|
} |
|
// If we're over the attempt minimum, start an exponential backoff. |
|
wait := backOffWait(c.options, attempt) |
|
|
|
// If we have a timer, wait for it |
|
wait += r.TypeEntry.Opts.RefreshTimer |
|
|
|
select { |
|
case <-time.After(wait): |
|
case <-c.stopCh: |
|
return // Check if cache was stopped |
|
} |
|
|
|
// Trigger. |
|
r.Info.MustRevalidate = false |
|
r.Info.MinIndex = 0 |
|
|
|
// We acquire a write lock because we may have to set Fetching to true. |
|
c.entriesLock.Lock() |
|
|
|
entry, ok := c.entries[key] |
|
if !ok || entry.GoroutineID != goroutineID { |
|
// If we don't have an existing entry, return immediately. |
|
// |
|
// Also if we already have an entry and it is actively fetching, then |
|
// return immediately. |
|
// |
|
// If we've somehow lost control of the entry, also return. |
|
c.entriesLock.Unlock() |
|
return |
|
} |
|
|
|
c.entries[key] = entry |
|
metrics.SetGauge([]string{"consul", "cache", "entries_count"}, float32(len(c.entries))) |
|
metrics.SetGauge([]string{"cache", "entries_count"}, float32(len(c.entries))) |
|
c.entriesLock.Unlock() |
|
} |
|
} |
|
|
|
func (c *Cache) runBackgroundFetcherOnce(goroutineID uint64, key string, r getOptions) (shouldStop, shouldBackoff bool) { |
|
// Freshly re-read this, rather than relying upon the caller to fetch it |
|
// and pass it in. |
|
c.entriesLock.RLock() |
|
entry, ok := c.entries[key] |
|
c.entriesLock.RUnlock() |
|
|
|
if !ok || entry.GoroutineID != goroutineID { |
|
// If we don't have an existing entry, return immediately. |
|
// |
|
// Also if something weird has happened to orphan this goroutine, also |
|
// return immediately. |
|
return true, false |
|
} |
|
|
|
tEntry := r.TypeEntry |
|
{ // NOTE: this indentation is here to facilitate the PR review diff only |
|
// If we have background refresh and currently are in "disconnected" state, |
|
// waiting for a response might mean we mark our results as stale for up to |
|
// 10 minutes (max blocking timeout) after connection is restored. To reduce |
|
// that window, we assume that if the fetch takes more than 31 seconds then |
|
// they are correctly blocking. We choose 31 seconds because yamux |
|
// keepalives are every 30 seconds so the RPC should fail if the packets are |
|
// being blackholed for more than 30 seconds. |
|
var connectedTimer *time.Timer |
|
if tEntry.Opts.Refresh && entry.Index > 0 && tEntry.Opts.QueryTimeout > 31*time.Second { |
|
connectedTimer = time.AfterFunc(31*time.Second, func() { |
|
c.entriesLock.Lock() |
|
defer c.entriesLock.Unlock() |
|
entry, ok := c.entries[key] |
|
if !ok || entry.RefreshLostContact.IsZero() || entry.GoroutineID != goroutineID { |
|
return |
|
} |
|
entry.RefreshLostContact = time.Time{} |
|
c.entries[key] = entry |
|
}) |
|
} |
|
|
|
fOpts := FetchOptions{} |
|
if tEntry.Opts.SupportsBlocking { |
|
fOpts.MinIndex = entry.Index |
|
fOpts.Timeout = tEntry.Opts.QueryTimeout |
|
|
|
if fOpts.Timeout == 0 { |
|
fOpts.Timeout = 10 * time.Minute |
|
} |
|
} |
|
if entry.Valid { |
|
fOpts.LastResult = &FetchResult{ |
|
Value: entry.Value, |
|
State: entry.State, |
|
Index: entry.Index, |
|
} |
|
} |
|
|
|
if err := entry.FetchRateLimiter.Wait(c.rateLimitContext); err != nil { |
|
if connectedTimer != nil { |
|
connectedTimer.Stop() |
|
} |
|
entry.Error = fmt.Errorf("rateLimitContext canceled: %s", err.Error()) |
|
// NOTE: this can only happen when the entire cache is being |
|
// shutdown and isn't something that can happen normally. |
|
return true, false |
|
} |
|
// Start building the new entry by blocking on the fetch. |
|
result, err := r.Fetch(fOpts) |
|
if connectedTimer != nil { |
|
connectedTimer.Stop() |
|
} |
|
|
|
// Copy the existing entry to start. |
|
newEntry := entry |
|
|
|
// Importantly, always reset the Error. Having both Error and a Value that |
|
// are non-nil is allowed in the cache entry but it indicates that the Error |
|
// is _newer_ than the last good value. So if the err is nil then we need to |
|
// reset to replace any _older_ errors and avoid them bubbling up. If the |
|
// error is non-nil then we need to set it anyway and used to do it in the |
|
// code below. See https://github.com/hashicorp/consul/issues/4480. |
|
newEntry.Error = err |
|
|
|
if result.Value != nil { |
|
// A new value was given, so we create a brand new entry. |
|
if !result.NotModified { |
|
newEntry.Value = result.Value |
|
} |
|
newEntry.State = result.State |
|
newEntry.Index = result.Index |
|
newEntry.FetchedAt = time.Now() |
|
if newEntry.Index < 1 { |
|
// Less than one is invalid unless there was an error and in this case |
|
// there wasn't since a value was returned. If a badly behaved RPC |
|
// returns 0 when it has no data, we might get into a busy loop here. We |
|
// set this to minimum of 1 which is safe because no valid user data can |
|
// ever be written at raft index 1 due to the bootstrap process for |
|
// raft. This insure that any subsequent background refresh request will |
|
// always block, but allows the initial request to return immediately |
|
// even if there is no data. |
|
newEntry.Index = 1 |
|
} |
|
|
|
// This is a valid entry with a result |
|
newEntry.Valid = true |
|
} else if result.State != nil && err == nil { |
|
// Also set state if it's non-nil but Value is nil. This is important in the |
|
// case we are returning nil due to a timeout or a transient error like rate |
|
// limiting that we want to mask from the user - there is no result yet but |
|
// we want to manage retrying internally before we return an error to user. |
|
// The retrying state is in State so we need to still update that in the |
|
// entry even if we don't have an actual result yet (e.g. hit a rate limit |
|
// on first request for a leaf certificate). |
|
newEntry.State = result.State |
|
} |
|
|
|
preventRefresh := acl.IsErrNotFound(err) |
|
|
|
// Error handling |
|
if err == nil { |
|
labels := []metrics.Label{{Name: "result_not_modified", Value: strconv.FormatBool(result.NotModified)}} |
|
// TODO(kit): move tEntry.Name to a label on the first write here and deprecate the second write |
|
metrics.IncrCounterWithLabels([]string{"consul", "cache", "fetch_success"}, 1, labels) |
|
metrics.IncrCounterWithLabels([]string{"cache", "fetch_success"}, 1, labels) |
|
metrics.IncrCounterWithLabels([]string{"consul", "cache", tEntry.Name, "fetch_success"}, 1, labels) |
|
metrics.IncrCounterWithLabels([]string{"cache", tEntry.Name, "fetch_success"}, 1, labels) |
|
|
|
if result.Index > 0 { |
|
// Reset the attempts counter so we don't have any backoff |
|
shouldBackoff = false |
|
} else { |
|
// Result having a zero index is an implicit error case. There was no |
|
// actual error but it implies the RPC found in index (nothing written |
|
// yet for that type) but didn't take care to return safe "1" index. We |
|
// don't want to actually treat it like an error by setting |
|
// newEntry.Error to something non-nil, but we should guard against 100% |
|
// CPU burn hot loops caused by that case which will never block but |
|
// also won't backoff either. So we treat it as a failed attempt so that |
|
// at least the failure backoff will save our CPU while still |
|
// periodically refreshing so normal service can resume when the servers |
|
// actually have something to return from the RPC. If we get in this |
|
// state it can be considered a bug in the RPC implementation (to ever |
|
// return a zero index) however since it can happen this is a safety net |
|
// for the future. |
|
shouldBackoff = true |
|
} |
|
|
|
// If we have refresh active, this successful response means cache is now |
|
// "connected" and should not be stale. Reset the lost contact timer. |
|
if tEntry.Opts.Refresh { |
|
newEntry.RefreshLostContact = time.Time{} |
|
} |
|
} else { |
|
// TODO (mkeeler) maybe change the name of this label to be more indicative of it just |
|
// stopping the background refresh |
|
labels := []metrics.Label{{Name: "fatal", Value: strconv.FormatBool(preventRefresh)}} |
|
|
|
// TODO(kit): Add tEntry.Name to label on fetch_error and deprecate second write |
|
metrics.IncrCounterWithLabels([]string{"consul", "cache", "fetch_error"}, 1, labels) |
|
metrics.IncrCounterWithLabels([]string{"cache", "fetch_error"}, 1, labels) |
|
metrics.IncrCounterWithLabels([]string{"consul", "cache", tEntry.Name, "fetch_error"}, 1, labels) |
|
metrics.IncrCounterWithLabels([]string{"cache", tEntry.Name, "fetch_error"}, 1, labels) |
|
|
|
// Increment attempt counter |
|
shouldBackoff = true |
|
|
|
// If we are refreshing and just failed, updated the lost contact time as |
|
// our cache will be stale until we get successfully reconnected. We only |
|
// set this on the first failure (if it's zero) so we can track how long |
|
// it's been since we had a valid connection/up-to-date view of the state. |
|
if tEntry.Opts.Refresh && newEntry.RefreshLostContact.IsZero() { |
|
newEntry.RefreshLostContact = time.Now() |
|
} |
|
} |
|
|
|
// Create a new waiter that will be used for the next fetch. |
|
newEntry.Waiter = make(chan struct{}) |
|
|
|
// Set our entry |
|
c.entriesLock.Lock() |
|
|
|
if currEntry, ok := c.entries[key]; !ok || currEntry.GoroutineID != goroutineID { |
|
// This entry was evicted during our fetch. DON'T re-insert it or fall |
|
// through to the refresh loop below otherwise it will live forever! In |
|
// theory there should not be any Get calls waiting on entry.Waiter since |
|
// they would have prevented the eviction, but in practice there may be |
|
// due to timing and the fact that we don't update the TTL on the entry if |
|
// errors are being returned for a while. So we do need to unblock them, |
|
// which will mean they recreate the entry again right away and so "reset" |
|
// to a good state anyway! |
|
c.entriesLock.Unlock() |
|
|
|
// Trigger any waiters that are around. |
|
close(entry.Waiter) |
|
return true, false |
|
} |
|
|
|
// If this is a new entry (not in the heap yet), then setup the |
|
// initial expiry information and insert. If we're already in |
|
// the heap we do nothing since we're reusing the same entry. |
|
if newEntry.Expiry == nil || newEntry.Expiry.Index() == ttlcache.NotIndexed { |
|
newEntry.Expiry = c.entriesExpiryHeap.Add(key, tEntry.Opts.LastGetTTL) |
|
} |
|
|
|
c.entries[key] = newEntry |
|
c.entriesLock.Unlock() |
|
|
|
// Trigger the old waiter |
|
close(entry.Waiter) |
|
|
|
// If refresh is enabled, run the refresh in due time. The refresh |
|
// below might block, but saves us from spawning another goroutine. |
|
// |
|
// We want to have ACL not found errors stop cache refresh for the cases |
|
// where the token used for the query was deleted. If the request |
|
// was coming from a cache notification then it will start the |
|
// request back up again shortly but in the general case this prevents |
|
// spamming the logs with tons of ACL not found errors for days. |
|
if tEntry.Opts.Refresh && !preventRefresh { |
|
return false, shouldBackoff |
|
} |
|
} |
|
|
|
return true, false |
|
} |
|
|
|
func backOffWait(opts Options, failures uint) time.Duration { |
|
if failures > opts.CacheRefreshBackoffMin { |
|
shift := failures - opts.CacheRefreshBackoffMin |
|
waitTime := opts.CacheRefreshMaxWait |
|
if shift < 31 { |
|
waitTime = (1 << shift) * time.Second |
|
} |
|
if waitTime > opts.CacheRefreshMaxWait { |
|
waitTime = opts.CacheRefreshMaxWait |
|
} |
|
return waitTime + lib.RandomStagger(waitTime) |
|
} |
|
return 0 |
|
} |
|
|
|
// runExpiryLoop is a blocking function that watches the expiration |
|
// heap and invalidates entries that have expired. |
|
func (c *Cache) runExpiryLoop() { |
|
for { |
|
c.entriesLock.RLock() |
|
timer := c.entriesExpiryHeap.Next() |
|
c.entriesLock.RUnlock() |
|
|
|
select { |
|
case <-c.stopCh: |
|
timer.Stop() |
|
return |
|
case <-c.entriesExpiryHeap.NotifyCh: |
|
timer.Stop() |
|
continue |
|
|
|
case <-timer.Wait(): |
|
c.entriesLock.Lock() |
|
|
|
entry := timer.Entry |
|
if closer, ok := c.entries[entry.Key()].State.(io.Closer); ok { |
|
closer.Close() |
|
} |
|
|
|
// Entry expired! Remove it. |
|
delete(c.entries, entry.Key()) |
|
c.entriesExpiryHeap.Remove(entry.Index()) |
|
|
|
// Set some metrics |
|
metrics.IncrCounter([]string{"consul", "cache", "evict_expired"}, 1) |
|
metrics.IncrCounter([]string{"cache", "evict_expired"}, 1) |
|
metrics.SetGauge([]string{"consul", "cache", "entries_count"}, float32(len(c.entries))) |
|
metrics.SetGauge([]string{"cache", "entries_count"}, float32(len(c.entries))) |
|
|
|
c.entriesLock.Unlock() |
|
} |
|
} |
|
} |
|
|
|
// Close stops any background work and frees all resources for the cache. |
|
// Current Fetch requests are allowed to continue to completion and callers may |
|
// still access the current cache values so coordination isn't needed with |
|
// callers, however no background activity will continue. It's intended to close |
|
// the cache at agent shutdown so no further requests should be made, however |
|
// concurrent or in-flight ones won't break. |
|
func (c *Cache) Close() error { |
|
wasStopped := atomic.SwapUint32(&c.stopped, 1) |
|
if wasStopped == 0 { |
|
// First time only, close stop chan |
|
close(c.stopCh) |
|
c.rateLimitCancel() |
|
} |
|
return nil |
|
} |
|
|
|
// Prepopulate puts something in the cache manually. This is useful when the |
|
// correct initial value is know and the cache shouldn't refetch the same thing |
|
// on startup. It is used to set the ConnectRootCA and AgentLeafCert when |
|
// AutoEncrypt.TLS is turned on. The cache itself cannot fetch that the first |
|
// time because it requires a special RPCType. Subsequent runs are fine though. |
|
func (c *Cache) Prepopulate(t string, res FetchResult, dc, peerName, token, k string) error { |
|
key := makeEntryKey(t, dc, peerName, token, k) |
|
newEntry := cacheEntry{ |
|
Valid: true, |
|
Value: res.Value, |
|
State: res.State, |
|
Index: res.Index, |
|
FetchedAt: time.Now(), |
|
Waiter: make(chan struct{}), |
|
FetchRateLimiter: rate.NewLimiter( |
|
c.options.EntryFetchRate, |
|
c.options.EntryFetchMaxBurst, |
|
), |
|
} |
|
c.entriesLock.Lock() |
|
c.entries[key] = newEntry |
|
c.entriesLock.Unlock() |
|
return nil |
|
}
|
|
|