Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 } | |
| OLD | NEW |