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