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

Side by Side Diff: luci_config/server/cfgclient/backend/caching/config.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 "fmt"
9 "net/url"
10
11 "github.com/luci/luci-go/common/errors"
12 log "github.com/luci/luci-go/common/logging"
13 "github.com/luci/luci-go/luci_config/server/cfgclient"
14 "github.com/luci/luci-go/luci_config/server/cfgclient/backend"
15
16 "golang.org/x/net/context"
17 )
18
19 // Schema is the current package's cache schema.
20 const Schema = "v2"
21
22 // Operation is a cache entry operation. Cache entries are all stored in the
23 // same object, with different parameters filled based on the operation that
24 // they represent.
25 type Operation string
26
27 const (
28 // OpGet is the Get operation.
29 OpGet = Operation("Get")
30 // OpGetAll is the GetAll operation.
31 OpGetAll = Operation("GetAll")
32 // OpConfigSetURL is the ConfigSetURL operation.
33 OpConfigSetURL = Operation("ConfigSetURL")
34 )
35
36 // Key is a cache key.
37 type Key struct {
38 // Schema is the schema for this key/value. If schema changes in a backw ards-
39 // incompatible way, this must also change.
40 Schema string `json:"s,omitempty"`
41
42 // Authority is the config authority to use.
43 Authority backend.Authority `json:"a,omitempty"`
44
45 // Op is the operation that is being cached.
46 Op Operation `json:"op,omitempty"`
47
48 // Content is true if this request asked for content.
49 Content bool `json:"c,omitempty"`
50
51 // Formatter is the requesting Key's Formatter parameter, expanded from its
52 // FormatSpec.
53 Formatter string `json:"f,omitempty"`
54 // FormatData is the requesting Key's format Data parameter, expanded fr om its
55 // FormatSpec.
56 FormatData string `json:"fd,omitempty"`
57
58 // ConfigSet is the config set parameter. This is valid for "OpGet".
59 ConfigSet string `json:"cs,omitempty"`
60 // Path is the path parameters. This is valid for "OpGet" and "OpGetAll" .
61 Path string `json:"p,omitempty"`
62
63 // GetAllTarget is the "GetAll" operation type. This is valid for "OpGet All".
64 GetAllTarget backend.GetAllTarget `json:"gat,omitempty"`
65 }
66
67 // ParamHash returns a deterministic hash of all of the key parameters.
68 func (k *Key) ParamHash() []byte {
69 cstr := ""
70 if k.Content {
71 cstr = "y"
72 }
73 return HashParams(k.Schema, string(k.Authority), string(k.Op), cstr, k.F ormatter, k.FormatData,
74 k.ConfigSet, k.Path, string(k.GetAllTarget))
75 }
76
77 // String prints a text representation of the key. No effort is made to ensure
78 // that this representation is consistent or deterministic, and it is not
79 // bound to the cache schema.
80 func (k *Key) String() string { return fmt.Sprintf("%#v", k) }
81
82 // Params returns the backend.Params that are encoded in this Key.
83 func (k *Key) Params() backend.Params {
84 return backend.Params{
85 Content: k.Content,
86 Authority: k.Authority,
87 FormatSpec: backend.FormatSpec{k.Formatter, k.FormatData},
88 }
89 }
90
91 // ValueItem is a cache-optimized backend.Item projection.
92 //
93 // This will mostly be the same as a backend.Item, with the exception of
94 // Content, which will hold the formatted value if it has been formatted.
95 //
96 // See Formatter in
97 // github.com/luci/luci-go/luci_config/server/cfgclient
98 // and Backend in
99 // github.com/luci/luci-go/luci_config/server/cfgclient.backend/format
100 // for more information.
101 type ValueItem struct {
102 ConfigSet string `json:"cs,omitempty"`
103 Path string `json:"p,omitempty"`
104
105 ContentHash string `json:"ch,omitempty"`
106 Revision string `json:"r,omitempty"`
107
108 Content []byte `json:"c,omitempty"`
109
110 Formatter string `json:"f,omitempty"`
111 FormatData []byte `json:"fd,omitempty"`
112 }
113
114 // MakeValueItem builds a caching ValueItem from a backend.Item.
115 func MakeValueItem(it *backend.Item) ValueItem {
116 return ValueItem{
117 ConfigSet: it.ConfigSet,
118 Path: it.Path,
119 ContentHash: it.ContentHash,
120 Revision: it.Revision,
121 Content: []byte(it.Content),
122 Formatter: it.FormatSpec.Formatter,
123 FormatData: []byte(it.FormatSpec.Data),
124 }
125 }
126
127 // ConfigItem returns the backend.Item equivalent of vi.
128 func (vi *ValueItem) ConfigItem() *backend.Item {
129 return &backend.Item{
130 Meta: backend.Meta{
131 ConfigSet: vi.ConfigSet,
132 Path: vi.Path,
133 ContentHash: vi.ContentHash,
134 Revision: vi.Revision,
135 },
136 Content: string(vi.Content),
137 FormatSpec: backend.FormatSpec{vi.Formatter, string(vi.FormatDat a)},
138 }
139 }
140
141 // Value is a cache value.
142 type Value struct {
143 // Items is the cached set of config response items.
144 //
145 // For Get, this will either be empty (cached miss) or have a single Ite m
146 // in it (cache hit).
147 Items []ValueItem `json:"i,omitempty"`
148
149 // URL is a URL string.
150 //
151 // Used with GetConfigSetURL.
152 URL string `json:"u,omitempty"`
153 }
154
155 // LoadItems loads a set of backend.Item into v's Items field. If items is nil,
156 // v.Items will be nil.
157 func (v *Value) LoadItems(items ...*backend.Item) {
158 if len(items) == 0 {
159 v.Items = nil
160 return
161 }
162
163 v.Items = make([]ValueItem, len(items))
164 for i, it := range items {
165 v.Items[i] = MakeValueItem(it)
166 }
167 }
168
169 // SingleItem returns the first backend.Item in v's Items slice. If the Items
170 // slice is empty, SingleItem will return nil.
171 func (v *Value) SingleItem() *backend.Item {
172 if len(v.Items) == 0 {
173 return nil
174 }
175 return v.Items[0].ConfigItem()
176 }
177
178 // ConfigItems returns the backend.Item projection of v's Items slice.
179 func (v *Value) ConfigItems() []*backend.Item {
180 if len(v.Items) == 0 {
181 return nil
182 }
183
184 res := make([]*backend.Item, len(v.Items))
185 for i := range v.Items {
186 res[i] = v.Items[i].ConfigItem()
187 }
188 return res
189 }
190
191 // DecodeValue loads a Value from is encoded representation.
192 func DecodeValue(d []byte) (*Value, error) {
193 var v Value
194 if err := Decode(d, &v); err != nil {
195 return nil, errors.Annotate(err).Err()
196 }
197 return &v, nil
198 }
199
200 // Encode encodes this Value.
201 //
202 // This is offered for convenience, but caches aren't required to use this
203 // encoding.
204 //
205 // The format stores the Value as compressed JSON.
206 func (v *Value) Encode() ([]byte, error) { return Encode(v) }
207
208 // Loader retrieves a Value by consulting the backing backend.
209 //
210 // The input Value is the current cached Value, or nil if there is no current
211 // cached Value. The output Value is the cached Value, if one exists. It is
212 // acceptable to return mutate "v" and/or return it as the output Value.
213 type Loader func(context.Context, Key, *Value) (*Value, error)
214
215 // Backend is a backend.B implementation that caches responses.
216 //
217 // All cached values are full-content regardless of whether or not full content
218 // was requested.
219 //
220 // Backend caches content and no-content requests as separate cache entries.
221 // This enables one cache to do low-overhead updating against another cache
222 // implementation.
223 type Backend struct {
224 // Backend is the backing Backend.
225 backend.B
226
227 // FailOnError, if true, means that a failure to retrieve a cached item will
228 // be propagated to the caller immediately. If false, a cache error will
229 // result in a direct fall-through call to the embedded backend.B.
230 //
231 // One might set FailOnError to true if the fallback is unacceptably slo w, or
232 // if they want to enforce caching via failure.
233 FailOnError bool
234
235 // CacheGet retrieves the cached value associated with key. If no such c ached
236 // value exists, CacheGet is responsible for resolving the cache value u sing
237 // the supplied Loader.
238 CacheGet func(context.Context, Key, Loader) (*Value, error)
239 }
240
241 // Get implements backend.B.
242 func (b *Backend) Get(c context.Context, configSet, path string, p backend.Param s) (*backend.Item, error) {
243 key := Key{
244 Schema: Schema,
245 Authority: p.Authority,
246 Op: OpGet,
247 Content: p.Content,
248 ConfigSet: configSet,
249 Path: path,
250 Formatter: p.FormatSpec.Formatter,
251 FormatData: p.FormatSpec.Data,
252 }
253 value, err := b.CacheGet(c, key, b.loader)
254 if err != nil {
255 if b.FailOnError {
256 log.Fields{
257 log.ErrorKey: err,
258 "authority": p.Authority,
259 "configSet": configSet,
260 "path": path,
261 }.Errorf(c, "(Hard Failure) failed to load cache value." )
262 return nil, errors.Annotate(err).Err()
263 }
264
265 log.Fields{
266 log.ErrorKey: err,
267 "authority": p.Authority,
268 "configSet": configSet,
269 "path": path,
270 }.Warningf(c, "Failed to load cache value.")
271 return b.B.Get(c, configSet, path, p)
272 }
273
274 it := value.SingleItem()
275 if it == nil {
276 // Sentinel for no config.
277 return nil, cfgclient.ErrNoConfig
278 }
279 return it, nil
280 }
281
282 // GetAll implements config.Backend.
283 func (b *Backend) GetAll(c context.Context, t backend.GetAllTarget, path string, p backend.Params) ([]*backend.Item, error) {
284 key := Key{
285 Schema: Schema,
286 Authority: p.Authority,
287 Op: OpGetAll,
288 Content: p.Content,
289 Path: path,
290 GetAllTarget: t,
291 Formatter: p.FormatSpec.Formatter,
292 FormatData: p.FormatSpec.Data,
293 }
294 value, err := b.CacheGet(c, key, b.loader)
295 if err != nil {
296 if b.FailOnError {
297 log.Fields{
298 log.ErrorKey: err,
299 "authority": p.Authority,
300 "type": t,
301 "path": path,
302 }.Errorf(c, "(Hard Failure) failed to load cache value." )
303 return nil, errors.Annotate(err).Err()
304 }
305
306 log.Fields{
307 log.ErrorKey: err,
308 "authority": p.Authority,
309 "type": t,
310 "path": path,
311 }.Warningf(c, "Failed to load cache value.")
312 return b.B.GetAll(c, t, path, p)
313 }
314 return value.ConfigItems(), nil
315 }
316
317 // ConfigSetURL implements backend.B.
318 func (b *Backend) ConfigSetURL(c context.Context, configSet string, p backend.Pa rams) (u url.URL, err error) {
319 key := Key{
320 Schema: Schema,
321 Authority: p.Authority,
322 Op: OpConfigSetURL,
323 ConfigSet: configSet,
324 }
325
326 var value *Value
327 if value, err = b.CacheGet(c, key, b.loader); err != nil {
328 if b.FailOnError {
329 log.Fields{
330 log.ErrorKey: err,
331 "authority": p.Authority,
332 "configSet": configSet,
333 }.Errorf(c, "(Hard Failure) failed to load cache value." )
334 err = errors.Annotate(err).Err()
335 return
336 }
337
338 log.Fields{
339 log.ErrorKey: err,
340 "authority": p.Authority,
341 "configSet": configSet,
342 }.Warningf(c, "Failed to load cache value.")
343 return b.B.ConfigSetURL(c, configSet, p)
344 }
345
346 if value.URL == "" {
347 // Sentinel for no config.
348 err = cfgclient.ErrNoConfig
349 return
350 }
351
352 up, err := url.Parse(value.URL)
353 if err != nil {
354 err = errors.Annotate(err).Reason("failed to parse cached URL: % (value)q").D("value", value.URL).Err()
355 return
356 }
357
358 u = *up
359 return
360 }
361
362 // loader runs a cache get against the configured Base backend.
363 //
364 // This should be used by caches that do not have the cached value.
365 func (b *Backend) loader(c context.Context, k Key, v *Value) (*Value, error) {
366 return CacheLoad(c, b.B, k, v)
367 }
368
369 // CacheLoad loads k from backend b.
370 //
371 // If an existing cache value is known, it should be supplied as v. Otherwise,
372 // v should be nil.
373 //
374 // This is effectively a Loader function that is detached from a given cache
375 // instance.
376 func CacheLoad(c context.Context, b backend.B, k Key, v *Value) (rv *Value, err error) {
377 switch k.Op {
378 case OpGet:
379 rv, err = doGet(c, b, k.ConfigSet, k.Path, v, k.Params())
380 case OpGetAll:
381 rv, err = doGetAll(c, b, k.GetAllTarget, k.Path, v, k.Params())
382 case OpConfigSetURL:
383 rv, err = doConfigSetURL(c, b, k.ConfigSet, k.Params())
384 default:
385 return nil, errors.Reason("unknown operation: %(op)v").D("op", k .Op).Err()
386 }
387 if err != nil {
388 return nil, err
389 }
390 return
391 }
392
393 func doGet(c context.Context, b backend.B, configSet, path string, v *Value, p b ackend.Params) (*Value, error) {
394 hadItem := (v != nil && len(v.Items) > 0)
395 if !hadItem {
396 // Initialize empty "v".
397 v = &Value{}
398 }
399
400 // If we have a current item, or if we are requesting "no-content", then
401 // perform a "no-content" lookup.
402 if hadItem || !p.Content {
403 noContentP := p
404 noContentP.Content = false
405
406 item, err := b.Get(c, configSet, path, noContentP)
407 switch err {
408 case nil:
409 if hadItem && (item.ContentHash == v.Items[0].ContentHas h) {
410 // Nothing changed.
411 return v, nil
412 }
413
414 // If our "Get" is, itself, no-content, then this is our actual result.
415 if !p.Content {
416 v.LoadItems(item)
417 return v, nil
418 }
419
420 // If we get here, we are requesting full-content and ou r hash check
421 // showed that the current content has a different hash.
422 break
423
424 case cfgclient.ErrNoConfig:
425 v.LoadItems()
426 return v, nil
427
428 default:
429 return nil, errors.Annotate(err).Err()
430 }
431 }
432
433 // Perform a full content request.
434 switch item, err := b.Get(c, configSet, path, p); err {
435 case nil:
436 v.LoadItems(item)
437 return v, nil
438
439 case cfgclient.ErrNoConfig:
440 // Empty "config missing" item.
441 v.LoadItems()
442 return v, nil
443
444 default:
445 return nil, errors.Annotate(err).Err()
446 }
447 }
448
449 func doGetAll(c context.Context, b backend.B, t backend.GetAllTarget, path strin g, v *Value, p backend.Params) (
450 *Value, error) {
451
452 // If we already have a cached value, or if we're requesting no-content, do a
453 // no-content refresh to see if anything has changed.
454 //
455 // Response values are in order, so this is a simple traversal.
456 if v != nil || !p.Content {
457 noContentP := p
458 noContentP.Content = false
459
460 items, err := b.GetAll(c, t, path, noContentP)
461 if err != nil {
462 return nil, errors.Annotate(err).Reason("failed RPC (has h-only)").Err()
463 }
464
465 // If we already have a cached item, validate it.
466 if v != nil && len(items) == len(v.Items) {
467 match := true
468 for i, other := range items {
469 cur := v.Items[i]
470 if cur.ConfigSet == other.ConfigSet && cur.Path == other.Path &&
471 cur.ContentHash == other.ContentHash {
472 continue
473 }
474
475 match = false
476 break
477 }
478
479 // If all configs match, our response hasn't changed.
480 if match {
481 return v, nil
482 }
483 }
484
485 // If we requested no-content, then this is our result.
486 if !p.Content {
487 var retV Value
488 retV.LoadItems(items...)
489 return &retV, nil
490 }
491 }
492
493 // Perform a full-content request.
494 items, err := b.GetAll(c, t, path, p)
495 if err != nil {
496 return nil, errors.Annotate(err).Err()
497 }
498 var retV Value
499 retV.LoadItems(items...)
500 return &retV, nil
501 }
502
503 func doConfigSetURL(c context.Context, b backend.B, configSet string, p backend. Params) (*Value, error) {
504 u, err := b.ConfigSetURL(c, configSet, p)
505 switch err {
506 case nil:
507 return &Value{
508 URL: u.String(),
509 }, nil
510
511 case cfgclient.ErrNoConfig:
512 return &Value{}, nil
513
514 default:
515 return nil, err
516 }
517 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698