| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2015 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 caching |
| 6 |
| 7 import ( |
| 8 "encoding/hex" |
| 9 "net/url" |
| 10 "testing" |
| 11 "time" |
| 12 |
| 13 "github.com/luci/luci-go/common/config/impl/memory" |
| 14 "github.com/luci/luci-go/server/config" |
| 15 "github.com/luci/luci-go/server/config/testconfig" |
| 16 |
| 17 "golang.org/x/net/context" |
| 18 |
| 19 . "github.com/smartystreets/goconvey/convey" |
| 20 ) |
| 21 |
| 22 type testCache struct { |
| 23 data map[string][]byte |
| 24 callback func(hit bool) |
| 25 } |
| 26 |
| 27 func (tc *testCache) Store(c context.Context, key string, expire time.Duration,
value []byte) { |
| 28 if tc.data == nil { |
| 29 tc.data = map[string][]byte{} |
| 30 } |
| 31 tc.data[key] = value |
| 32 } |
| 33 |
| 34 func (tc *testCache) Retrieve(c context.Context, key string) []byte { |
| 35 d, ok := tc.data[key] |
| 36 if tc.callback != nil { |
| 37 tc.callback(ok) |
| 38 } |
| 39 return d |
| 40 } |
| 41 |
| 42 func (tc *testCache) invalidate() { |
| 43 tc.data = nil |
| 44 } |
| 45 |
| 46 func (tc *testCache) nothingCached() bool { |
| 47 return len(tc.data) == 0 |
| 48 } |
| 49 |
| 50 type testingBackend struct { |
| 51 config.Backend |
| 52 |
| 53 getContentCalls int |
| 54 getNoContentCalls int |
| 55 |
| 56 err error |
| 57 } |
| 58 |
| 59 func (b *testingBackend) Get(c context.Context, configSet, path string, p config
.Params) (*config.Item, error) { |
| 60 if p.Content { |
| 61 b.getContentCalls++ |
| 62 } else { |
| 63 b.getNoContentCalls++ |
| 64 } |
| 65 |
| 66 if b.err != nil { |
| 67 return nil, b.err |
| 68 } |
| 69 return b.Backend.Get(c, configSet, path, p) |
| 70 } |
| 71 |
| 72 func (b *testingBackend) GetAll(c context.Context, t config.GetAllType, path str
ing, p config.Params) ([]*config.Item, error) { |
| 73 if p.Content { |
| 74 b.getContentCalls++ |
| 75 } else { |
| 76 b.getNoContentCalls++ |
| 77 } |
| 78 |
| 79 if b.err != nil { |
| 80 return nil, b.err |
| 81 } |
| 82 return b.Backend.GetAll(c, t, path, p) |
| 83 } |
| 84 |
| 85 func (b *testingBackend) ConfigSetURL(c context.Context, a config.Authority, con
figSet string) (url.URL, error) { |
| 86 b.getContentCalls++ |
| 87 |
| 88 if b.err != nil { |
| 89 return url.URL{}, b.err |
| 90 } |
| 91 return b.Backend.ConfigSetURL(c, a, configSet) |
| 92 } |
| 93 |
| 94 func (b *testingBackend) reset() { |
| 95 b.getContentCalls = 0 |
| 96 b.getNoContentCalls = 0 |
| 97 } |
| 98 |
| 99 func TestConfig(t *testing.T) { |
| 100 t.Parallel() |
| 101 |
| 102 Convey(`A cache backed by a memory Config`, t, func() { |
| 103 c := context.Background() |
| 104 |
| 105 // Very simple cache. |
| 106 var cache map[string][]byte |
| 107 flushCache := func() { |
| 108 cache = make(map[string][]byte) |
| 109 } |
| 110 flushCache() |
| 111 |
| 112 mbase := map[string]memory.ConfigSet{ |
| 113 "services/foo": { |
| 114 "file": "body", |
| 115 }, |
| 116 "projects/proj1": { |
| 117 "file": "project1 file", |
| 118 }, |
| 119 "projects/goesaway": { |
| 120 "file": "goesaway file", |
| 121 }, |
| 122 "projects/goesaway/refs/heads/master": { |
| 123 "file": "goesaway master ref", |
| 124 }, |
| 125 "projects/goesaway/refs/heads/other": { |
| 126 "file": "goesaway other ref", |
| 127 }, |
| 128 } |
| 129 mconfig := memory.New(mbase) |
| 130 |
| 131 // Install our backend: memory backed by cache backed by force e
rror. |
| 132 // |
| 133 // Cache => Testing => In-Memory |
| 134 var backend config.Backend |
| 135 backend = &config.ClientBackend{ |
| 136 Provider: &testconfig.LocalClientProvider{ |
| 137 Base: mconfig, |
| 138 }, |
| 139 } |
| 140 tb := testingBackend{Backend: backend} |
| 141 backend = &tb |
| 142 |
| 143 metaFor := func(configSet, path string) *config.Meta { |
| 144 cfg, err := mconfig.GetConfig(c, configSet, path, false) |
| 145 if err != nil { |
| 146 panic(err) |
| 147 } |
| 148 return &config.Meta{ |
| 149 ConfigSet: cfg.ConfigSet, |
| 150 Path: cfg.Path, |
| 151 ContentHash: cfg.ContentHash, |
| 152 Revision: cfg.Revision, |
| 153 } |
| 154 } |
| 155 |
| 156 var expired bool |
| 157 backend = &Backend{ |
| 158 Backend: backend, |
| 159 CacheGet: func(c context.Context, k Key, l Loader) (*Val
ue, error) { |
| 160 cacheKey := hex.EncodeToString(k.ParamHash()) |
| 161 |
| 162 var v *Value |
| 163 if d, ok := cache[cacheKey]; ok { |
| 164 dv, err := DecodeValue(d) |
| 165 if err != nil { |
| 166 return nil, err |
| 167 } |
| 168 if !expired { |
| 169 return dv, nil |
| 170 } |
| 171 |
| 172 v = dv |
| 173 } |
| 174 |
| 175 v, err := l(c, k, v) |
| 176 if err != nil { |
| 177 return nil, err |
| 178 } |
| 179 d, err := v.Encode() |
| 180 if err != nil { |
| 181 panic(err) |
| 182 } |
| 183 cache[cacheKey] = d |
| 184 return v, nil |
| 185 }, |
| 186 } |
| 187 |
| 188 c = config.WithBackend(c, backend) |
| 189 |
| 190 // Advance underlying config, expectation. |
| 191 advance := func() { |
| 192 mbase["services/foo"]["file"] = "body2" |
| 193 mbase["services/foo"]["late"] = "late config" |
| 194 mbase["projects/showsup"] = memory.ConfigSet{ |
| 195 "file": "shows up", |
| 196 } |
| 197 delete(mbase, "projects/goesaway") |
| 198 delete(mbase, "projects/goesaway/refs/heads/master") |
| 199 delete(mbase, "projects/goesaway/refs/heads/other") |
| 200 } |
| 201 |
| 202 Convey(`Get`, func() { |
| 203 Convey(`Get works, caches, invalidates.`, func() { |
| 204 var s string |
| 205 So(config.Get(c, config.AsService, "services/foo
", "file", config.String(&s), nil), ShouldBeNil) |
| 206 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 207 So(tb.getContentCalls, ShouldEqual, 1) |
| 208 So(s, ShouldEqual, "body") |
| 209 |
| 210 // The value should now be cached. |
| 211 s = "" |
| 212 So(config.Get(c, config.AsService, "services/foo
", "file", config.String(&s), nil), ShouldBeNil) |
| 213 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 214 So(tb.getContentCalls, ShouldEqual, 1) // (Uncha
nged) |
| 215 So(s, ShouldEqual, "body") |
| 216 |
| 217 // Expire content. Should do one Get w/out conte
nt, see no change, and |
| 218 // be done. |
| 219 expired = true |
| 220 So(config.Get(c, config.AsService, "services/foo
", "file", config.String(&s), nil), ShouldBeNil) |
| 221 So(tb.getNoContentCalls, ShouldEqual, 1) |
| 222 So(tb.getContentCalls, ShouldEqual, 1) // (Uncha
nged) |
| 223 So(s, ShouldEqual, "body") |
| 224 |
| 225 // Backing config changes, but not expired. |
| 226 advance() |
| 227 expired = false |
| 228 |
| 229 So(config.Get(c, config.AsService, "services/foo
", "file", config.String(&s), nil), ShouldBeNil) |
| 230 So(tb.getNoContentCalls, ShouldEqual, 1) // (Unc
hanged) |
| 231 So(tb.getContentCalls, ShouldEqual, 1) // (Unc
hanged) |
| 232 So(s, ShouldEqual, "body") // Real
one is "body2", but we load from cache. |
| 233 |
| 234 // Expire local config, does full reload on hash
difference. |
| 235 expired = true |
| 236 So(config.Get(c, config.AsService, "services/foo
", "file", config.String(&s), nil), ShouldBeNil) |
| 237 So(tb.getNoContentCalls, ShouldEqual, 2) |
| 238 So(tb.getContentCalls, ShouldEqual, 2) |
| 239 So(s, ShouldEqual, "body2") |
| 240 }) |
| 241 |
| 242 Convey(`Get w/ missing entry caches the miss.`, func() { |
| 243 // Get missing entry. |
| 244 var s string |
| 245 So(config.Get(c, config.AsService, "services/foo
", "late", config.String(&s), nil), ShouldEqual, config.ErrNoConfig) |
| 246 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 247 So(tb.getContentCalls, ShouldEqual, 1) |
| 248 |
| 249 // Entry is still gone (cached). |
| 250 So(config.Get(c, config.AsService, "services/foo
", "late", config.String(&s), nil), ShouldEqual, config.ErrNoConfig) |
| 251 So(tb.getNoContentCalls, ShouldEqual, 0) // (Unc
hanged) |
| 252 So(tb.getContentCalls, ShouldEqual, 1) // (Unc
hanged) |
| 253 |
| 254 // Entry comes into existence, but still cached
as gone. |
| 255 advance() |
| 256 |
| 257 So(config.Get(c, config.AsService, "services/foo
", "late", config.String(&s), nil), ShouldEqual, config.ErrNoConfig) |
| 258 So(tb.getNoContentCalls, ShouldEqual, 0) // (Unc
hanged) |
| 259 So(tb.getContentCalls, ShouldEqual, 1) // (Unc
hanged) |
| 260 |
| 261 // Cache expires, entry content is loaded. |
| 262 expired = true |
| 263 So(config.Get(c, config.AsService, "services/foo
", "late", config.String(&s), nil), ShouldBeNil) |
| 264 So(tb.getNoContentCalls, ShouldEqual, 0) // (Unc
hanged) |
| 265 So(tb.getContentCalls, ShouldEqual, 2) |
| 266 So(s, ShouldEqual, "late config") |
| 267 |
| 268 // Entry disappears, re-caches as missing throug
h no content load. |
| 269 expired = true |
| 270 delete(mbase["services/foo"], "late") |
| 271 |
| 272 So(config.Get(c, config.AsService, "services/foo
", "late", config.String(&s), nil), ShouldEqual, config.ErrNoConfig) |
| 273 So(tb.getNoContentCalls, ShouldEqual, 1) |
| 274 So(tb.getContentCalls, ShouldEqual, 2) // (Uncha
nged) |
| 275 }) |
| 276 }) |
| 277 |
| 278 Convey(`GetAll`, func() { |
| 279 Convey(`Successfully loads, caches, refreshes projects.`
, func() { |
| 280 origMetas := []*config.Meta{ |
| 281 metaFor("projects/goesaway", "file"), |
| 282 metaFor("projects/proj1", "file"), |
| 283 } |
| 284 |
| 285 // Load all successfully. |
| 286 var s []string |
| 287 var meta []*config.Meta |
| 288 So(config.GetAll(c, config.AsService, config.Pro
ject, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 289 So(s, ShouldResemble, []string{"goesaway file",
"project1 file"}) |
| 290 So(meta, ShouldResemble, origMetas) |
| 291 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 292 So(tb.getContentCalls, ShouldEqual, 1) |
| 293 |
| 294 // Expire the cache, reloads, same entries, no c
ontent only. |
| 295 expired = true |
| 296 So(config.GetAll(c, config.AsService, config.Pro
ject, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 297 So(s, ShouldResemble, []string{"goesaway file",
"project1 file"}) |
| 298 So(meta, ShouldResemble, origMetas) |
| 299 So(tb.getNoContentCalls, ShouldEqual, 1) |
| 300 So(tb.getContentCalls, ShouldEqual, 1) // (Uncha
nged) |
| 301 |
| 302 // Advance, "projects/goesaway" goes away, still
loads all successfully |
| 303 // (cache). |
| 304 expired = false |
| 305 advance() |
| 306 |
| 307 So(config.GetAll(c, config.AsService, config.Pro
ject, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 308 So(s, ShouldResemble, []string{"goesaway file",
"project1 file"}) |
| 309 So(meta, ShouldResemble, origMetas) |
| 310 So(tb.getNoContentCalls, ShouldEqual, 1) // (Unc
hanged) |
| 311 So(tb.getContentCalls, ShouldEqual, 1) // (Unc
hanged) |
| 312 |
| 313 // Expire the cache, reloads, notices missing en
try (count same), reloads. |
| 314 expired = true |
| 315 |
| 316 So(config.GetAll(c, config.AsService, config.Pro
ject, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 317 So(s, ShouldResemble, []string{"project1 file",
"shows up"}) |
| 318 So(meta, ShouldResemble, []*config.Meta{ |
| 319 metaFor("projects/proj1", "file"), |
| 320 metaFor("projects/showsup", "file"), |
| 321 }) |
| 322 So(tb.getNoContentCalls, ShouldEqual, 2) |
| 323 So(tb.getContentCalls, ShouldEqual, 2) |
| 324 |
| 325 // Expire the cache, reloads, notices missing en
try (count differs), |
| 326 // reloads. |
| 327 delete(mbase, "projects/showsup") |
| 328 expired = true |
| 329 |
| 330 So(config.GetAll(c, config.AsService, config.Pro
ject, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 331 So(s, ShouldResemble, []string{"project1 file"}) |
| 332 So(meta, ShouldResemble, []*config.Meta{ |
| 333 metaFor("projects/proj1", "file"), |
| 334 }) |
| 335 So(tb.getNoContentCalls, ShouldEqual, 3) |
| 336 So(tb.getContentCalls, ShouldEqual, 3) |
| 337 }) |
| 338 |
| 339 Convey(`Works with refs too.`, func() { |
| 340 origMetas := []*config.Meta{ |
| 341 metaFor("projects/goesaway/refs/heads/ma
ster", "file"), |
| 342 metaFor("projects/goesaway/refs/heads/ot
her", "file"), |
| 343 } |
| 344 |
| 345 // Load all successfully. |
| 346 var s []string |
| 347 var meta []*config.Meta |
| 348 |
| 349 So(config.GetAll(c, config.AsService, config.Ref
, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 350 So(s, ShouldResemble, []string{"goesaway master
ref", "goesaway other ref"}) |
| 351 So(meta, ShouldResemble, origMetas) |
| 352 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 353 So(tb.getContentCalls, ShouldEqual, 1) // (Uncha
nged) |
| 354 |
| 355 // Delete project, entries still cached. |
| 356 advance() |
| 357 |
| 358 So(config.GetAll(c, config.AsService, config.Ref
, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 359 So(s, ShouldResemble, []string{"goesaway master
ref", "goesaway other ref"}) |
| 360 So(meta, ShouldResemble, origMetas) |
| 361 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 362 So(tb.getContentCalls, ShouldEqual, 1) // (Uncha
nged) |
| 363 |
| 364 // Expire the cache, reloads, same entries, no c
ontent only. |
| 365 expired = true |
| 366 So(config.GetAll(c, config.AsService, config.Ref
, "file", config.StringSlice(&s), &meta), ShouldBeNil) |
| 367 So(s, ShouldResemble, []string(nil)) |
| 368 So(tb.getNoContentCalls, ShouldEqual, 1) |
| 369 So(tb.getContentCalls, ShouldEqual, 2) // (Uncha
nged) |
| 370 }) |
| 371 |
| 372 Convey(`Handles no entries.`, func() { |
| 373 var s []string |
| 374 So(config.GetAll(c, config.AsService, config.Ref
, "none", config.StringSlice(&s), nil), ShouldBeNil) |
| 375 So(s, ShouldResemble, []string(nil)) |
| 376 So(tb.getNoContentCalls, ShouldEqual, 0) |
| 377 So(tb.getContentCalls, ShouldEqual, 1) |
| 378 }) |
| 379 }) |
| 380 |
| 381 Convey(`GetConfigSetURL`, func() { |
| 382 u, err := config.GetConfigSetURL(c, config.AsService, "p
rojects/goesaway") |
| 383 So(err, ShouldBeNil) |
| 384 So(u, ShouldResemble, url.URL{Scheme: "https", Host: "ex
ample.com", Path: "/fake-config/projects/goesaway"}) |
| 385 So(tb.getContentCalls, ShouldEqual, 1) |
| 386 |
| 387 // Delete project, entries still cached. |
| 388 advance() |
| 389 |
| 390 u, err = config.GetConfigSetURL(c, config.AsService, "pr
ojects/goesaway") |
| 391 So(err, ShouldBeNil) |
| 392 So(u, ShouldResemble, url.URL{Scheme: "https", Host: "ex
ample.com", Path: "/fake-config/projects/goesaway"}) |
| 393 So(tb.getContentCalls, ShouldEqual, 1) // (Unchanged) |
| 394 |
| 395 // Expire the cache, ErrNoConfig. |
| 396 expired = true |
| 397 _, err = config.GetConfigSetURL(c, config.AsService, "pr
ojects/goesaway") |
| 398 So(err, ShouldEqual, config.ErrNoConfig) |
| 399 So(tb.getContentCalls, ShouldEqual, 2) // Reload on expi
re. |
| 400 |
| 401 // Retains "missing" cache entry. |
| 402 expired = false |
| 403 _, err = config.GetConfigSetURL(c, config.AsService, "pr
ojects/goesaway") |
| 404 So(err, ShouldEqual, config.ErrNoConfig) |
| 405 So(tb.getContentCalls, ShouldEqual, 2) // (Unchanged) |
| 406 }) |
| 407 }) |
| 408 } |
| OLD | NEW |