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

Side by Side Diff: appengine/gaeconfig/ds.go

Issue 2576923003: Implement config service cache on top of datastore (Closed)
Patch Set: Created 4 years 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
« no previous file with comments | « appengine/gaeconfig/default.go ('k') | appengine/gaeconfig/ds_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 gaeconfig
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/server/config"
15 "github.com/luci/luci-go/server/config/access"
16 "github.com/luci/luci-go/server/config/caching"
17
18 "golang.org/x/net/context"
19 )
20
21 const (
22 dsCacheSchema = "v1"
23
24 // dsRPCDeadline is the deadline applied to config service RPCs.
25 dsRPCDeadline = 10 * time.Minute
26 )
27
28 var dsHandlerKey = "github.com/luci/luci-go/appengine/gaeconfig.dsHandlerKey"
29
30 func getCacheHandler(c context.Context) datastorecache.Handler {
31 v, _ := c.Value(&dsHandlerKey).(datastorecache.Handler)
32 return v
33 }
34
35 // dsCacheHandler is our registered datastore cache, bound to our Config
36 // Handler. The generator function is used by the cache manager task to get
37 // a Handler instance during refresh.
38 var dsCache = datastorecache.Cache{
39 Name: "github.com/luci/luci-go/appengine/gaeconfig",
40 AccessUpdateInterval: 24 * time.Hour,
41 PruneFactor: 4,
42 Parallel: 16,
43 HandlerFunc: getCacheHandler,
44 }
45
46 // dsCacheBackend is an interface around datastoreCache.Cache functionality used
47 // by dsCacheFilter.
48 //
49 // We specialize this in testing to swap in other cache backends.
50 type dsCacheBackend interface {
51 Get(c context.Context, key []byte) (datastorecache.Value, error)
52 }
53
54 type datastoreCache struct {
55 refreshInterval time.Duration
56 failOpen bool
57
58 // userProjAccess is cache of the current user's project access lookups.
59 userProjAccess map[string]bool
60 // anonProjAccess is cache of the current user's project access lookups.
61 anonProjAccess map[string]bool
62
63 // cache, if nil, is the datastore cache. This can be set for testing.
64 cache dsCacheBackend
65 }
66
67 func (dc *datastoreCache) getBackend(base config.Backend) config.Backend {
68 return &caching.Backend{
69 Backend: base,
70 HardFailure: !dc.failOpen,
71 CacheGet: dc.cacheGet,
72 }
73 }
74
75 // withHandler installs a datastorecache.Handler into our Context. This is
76 // used during dsCache.Refresh calls.
77 //
78 // The Handler binds the parameters and Loader to the resolution call. For
79 // service resolution, this will be the Loader that is provided by the caching
80 // layer. For cron refresh, his will be the generic Loader provided by
iannucci 2017/01/07 21:18:36 this
dnj 2017/01/10 03:32:40 his will be Done.
81 // datastoreCronLoader.
82 func (dc *datastoreCache) withHandler(c context.Context, l caching.Loader, timeo ut time.Duration) context.Context {
83 handler := dsCacheHandler{
84 refreshInterval: dc.refreshInterval,
85 failOpen: dc.failOpen,
86 loader: l,
87 loaderTimeout: timeout,
88 }
89 return context.WithValue(c, &dsHandlerKey, &handler)
90 }
91
92 func (dc *datastoreCache) cacheGet(c context.Context, key caching.Key, l caching .Loader) (
93 *caching.Value, error) {
94
95 cache := dc.cache
96 if cache == nil {
97 cache = &dsCache
98 }
99
100 // Modify our cache key to always refresh AsService (ACLs will be assert ed on
101 // load) and request full-content.
102 origAuthority := key.Authority
103 key.Authority = config.AsService
104
105 // Pre-operation checks and adjustments, for operations whose access can be
106 // denied without needing to actually on the input parameters alone.
iannucci 2017/01/07 21:18:36 I think something went missing in this sentance. o
dnj 2017/01/10 03:32:40 I cleaned this up, breaking into two stages: one t
107 switch key.Op {
108 case caching.OpGet, caching.OpGetAll:
109 // Always ask for full content.
110 key.Content = true
111
112 case caching.OpConfigSetURL:
113 if err := access.Check(c, origAuthority, key.ConfigSet); err != nil {
114 // Empty "URL" field in value means
115 return &caching.Value{}, nil
116 }
117 }
118
119 // Encode our caching key, and use this for our datastore cache key.
120 //
121 // This gets recoded in dsCacheHandler's "Refresh" to identify the cache
122 // operation that is being performed.
123 encKey, err := caching.Encode(&key)
124 if err != nil {
125 return nil, errors.Annotate(err).Reason("failed to encode cache key").Err()
126 }
127
128 // Construct a cache handler.
129 v, err := cache.Get(dc.withHandler(c, l, 0), encKey)
iannucci 2017/01/07 21:18:36 the datastorecache handler will implicitly do a pr
dnj 2017/01/10 03:32:40 Yes. If the entry is missing, it will be added, an
130 if err != nil {
131 return nil, err
132 }
133
134 // Decode our response.
135 if v.Schema != dsCacheSchema {
136 return nil, errors.Reason("response schema (%(resp)q) doesn't ma tch current (%(cur)q)").
137 D("resp", v.Schema).D("cur", dsCacheSchema).Err()
138 }
139
140 cacheValue, err := caching.DecodeValue(v.Data)
141 if err != nil {
142 return nil, errors.Annotate(err).Reason("failed to decode cached value").Err()
143 }
144
145 // Prune any responses that are not permitted for the supplied Authority .
146 switch key.Op {
147 case caching.OpGet, caching.OpGetAll:
148 if len(cacheValue.Items) > 0 {
149 // Shift over any elements that can't be accessed.
150 ptr := 0
151 for _, itm := range cacheValue.Items {
152 if dc.accessConfigSet(c, origAuthority, itm.Conf igSet) {
153 cacheValue.Items[ptr] = itm
154 ptr++
155 }
156 }
157 cacheValue.Items = cacheValue.Items[:ptr]
158 }
159 }
160
161 return cacheValue, nil
162 }
163
164 func (dc *datastoreCache) accessConfigSet(c context.Context, a config.Authority, configSet string) bool {
165 var cacheMap *map[string]bool
166 switch a {
167 case config.AsService:
168 return true
169 case config.AsUser:
170 cacheMap = &dc.userProjAccess
171 default:
172 cacheMap = &dc.anonProjAccess
173 }
174
175 // If we've already cached this project access, return the cached value.
176 if v, ok := (*cacheMap)[configSet]; ok {
177 return v
178 }
179
180 // Perform a soft access check.
181 canAccess := false
182 switch err := access.Check(c, a, configSet); err {
183 case nil:
184 canAccess = true
185
186 case access.ErrNoAccess:
187 // No access.
188 break
189 case config.ErrNoConfig:
190 log.Fields{
191 "configSet": configSet,
192 }.Debugf(c, "Checking access to project without a config.")
193 default:
194 log.Fields{
195 log.ErrorKey: err,
196 "configSet": configSet,
197 }.Warningf(c, "Error checking for project access.")
198 }
199
200 // Cache the result for future lookups.
201 if *cacheMap == nil {
202 *cacheMap = make(map[string]bool)
203 }
204 (*cacheMap)[configSet] = canAccess
205 return canAccess
206 }
207
208 type dsCacheValue struct {
209 // Key is the cache Key.
210 Key caching.Key `json:"k"`
211 // Value is the cache Value.
212 Value *caching.Value `json:"v"`
213 }
214
215 type dsCacheHandler struct {
216 failOpen bool
217 refreshInterval time.Duration
218 loader caching.Loader
219
220 // loaderTimeout, if >0, will be applied prior to performing the loader
221 // operation. This is used for cron operations.
222 loaderTimeout time.Duration
223 }
224
225 func (dch *dsCacheHandler) FailOpen() bool { return dch.fa ilOpen }
226 func (dch *dsCacheHandler) RefreshInterval([]byte) time.Duration { return dch.re freshInterval }
227
228 func (dch *dsCacheHandler) Refresh(c context.Context, key []byte, v datastorecac he.Value) (datastorecache.Value, error) {
229 // Decode the key into our caching key.
230 var ck caching.Key
231 if err := caching.Decode(key, &ck); err != nil {
232 return v, errors.Annotate(err).Reason("failed to decode cache ke y").Err()
233 }
234
235 var cv *caching.Value
236 if v.Schema == dsCacheSchema && len(v.Data) > 0 {
237 // We have a currently-cached value, so decode it into "cv".
238 var err error
239 if cv, err = caching.DecodeValue(v.Data); err != nil {
240 return v, errors.Annotate(err).Reason("failed to decode cache value").Err()
241 }
242 }
243
244 // Apply our timeout, if configured (influences urlfetch).
245 if dch.loaderTimeout > 0 {
246 var cancelFunc context.CancelFunc
247 c, cancelFunc = clock.WithTimeout(c, dch.loaderTimeout)
248 defer cancelFunc()
249 }
250
251 // Perform a cache load on this value.
252 cv, err := dch.loader(c, ck, cv)
253 if err != nil {
254 return v, errors.Annotate(err).Reason("failed to load cache valu e").Err()
255 }
256
257 // Encode the resulting cache value.
258 if v.Data, err = cv.Encode(); err != nil {
259 return v, errors.Annotate(err).Reason("failed to encode cache va lue").Err()
260 }
261 v.Schema = dsCacheSchema
262 v.Description = ck.String()
263 return v, nil
264 }
265
266 // datastoreCronLoader returns a caching.Loader implementation to be used
267 // by the Cron task.
268 func datastoreCronLoader(b config.Backend) caching.Loader {
269 return func(c context.Context, k caching.Key, v *caching.Value) (*cachin g.Value, error) {
270 return caching.CacheLoad(c, b, k, v)
271 }
272 }
OLDNEW
« no previous file with comments | « appengine/gaeconfig/default.go ('k') | appengine/gaeconfig/ds_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698