Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(207)

Side by Side Diff: luci_config/appengine/backend/datastore/ds.go

Issue 2576923003: Implement config service cache on top of datastore (Closed)
Patch Set: Relocated, fix, split integration test, rebase. Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package datastore
6
7 import (
8 "time"
9
10 "github.com/luci/luci-go/appengine/datastorecache"
11 "github.com/luci/luci-go/common/clock"
12 "github.com/luci/luci-go/common/errors"
13 log "github.com/luci/luci-go/common/logging"
14 "github.com/luci/luci-go/luci_config/common/cfgtypes"
15 "github.com/luci/luci-go/luci_config/server/cfgclient"
16 "github.com/luci/luci-go/luci_config/server/cfgclient/access"
17 "github.com/luci/luci-go/luci_config/server/cfgclient/backend"
18 "github.com/luci/luci-go/luci_config/server/cfgclient/backend/caching"
19
20 "golang.org/x/net/context"
21 )
22
23 const (
24 dsCacheSchema = "v1"
25
26 // RPCDeadline is the deadline applied to config service RPCs.
27 RPCDeadline = 10 * time.Minute
28 )
29
30 var dsHandlerKey = "github.com/luci/luci-go/appengine/gaeconfig.dsHandlerKey"
31
32 func getCacheHandler(c context.Context) datastorecache.Handler {
33 v, _ := c.Value(&dsHandlerKey).(datastorecache.Handler)
34 return v
35 }
36
37 // Cache is our registered datastore cache, bound to our Config Handler. The
38 // generator function is used by the cache manager task to get a Handler
39 // instance during refresh.
40 var Cache = datastorecache.Cache{
41 Name: "github.com/luci/luci-go/appengine/gaeconfig",
42 AccessUpdateInterval: 24 * time.Hour,
43 PruneFactor: 4,
44 Parallel: 16,
45 HandlerFunc: getCacheHandler,
46 }
47
48 // dsCacheBackend is an interface around datastoreCache.Cache functionality used
49 // by dsCacheFilter.
50 //
51 // We specialize this in testing to swap in other cache backends.
52 type dsCacheBackend interface {
53 Get(c context.Context, key []byte) (datastorecache.Value, error)
54 }
55
56 // Config is a datastore cache configuration.
57 //
58 // Cache parameters can be extracted from a config instance.
59 type Config struct {
60 // RefreshInterval is the cache entry refresh interval.
61 RefreshInterval time.Duration
62 // FailOpen, if true, means that a cache miss or failure should be passe d
63 // through to the underlying cache.
64 FailOpen bool
65
66 // userProjAccess is cache of the current user's project access lookups.
67 userProjAccess map[string]bool
68 // anonProjAccess is cache of the current user's project access lookups.
69 anonProjAccess map[string]bool
70
71 // cache, if nil, is the datastore cache. This can be set for testing.
72 cache dsCacheBackend
73 }
74
75 // Backend wraps the specified Backend in a datastore-backed cache.
76 func (dc *Config) Backend(base backend.B) backend.B {
77 return &caching.Backend{
78 B: base,
79 FailOnError: !dc.FailOpen,
80 CacheGet: dc.cacheGet,
81 }
82 }
83
84 // WithHandler installs a datastorecache.Handler into our Context. This is
85 // used during datastore cache's Refresh calls.
86 //
87 // The Handler binds the parameters and Loader to the resolution call. For
88 // service resolution, this will be the Loader that is provided by the caching
89 // layer. For cron refresh, this will be the generic Loader provided by
90 // CronLoader.
91 func (dc *Config) WithHandler(c context.Context, l caching.Loader, timeout time. Duration) context.Context {
92 handler := dsCacheHandler{
93 refreshInterval: dc.RefreshInterval,
94 failOpen: dc.FailOpen,
95 loader: l,
96 loaderTimeout: timeout,
97 }
98 return context.WithValue(c, &dsHandlerKey, &handler)
99 }
100
101 func (dc *Config) cacheGet(c context.Context, key caching.Key, l caching.Loader) (
102 *caching.Value, error) {
103
104 cache := dc.cache
105 if cache == nil {
106 cache = &Cache
107 }
108
109 // Modify our cache key to always refresh AsService (ACLs will be assert ed on
110 // load) and request full-content.
111 origAuthority := key.Authority
112 key.Authority = backend.AsService
113
114 // If we can assert access based on the operation's target ConfigSet, do that
115 // before actually performing the action.
116 switch key.Op {
117 case caching.OpGet, caching.OpConfigSetURL:
118 // We can deny access to this operation based solely on the requ ested
119 // ConfigSet.
120 if err := access.Check(c, origAuthority, cfgtypes.ConfigSet(key. ConfigSet)); err != nil {
121 // Access check failed, simulate ErrNoSuchConfig.
122 //
123 // OpGet: Indicate no content via empty Items.
124 // OpConfigSetURL: Indicate no content via empty "URL" f ield in value.
125 return &caching.Value{}, nil
126 }
127 }
128
129 // For content-fetching operations, always request full content.
130 switch key.Op {
131 case caching.OpGet, caching.OpGetAll:
132 // Always ask for full content.
133 key.Content = true
134 }
135
136 // Encode our caching key, and use this for our datastore cache key.
137 //
138 // This gets recoded in dsCacheHandler's "Refresh" to identify the cache
139 // operation that is being performed.
140 encKey, err := caching.Encode(&key)
141 if err != nil {
142 return nil, errors.Annotate(err).Reason("failed to encode cache key").Err()
143 }
144
145 // Construct a cache handler.
146 v, err := cache.Get(dc.WithHandler(c, l, 0), encKey)
147 if err != nil {
148 return nil, err
149 }
150
151 // Decode our response.
152 if v.Schema != dsCacheSchema {
153 return nil, errors.Reason("response schema (%(resp)q) doesn't ma tch current (%(cur)q)").
154 D("resp", v.Schema).D("cur", dsCacheSchema).Err()
155 }
156
157 cacheValue, err := caching.DecodeValue(v.Data)
158 if err != nil {
159 return nil, errors.Annotate(err).Reason("failed to decode cached value").Err()
160 }
161
162 // Prune any responses that are not permitted for the supplied Authority .
163 switch key.Op {
164 case caching.OpGetAll:
165 if len(cacheValue.Items) > 0 {
166 // Shift over any elements that can't be accessed.
167 ptr := 0
168 for _, itm := range cacheValue.Items {
169 if dc.accessConfigSet(c, origAuthority, itm.Conf igSet) {
170 cacheValue.Items[ptr] = itm
171 ptr++
172 }
173 }
174 cacheValue.Items = cacheValue.Items[:ptr]
175 }
176 }
177
178 return cacheValue, nil
179 }
180
181 func (dc *Config) accessConfigSet(c context.Context, a backend.Authority, config Set string) bool {
182 var cacheMap *map[string]bool
183 switch a {
184 case backend.AsService:
185 return true
186 case backend.AsUser:
187 cacheMap = &dc.userProjAccess
188 default:
189 cacheMap = &dc.anonProjAccess
190 }
191
192 // If we've already cached this project access, return the cached value.
193 if v, ok := (*cacheMap)[configSet]; ok {
194 return v
195 }
196
197 // Perform a soft access check.
198 canAccess := false
199 switch err := access.Check(c, a, cfgtypes.ConfigSet(configSet)); err {
200 case nil:
201 canAccess = true
202
203 case access.ErrNoAccess:
204 // No access.
205 break
206 case cfgclient.ErrNoConfig:
207 log.Fields{
208 "configSet": configSet,
209 }.Debugf(c, "Checking access to project without a config.")
210 default:
211 log.Fields{
212 log.ErrorKey: err,
213 "configSet": configSet,
214 }.Warningf(c, "Error checking for project access.")
215 }
216
217 // Cache the result for future lookups.
218 if *cacheMap == nil {
219 *cacheMap = make(map[string]bool)
220 }
221 (*cacheMap)[configSet] = canAccess
222 return canAccess
223 }
224
225 type dsCacheValue struct {
226 // Key is the cache Key.
227 Key caching.Key `json:"k"`
228 // Value is the cache Value.
229 Value *caching.Value `json:"v"`
230 }
231
232 type dsCacheHandler struct {
233 failOpen bool
234 refreshInterval time.Duration
235 loader caching.Loader
236
237 // loaderTimeout, if >0, will be applied prior to performing the loader
238 // operation. This is used for cron operations.
239 loaderTimeout time.Duration
240 }
241
242 func (dch *dsCacheHandler) FailOpen() bool { return dch.fa ilOpen }
243 func (dch *dsCacheHandler) RefreshInterval([]byte) time.Duration { return dch.re freshInterval }
244
245 func (dch *dsCacheHandler) Refresh(c context.Context, key []byte, v datastorecac he.Value) (datastorecache.Value, error) {
246 // Decode the key into our caching key.
247 var ck caching.Key
248 if err := caching.Decode(key, &ck); err != nil {
249 return v, errors.Annotate(err).Reason("failed to decode cache ke y").Err()
250 }
251
252 var cv *caching.Value
253 if v.Schema == dsCacheSchema && len(v.Data) > 0 {
254 // We have a currently-cached value, so decode it into "cv".
255 var err error
256 if cv, err = caching.DecodeValue(v.Data); err != nil {
257 return v, errors.Annotate(err).Reason("failed to decode cache value").Err()
258 }
259 }
260
261 // Apply our timeout, if configured (influences urlfetch).
262 if dch.loaderTimeout > 0 {
263 var cancelFunc context.CancelFunc
264 c, cancelFunc = clock.WithTimeout(c, dch.loaderTimeout)
265 defer cancelFunc()
266 }
267
268 // Perform a cache load on this value.
269 cv, err := dch.loader(c, ck, cv)
270 if err != nil {
271 return v, errors.Annotate(err).Reason("failed to load cache valu e").Err()
272 }
273
274 // Encode the resulting cache value.
275 if v.Data, err = cv.Encode(); err != nil {
276 return v, errors.Annotate(err).Reason("failed to encode cache va lue").Err()
277 }
278 v.Schema = dsCacheSchema
279 v.Description = ck.String()
280 return v, nil
281 }
282
283 // CronLoader returns a caching.Loader implementation to be used
284 // by the Cron task.
285 func CronLoader(b backend.B) caching.Loader {
286 return func(c context.Context, k caching.Key, v *caching.Value) (*cachin g.Value, error) {
287 return caching.CacheLoad(c, b, k, v)
288 }
289 }
OLDNEW
« no previous file with comments | « common/testing/assertions/error_tests.go ('k') | luci_config/appengine/backend/datastore/ds_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698