Skip to content

Commit c26257d

Browse files
authored
rls: Initial implementation of the LRU cache. (#3359)
Contains enough functionality to start working on the users of this cache: the LB policy, picker and cache expiry goroutine.
1 parent 2cd9da6 commit c26257d

File tree

2 files changed

+454
-0
lines changed

2 files changed

+454
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/*
2+
*
3+
* Copyright 2020 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
// Package cache provides an LRU cache implementation to be used by the RLS LB
20+
// policy to cache RLS response data.
21+
package cache
22+
23+
import (
24+
"container/list"
25+
"sync"
26+
"time"
27+
28+
"google.golang.org/grpc/grpclog"
29+
"google.golang.org/grpc/internal/backoff"
30+
)
31+
32+
// Key represents the cache key used to uniquely identify a cache entry.
33+
type Key struct {
34+
// Path is the full path of the incoming RPC request.
35+
Path string
36+
// KeyMap is a stringified version of the RLS request keys built using the
37+
// RLS keyBuilder. Since map is not a Type which is comparable in Go, it
38+
// cannot be part of the key for another map (the LRU cache is implemented
39+
// using a native map type).
40+
KeyMap string
41+
}
42+
43+
// Entry wraps all the data to be stored in a cache entry.
44+
type Entry struct {
45+
// Mu synchronizes access to this particular cache entry. The LB policy
46+
// will also hold another mutex to synchronize access to the cache as a
47+
// whole. To avoid holding the top-level mutex for the whole duration for
48+
// which one particular cache entry is acted upon, we use this entry mutex.
49+
Mu sync.Mutex
50+
// ExpiryTime is the absolute time at which the data cached as part of this
51+
// entry stops being valid. When an RLS request succeeds, this is set to
52+
// the current time plus the max_age field from the LB policy config. An
53+
// entry with this field in the past is not used to process picks.
54+
ExpiryTime time.Time
55+
// BackoffExpiryTime is the absolute time at which an entry which has gone
56+
// through backoff stops being valid. When an RLS request fails, this is
57+
// set to the current time plus twice the backoff time. The cache expiry
58+
// timer will only delete entries for which both ExpiryTime and
59+
// BackoffExpiryTime are in the past.
60+
BackoffExpiryTime time.Time
61+
// StaleTime is the absolute time after which this entry will be
62+
// proactively refreshed if we receive a request for it. When an RLS
63+
// request succeeds, this is set to the current time plus the stale_age
64+
// from the LB policy config.
65+
StaleTime time.Time
66+
// BackoffTime is the absolute time at which the backoff period for this
67+
// entry ends. The backoff timer is setup with this value. No new RLS
68+
// requests are sent out for this entry until the backoff period ends.
69+
BackoffTime time.Time
70+
// EarliestEvictTime is the absolute time before which this entry should
71+
// not be evicted from the cache. This is set to a default value of 5
72+
// seconds when the entry is created. This is required to make sure that a
73+
// new entry added to the cache is not evicted before the RLS response
74+
// arrives (usually when the cache is too small).
75+
EarliestEvictTime time.Time
76+
// CallStatus stores the RPC status of the previous RLS request for this
77+
// entry. Picks for entries with a non-nil value for this field are failed
78+
// with the error stored here.
79+
CallStatus error
80+
// Backoff contains all backoff related state. When an RLS request
81+
// succeeds, backoff state is reset.
82+
Backoff *BackoffState
83+
// HeaderData is received in an RLS response and is to be sent in the
84+
// X-Google-RLS-Data header for matching RPCs.
85+
HeaderData string
86+
// TODO(easwars): Add support to store the ChildPolicy here. Need a
87+
// balancerWrapper type to be implemented for this.
88+
89+
// size stores the size of this cache entry. Uses only a subset of the
90+
// fields. See `entrySize` for this is computed.
91+
size int
92+
// key contains the cache key corresponding to this entry. This is required
93+
// from methods like `removeElement` which only have a pointer to the
94+
// list.Element which contains a reference to the cache.Entry. But these
95+
// methods need the cache.Key to be able to remove the entry from the
96+
// underlying map.
97+
key Key
98+
}
99+
100+
// BackoffState wraps all backoff related state associated with a cache entry.
101+
type BackoffState struct {
102+
// Retries keeps track of the number of RLS failures, to be able to
103+
// determine the amount of time to backoff before the next attempt.
104+
Retries int
105+
// Backoff is an exponential backoff implementation which returns the
106+
// amount of time to backoff, given the number of retries.
107+
Backoff backoff.Strategy
108+
// Timer fires when the backoff period ends and incoming requests after
109+
// this will trigger a new RLS request.
110+
Timer *time.Timer
111+
// Callback provided by the LB policy to be notified when the backoff timer
112+
// expires. This will trigger a new picker to be returned to the
113+
// ClientConn, to force queued up RPCs to be retried.
114+
Callback func()
115+
}
116+
117+
// LRU is a cache with a least recently used eviction policy. It is not safe
118+
// for concurrent access.
119+
type LRU struct {
120+
maxSize int
121+
usedSize int
122+
onEvicted func(Key, *Entry)
123+
124+
ll *list.List
125+
cache map[Key]*list.Element
126+
}
127+
128+
// NewLRU creates a cache.LRU with a size limit of maxSize and the provided
129+
// eviction callback.
130+
//
131+
// Currently, only the cache.Key and the HeaderData field from cache.Entry
132+
// count towards the size of the cache (other overhead per cache entry is not
133+
// counted). The cache could temporarily exceed the configured maxSize because
134+
// we want the entries to spend a configured minimum amount of time in the
135+
// cache before they are LRU evicted (so that all the work performed in sending
136+
// an RLS request and caching the response is not a total waste).
137+
//
138+
// The provided onEvited callback must not attempt to re-add the entry inline
139+
// and the RLS LB policy does not have a need to do that.
140+
//
141+
// The cache package trusts the RLS policy (its only user) to supply a default
142+
// minimum non-zero maxSize, in the event that the ServiceConfig does not
143+
// provide a value for it.
144+
func NewLRU(maxSize int, onEvicted func(Key, *Entry)) *LRU {
145+
return &LRU{
146+
maxSize: maxSize,
147+
onEvicted: onEvicted,
148+
ll: list.New(),
149+
cache: make(map[Key]*list.Element),
150+
}
151+
}
152+
153+
// TODO(easwars): If required, make this function more sophisticated.
154+
func entrySize(key Key, value *Entry) int {
155+
return len(key.Path) + len(key.KeyMap) + len(value.HeaderData)
156+
}
157+
158+
// removeToFit removes older entries from the cache to make room for a new
159+
// entry of size newSize.
160+
func (lru *LRU) removeToFit(newSize int) {
161+
now := time.Now()
162+
for lru.usedSize+newSize > lru.maxSize {
163+
elem := lru.ll.Back()
164+
if elem == nil {
165+
// This is a corner case where the cache is empty, but the new entry
166+
// to be added is bigger than maxSize.
167+
grpclog.Info("rls: newly added cache entry exceeds cache maxSize")
168+
return
169+
}
170+
171+
entry := elem.Value.(*Entry)
172+
if t := entry.EarliestEvictTime; !t.IsZero() && t.Before(now) {
173+
// When the oldest entry is too new (it hasn't even spent a default
174+
// minimum amount of time in the cache), we abort and allow the
175+
// cache to grow bigger than the configured maxSize.
176+
grpclog.Info("rls: LRU eviction finds oldest entry to be too new. Allowing cache to exceed maxSize momentarily")
177+
return
178+
}
179+
lru.removeElement(elem)
180+
}
181+
}
182+
183+
// Add adds a new entry to the cache.
184+
func (lru *LRU) Add(key Key, value *Entry) {
185+
size := entrySize(key, value)
186+
elem, ok := lru.cache[key]
187+
if !ok {
188+
lru.removeToFit(size)
189+
lru.usedSize += size
190+
value.size = size
191+
value.key = key
192+
elem := lru.ll.PushFront(value)
193+
lru.cache[key] = elem
194+
return
195+
}
196+
197+
existing := elem.Value.(*Entry)
198+
sizeDiff := size - existing.size
199+
lru.removeToFit(sizeDiff)
200+
value.size = size
201+
elem.Value = value
202+
lru.ll.MoveToFront(elem)
203+
lru.usedSize += sizeDiff
204+
}
205+
206+
// Remove removes a cache entry wth key key, if one exists.
207+
func (lru *LRU) Remove(key Key) {
208+
if elem, ok := lru.cache[key]; ok {
209+
lru.removeElement(elem)
210+
}
211+
}
212+
213+
func (lru *LRU) removeElement(e *list.Element) {
214+
entry := e.Value.(*Entry)
215+
lru.ll.Remove(e)
216+
delete(lru.cache, entry.key)
217+
lru.usedSize -= entry.size
218+
if lru.onEvicted != nil {
219+
lru.onEvicted(entry.key, entry)
220+
}
221+
}
222+
223+
// Get returns a cache entry with key key.
224+
func (lru *LRU) Get(key Key) *Entry {
225+
elem, ok := lru.cache[key]
226+
if !ok {
227+
return nil
228+
}
229+
lru.ll.MoveToFront(elem)
230+
return elem.Value.(*Entry)
231+
}

0 commit comments

Comments
 (0)