Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 // Package lazyslot implements a caching scheme for globally shared objects that | 5 // Package lazyslot implements a caching scheme for globally shared objects that |
| 6 // take significant time to refresh. The defining property of the implementation | 6 // take significant time to refresh. |
| 7 // is that only one goroutine (can be background one) will block when refreshing | 7 // |
| 8 // such object, while all others will use a slightly stale cached copy. | 8 // The defining property of the implementation is that only one goroutine |
| 9 // (can be background one) will block when refreshing such object, while all | |
| 10 // others will use a slightly stale cached copy. | |
| 9 package lazyslot | 11 package lazyslot |
| 10 | 12 |
| 11 import ( | 13 import ( |
| 14 "fmt" | |
| 12 "sync" | 15 "sync" |
| 13 "time" | 16 "time" |
| 14 | 17 |
| 15 "golang.org/x/net/context" | 18 "golang.org/x/net/context" |
| 16 | 19 |
| 17 "github.com/luci/luci-go/common/clock" | 20 "github.com/luci/luci-go/common/clock" |
| 18 ) | 21 ) |
| 19 | 22 |
| 20 // Value is what's stored in a Slot. It is treated as immutable value. | 23 // Value is what's stored in a Slot. It is treated as immutable value. |
| 21 type Value struct { | 24 type Value struct { |
| 22 // Value is whatever fetcher returned. | 25 // Value is whatever fetcher returned. |
| 23 Value interface{} | 26 Value interface{} |
| 24 // Expiration is time when this value expires and should be refetched. | 27 // Expiration is time when this value expires and should be refetched. |
| 25 Expiration time.Time | 28 Expiration time.Time |
| 26 } | 29 } |
| 27 | 30 |
| 28 // Fetcher knows how to load new value. | 31 // Fetcher knows how to load a new value. |
| 32 // | |
| 33 // If it returns no errors, it MUST return non-nil Value.Value or Slot.Get will | |
| 34 // return error. | |
| 35 // | |
| 36 // Panics inside Fetcher will be caught and converted to errors. | |
|
iannucci
2016/03/01 00:42:41
ugh.. can we just let panics crash the program? I
Vadim Sh.
2016/03/01 01:11:26
Okay. It used to work like that. Reverted.
dnj
2016/03/01 01:21:45
+1
| |
| 29 type Fetcher func(c context.Context, prev Value) (Value, error) | 37 type Fetcher func(c context.Context, prev Value) (Value, error) |
| 30 | 38 |
| 31 // Slot holds a cached Value and refreshes it when it expires. Only one | 39 // Slot holds a cached Value and refreshes it when it expires. |
| 32 // goroutine will be busy refreshing, all others will see a slightly stale | 40 // |
| 33 // copy of the value during the refresh. | 41 // Only one goroutine will be busy refreshing, all others will see a slightly |
| 42 // stale copy of the value during the refresh. | |
| 34 type Slot struct { | 43 type Slot struct { |
| 35 Fetcher Fetcher // used to actually load the value on demand | 44 Fetcher Fetcher // used to actually load the value on demand |
| 36 Timeout time.Duration // how long to allow to fetch, 5 sec by default. | 45 Timeout time.Duration // how long to allow to fetch, 5 sec by default. |
| 37 Async bool // if true do fetches in background goroutine | 46 Async bool // if true do fetches in background goroutine |
|
iannucci
2016/03/01 00:42:41
So, reading the go memory model, I don't think bac
Vadim Sh.
2016/03/01 01:11:26
async mode is not used currently, removed it until
| |
| 38 | 47 |
| 39 » lock sync.Mutex // protects the guts below | 48 » lock sync.Mutex // protects the guts below |
| 40 » current *Value // currently known value or nil if not fe tched | 49 » current *Value // currently known value or nil if not fetched |
| 41 » currentFetcher context.Context // non nil if some goroutine is fetching now | 50 » currentFetcherCtx context.Context // non-nil if some goroutine is fetchi ng now |
| 42 } | 51 } |
| 43 | 52 |
| 44 // Peek returns currently cached value if there's one or zero Value{} if not. | 53 // Get returns stored value if it is still fresh. |
| 45 // It doesn't try to fetch a value. | 54 // |
| 46 func (s *Slot) Peek() Value { | 55 // It may return slightly stale copy if some other goroutine is fetching a new |
|
Vadim Sh.
2016/02/29 22:38:28
it wasn't used outside tests and was racy, so I re
| |
| 47 » if s.current == nil { | 56 // copy now. If there's no cached copy at all, blocks until it is retrieved |
| 48 » » return Value{} | 57 // (even if the slot is configured with Async = true). |
| 49 » } | 58 // |
| 50 » return *s.current | 59 // Returns an error only when Fetcher returns an error. If no error is returned, |
| 51 } | 60 // Value.Value is guaranteed to be non-nil. |
| 52 | 61 func (s *Slot) Get(c context.Context) (result Value, err error) { |
| 53 // Get returns stored value if it is still fresh. It may return slightly stale | |
| 54 // copy if some other goroutine is fetching a new copy now. If there's no cached | |
| 55 // copy at all, blocks until it is retrieved (even if slot is configured with | |
| 56 // Async = true). Returns an error only when Fetcher returns an error. | |
| 57 func (s *Slot) Get(c context.Context) (Value, error) { | |
| 58 now := clock.Now(c) | 62 now := clock.Now(c) |
| 59 | 63 |
| 60 » // Set in the local function below, used it fetch is needed. | 64 » // This lock protects the guts of the slot and makes sure only one gorou tine |
| 61 » var ( | 65 » // is doing an initial fetch. It is released explicitly before returns ( and |
| 62 » » ctx context.Context | 66 » // not via defer) to simplify code path that needs to release the lock b efore |
| 63 » » fetchCb Fetcher | 67 » // exiting the function. |
|
iannucci
2016/03/01 00:42:41
why not just have a sub-function do this?
rslt, o
Vadim Sh.
2016/03/01 01:11:26
It used to be like that and I found it hard to rea
| |
| 64 » » prevVal Value | 68 » s.lock.Lock() |
| 65 » » async bool | |
| 66 » ) | |
| 67 | 69 |
| 68 » // If done is true, val and err are returned right away. | 70 » // A cached value exists and it is still fresh? Return it right away. |
| 69 » done, val, err := func() (bool, Value, error) { | 71 » if s.current != nil && now.Before(s.current.Expiration) { |
| 70 » » s.lock.Lock() | 72 » » result = *s.current |
| 71 » » defer s.lock.Unlock() | 73 » » s.lock.Unlock() |
| 72 | 74 » » return |
| 73 » » // Still fresh? Return right away. | |
| 74 » » if s.current != nil && now.Before(s.current.Expiration) { | |
| 75 » » » return true, *s.current, nil | |
| 76 » » } | |
| 77 | |
| 78 » » // Fetching the value for the first time ever? Do it under the l ock because | |
| 79 » » // there's nothing to return yet. All goroutines would have to w ait for this | |
| 80 » » // initial fetch to complete. They'll all block on s.lock.Lock() above. | |
| 81 » » if s.current == nil { | |
| 82 » » » val, err := s.Fetcher(c, Value{}) | |
| 83 » » » if err != nil { | |
| 84 » » » » return true, Value{}, err | |
| 85 » » » } | |
| 86 » » » s.current = &val | |
| 87 » » » return true, val, nil | |
| 88 » » } | |
| 89 | |
| 90 » » // We have a cached copy and it has expired. Maybe some other go routine is | |
| 91 » » // fetching it already? Returns the stale copy if so. | |
| 92 » » if s.currentFetcher != nil { | |
| 93 » » » return true, *s.current, nil | |
| 94 » » } | |
| 95 | |
| 96 » » // No one is fetching the value now, we should do it. Release th e lock while | |
| 97 » » // fetching to allow other goroutines to grab the stale copy. | |
| 98 » » timeout := 5 * time.Second | |
| 99 » » if s.Timeout != 0 { | |
| 100 » » » timeout = s.Timeout | |
| 101 » » } | |
| 102 » » s.currentFetcher, _ = context.WithTimeout(c, timeout) | |
| 103 » » ctx = s.currentFetcher | |
| 104 » » fetchCb = s.Fetcher | |
| 105 » » prevVal = *s.current | |
| 106 » » async = s.Async | |
| 107 » » return false, Value{}, nil | |
| 108 » }() | |
| 109 » if done { | |
| 110 » » return val, err | |
| 111 } | 75 } |
| 112 | 76 |
| 113 » fetch := func() (val Value, err error) { | 77 » // Fetching the value for the first time ever? Do it under the lock beca use |
| 78 » // there's nothing to return yet. All goroutines would have to wait for this | |
| 79 » // initial fetch to complete. They'll all block on s.lock.Lock() above. | |
| 80 » if s.current == nil { | |
| 81 » » result, err = doFetchNoPanic(c, s.Fetcher, Value{}) | |
| 82 » » if err == nil { | |
| 83 » » » s.current = &result | |
| 84 » » } | |
| 85 » » s.lock.Unlock() | |
| 86 » » return | |
| 87 » } | |
| 88 | |
| 89 » // We have a cached copy but it has expired. Maybe some other goroutine is | |
| 90 » // fetching it already? Returns the cached stale copy if so. | |
| 91 » if s.currentFetcherCtx != nil { | |
| 92 » » result = *s.current | |
| 93 » » s.lock.Unlock() | |
| 94 » » return | |
| 95 » } | |
| 96 | |
| 97 » // No one is fetching the value now, we should do it. Prepare new contex t | |
| 98 » // that will be used to do the fetch once lock is released. | |
| 99 » timeout := 5 * time.Second | |
| 100 » if s.Timeout != 0 { | |
| 101 » » timeout = s.Timeout | |
| 102 » } | |
| 103 » s.currentFetcherCtx, _ = context.WithTimeout(c, timeout) | |
| 104 | |
| 105 » // Copy lock-protected guts into local variables before releasing the lo ck. | |
| 106 » currentFetcherCtx := s.currentFetcherCtx | |
| 107 » fetchCb := s.Fetcher | |
| 108 » prevVal := *s.current | |
| 109 » async := s.Async | |
| 110 | |
| 111 » // Release the lock to allow other goroutines to grab stale copy while f etch | |
| 112 » // is in the progress. | |
| 113 » s.lock.Unlock() | |
| 114 | |
| 115 » // fetch finishes the fetch and updates cached value. | |
| 116 » fetch := func() (result Value, err error) { | |
| 114 defer func() { | 117 defer func() { |
| 115 s.lock.Lock() | 118 s.lock.Lock() |
| 116 defer s.lock.Unlock() | 119 defer s.lock.Unlock() |
| 117 » » » s.currentFetcher = nil | 120 » » » s.currentFetcherCtx = nil |
| 118 » » » if err == nil && val.Value != nil { | 121 » » » if err == nil { |
| 119 » » » » s.current = &val | 122 » » » » s.current = &result |
| 120 } | 123 } |
| 121 }() | 124 }() |
| 122 » » return fetchCb(ctx, prevVal) | 125 » » return doFetchNoPanic(currentFetcherCtx, fetchCb, prevVal) |
| 123 } | 126 } |
| 124 | 127 |
| 125 if async { | 128 if async { |
| 129 // Start async fetch and return stale copy while it is running. | |
| 126 go fetch() | 130 go fetch() |
| 127 return prevVal, nil | 131 return prevVal, nil |
| 128 } | 132 } |
| 129 return fetch() | 133 return fetch() |
| 130 } | 134 } |
| 135 | |
| 136 // doFetchNoPanic calls fetcher callback, trapping panics and validating | |
| 137 // return value. | |
|
iannucci
2016/03/01 00:42:41
let it burrrnnnnnnnnnn....
_ ._
Vadim Sh.
2016/03/01 01:11:26
Done.
| |
| 138 func doFetchNoPanic(ctx context.Context, cb Fetcher, prev Value) (result Value, err error) { | |
| 139 defer func() { | |
| 140 if r := recover(); r != nil { | |
| 141 err = fmt.Errorf("panic caught in lazyslot.Slot Fetcher - %s", r) | |
| 142 return | |
| 143 } | |
| 144 switch { | |
| 145 case err == nil && result.Value == nil: | |
| 146 err = fmt.Errorf("lazyslot.Slot Fetcher returned nil val ue") | |
| 147 case err != nil: | |
| 148 result = Value{} | |
| 149 } | |
| 150 }() | |
| 151 return cb(ctx, prev) | |
| 152 } | |
| OLD | NEW |