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