mirror of https://github.com/grpc/grpc-go.git
245 lines
8.8 KiB
Go
245 lines
8.8 KiB
Go
/*
|
|
*
|
|
* Copyright 2020 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
// Package cache provides an LRU cache implementation to be used by the RLS LB
|
|
// policy to cache RLS response data.
|
|
package cache
|
|
|
|
import (
|
|
"container/list"
|
|
"sync"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/balancer"
|
|
"google.golang.org/grpc/grpclog"
|
|
"google.golang.org/grpc/internal/backoff"
|
|
)
|
|
|
|
var logger = grpclog.Component("rls")
|
|
|
|
// Key represents the cache key used to uniquely identify a cache entry.
|
|
type Key struct {
|
|
// Path is the full path of the incoming RPC request.
|
|
Path string
|
|
// KeyMap is a stringified version of the RLS request keys built using the
|
|
// RLS keyBuilder. Since map is not a Type which is comparable in Go, it
|
|
// cannot be part of the key for another map (the LRU cache is implemented
|
|
// using a native map type).
|
|
KeyMap string
|
|
}
|
|
|
|
// Entry wraps all the data to be stored in a cache entry.
|
|
type Entry struct {
|
|
// Mu synchronizes access to this particular cache entry. The LB policy
|
|
// will also hold another mutex to synchronize access to the cache as a
|
|
// whole. To avoid holding the top-level mutex for the whole duration for
|
|
// which one particular cache entry is acted upon, we use this entry mutex.
|
|
Mu sync.Mutex
|
|
// ExpiryTime is the absolute time at which the data cached as part of this
|
|
// entry stops being valid. When an RLS request succeeds, this is set to
|
|
// the current time plus the max_age field from the LB policy config. An
|
|
// entry with this field in the past is not used to process picks.
|
|
ExpiryTime time.Time
|
|
// BackoffExpiryTime is the absolute time at which an entry which has gone
|
|
// through backoff stops being valid. When an RLS request fails, this is
|
|
// set to the current time plus twice the backoff time. The cache expiry
|
|
// timer will only delete entries for which both ExpiryTime and
|
|
// BackoffExpiryTime are in the past.
|
|
BackoffExpiryTime time.Time
|
|
// StaleTime is the absolute time after which this entry will be
|
|
// proactively refreshed if we receive a request for it. When an RLS
|
|
// request succeeds, this is set to the current time plus the stale_age
|
|
// from the LB policy config.
|
|
StaleTime time.Time
|
|
// BackoffTime is the absolute time at which the backoff period for this
|
|
// entry ends. The backoff timer is setup with this value. No new RLS
|
|
// requests are sent out for this entry until the backoff period ends.
|
|
BackoffTime time.Time
|
|
// EarliestEvictTime is the absolute time before which this entry should
|
|
// not be evicted from the cache. This is set to a default value of 5
|
|
// seconds when the entry is created. This is required to make sure that a
|
|
// new entry added to the cache is not evicted before the RLS response
|
|
// arrives (usually when the cache is too small).
|
|
EarliestEvictTime time.Time
|
|
// CallStatus stores the RPC status of the previous RLS request for this
|
|
// entry. Picks for entries with a non-nil value for this field are failed
|
|
// with the error stored here.
|
|
CallStatus error
|
|
// Backoff contains all backoff related state. When an RLS request
|
|
// succeeds, backoff state is reset.
|
|
Backoff BackoffState
|
|
// HeaderData is received in an RLS response and is to be sent in the
|
|
// X-Google-RLS-Data header for matching RPCs.
|
|
HeaderData string
|
|
// ChildPicker is a very thin wrapper around the child policy wrapper.
|
|
// The type is declared as a Picker interface since the users of
|
|
// the cache only care about the picker provided by the child policy, and
|
|
// this makes it easy for testing.
|
|
ChildPicker balancer.Picker
|
|
|
|
// size stores the size of this cache entry. Uses only a subset of the
|
|
// fields. See `entrySize` for this is computed.
|
|
size int64
|
|
// key contains the cache key corresponding to this entry. This is required
|
|
// from methods like `removeElement` which only have a pointer to the
|
|
// list.Element which contains a reference to the cache.Entry. But these
|
|
// methods need the cache.Key to be able to remove the entry from the
|
|
// underlying map.
|
|
key Key
|
|
}
|
|
|
|
// BackoffState wraps all backoff related state associated with a cache entry.
|
|
type BackoffState struct {
|
|
// Retries keeps track of the number of RLS failures, to be able to
|
|
// determine the amount of time to backoff before the next attempt.
|
|
Retries int
|
|
// Backoff is an exponential backoff implementation which returns the
|
|
// amount of time to backoff, given the number of retries.
|
|
Backoff backoff.Strategy
|
|
// Timer fires when the backoff period ends and incoming requests after
|
|
// this will trigger a new RLS request.
|
|
Timer *time.Timer
|
|
// Callback provided by the LB policy to be notified when the backoff timer
|
|
// expires. This will trigger a new picker to be returned to the
|
|
// ClientConn, to force queued up RPCs to be retried.
|
|
Callback func()
|
|
}
|
|
|
|
// LRU is a cache with a least recently used eviction policy. It is not safe
|
|
// for concurrent access.
|
|
type LRU struct {
|
|
maxSize int64
|
|
usedSize int64
|
|
onEvicted func(Key, *Entry)
|
|
|
|
ll *list.List
|
|
cache map[Key]*list.Element
|
|
}
|
|
|
|
// NewLRU creates a cache.LRU with a size limit of maxSize and the provided
|
|
// eviction callback.
|
|
//
|
|
// Currently, only the cache.Key and the HeaderData field from cache.Entry
|
|
// count towards the size of the cache (other overhead per cache entry is not
|
|
// counted). The cache could temporarily exceed the configured maxSize because
|
|
// we want the entries to spend a configured minimum amount of time in the
|
|
// cache before they are LRU evicted (so that all the work performed in sending
|
|
// an RLS request and caching the response is not a total waste).
|
|
//
|
|
// The provided onEvited callback must not attempt to re-add the entry inline
|
|
// and the RLS LB policy does not have a need to do that.
|
|
//
|
|
// The cache package trusts the RLS policy (its only user) to supply a default
|
|
// minimum non-zero maxSize, in the event that the ServiceConfig does not
|
|
// provide a value for it.
|
|
func NewLRU(maxSize int64, onEvicted func(Key, *Entry)) *LRU {
|
|
return &LRU{
|
|
maxSize: maxSize,
|
|
onEvicted: onEvicted,
|
|
ll: list.New(),
|
|
cache: make(map[Key]*list.Element),
|
|
}
|
|
}
|
|
|
|
// Resize sets the size limit of the LRU to newMaxSize and removes older
|
|
// entries, if required, to comply with the new limit.
|
|
func (lru *LRU) Resize(newMaxSize int64) {
|
|
lru.maxSize = newMaxSize
|
|
lru.removeToFit(0)
|
|
}
|
|
|
|
// TODO(easwars): If required, make this function more sophisticated.
|
|
func entrySize(key Key, value *Entry) int64 {
|
|
return int64(len(key.Path) + len(key.KeyMap) + len(value.HeaderData))
|
|
}
|
|
|
|
// removeToFit removes older entries from the cache to make room for a new
|
|
// entry of size newSize.
|
|
func (lru *LRU) removeToFit(newSize int64) {
|
|
now := time.Now()
|
|
for lru.usedSize+newSize > lru.maxSize {
|
|
elem := lru.ll.Back()
|
|
if elem == nil {
|
|
// This is a corner case where the cache is empty, but the new entry
|
|
// to be added is bigger than maxSize.
|
|
logger.Info("rls: newly added cache entry exceeds cache maxSize")
|
|
return
|
|
}
|
|
|
|
entry := elem.Value.(*Entry)
|
|
if t := entry.EarliestEvictTime; !t.IsZero() && t.Before(now) {
|
|
// When the oldest entry is too new (it hasn't even spent a default
|
|
// minimum amount of time in the cache), we abort and allow the
|
|
// cache to grow bigger than the configured maxSize.
|
|
logger.Info("rls: LRU eviction finds oldest entry to be too new. Allowing cache to exceed maxSize momentarily")
|
|
return
|
|
}
|
|
lru.removeElement(elem)
|
|
}
|
|
}
|
|
|
|
// Add adds a new entry to the cache.
|
|
func (lru *LRU) Add(key Key, value *Entry) {
|
|
size := entrySize(key, value)
|
|
elem, ok := lru.cache[key]
|
|
if !ok {
|
|
lru.removeToFit(size)
|
|
lru.usedSize += size
|
|
value.size = size
|
|
value.key = key
|
|
elem := lru.ll.PushFront(value)
|
|
lru.cache[key] = elem
|
|
return
|
|
}
|
|
|
|
existing := elem.Value.(*Entry)
|
|
sizeDiff := size - existing.size
|
|
lru.removeToFit(sizeDiff)
|
|
value.size = size
|
|
elem.Value = value
|
|
lru.ll.MoveToFront(elem)
|
|
lru.usedSize += sizeDiff
|
|
}
|
|
|
|
// Remove removes a cache entry wth key key, if one exists.
|
|
func (lru *LRU) Remove(key Key) {
|
|
if elem, ok := lru.cache[key]; ok {
|
|
lru.removeElement(elem)
|
|
}
|
|
}
|
|
|
|
func (lru *LRU) removeElement(e *list.Element) {
|
|
entry := e.Value.(*Entry)
|
|
lru.ll.Remove(e)
|
|
delete(lru.cache, entry.key)
|
|
lru.usedSize -= entry.size
|
|
if lru.onEvicted != nil {
|
|
lru.onEvicted(entry.key, entry)
|
|
}
|
|
}
|
|
|
|
// Get returns a cache entry with key key.
|
|
func (lru *LRU) Get(key Key) *Entry {
|
|
elem, ok := lru.cache[key]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
lru.ll.MoveToFront(elem)
|
|
return elem.Value.(*Entry)
|
|
}
|