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

Side by Side Diff: luci_config/server/cfgclient/backend/caching/config_test.go

Issue 2573403002: server/config: Generic caching backend. (Closed)
Patch Set: Updated interface, rebased Created 3 years, 11 months 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
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « luci_config/server/cfgclient/backend/caching/config.go ('k') | luci_config/server/cfgclient/backend/caching/doc.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698