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