| 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 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 } |
| OLD | NEW |