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. | 6 // take significant time to refresh. |
| 7 // | 7 // |
| 8 // The defining property of the implementation is that only one goroutine | 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 | 9 // (can be background one) will block when refreshing such object, while all |
| 10 // others will use a slightly stale cached copy. | 10 // others will use a slightly stale cached copy. |
| 11 package lazyslot | 11 package lazyslot |
| 12 | 12 |
| 13 import ( | 13 import ( |
| 14 "fmt" | 14 "fmt" |
| 15 "sync" | 15 "sync" |
| 16 "time" | 16 "time" |
| 17 | 17 |
| 18 "golang.org/x/net/context" | 18 "golang.org/x/net/context" |
| 19 | 19 |
| 20 "github.com/luci/luci-go/common/clock" | 20 "github.com/luci/luci-go/common/clock" |
| 21 "github.com/luci/luci-go/common/retry" | |
| 21 ) | 22 ) |
| 22 | 23 |
| 23 // Value is what's stored in a Slot. It is treated as immutable value. | 24 // Value is what's stored in a Slot. It is treated as immutable value. |
| 24 type Value struct { | 25 type Value struct { |
| 25 // Value is whatever fetcher returned. | 26 // Value is whatever fetcher returned. |
| 26 Value interface{} | 27 Value interface{} |
| 27 // Expiration is time when this value expires and should be refetched. | 28 // Expiration is time when this value expires and should be refetched. |
| 28 Expiration time.Time | 29 Expiration time.Time |
| 29 } | 30 } |
| 30 | 31 |
| 31 // Fetcher knows how to load a new value. | 32 // Fetcher knows how to load a new value. |
| 32 // | 33 // |
| 33 // If it returns no errors, it MUST return non-nil Value.Value or Slot.Get will | 34 // If it returns no errors, it MUST return non-nil Value.Value or Slot.Get will |
| 34 // return error. | 35 // return error. |
| 35 // | 36 // |
| 36 // Panics inside Fetcher will be caught and converted to errors. | 37 // Panics inside Fetcher will be caught and converted to errors. |
| 37 type Fetcher func(c context.Context, prev Value) (Value, error) | 38 type Fetcher func(c context.Context, prev Value) (Value, error) |
| 38 | 39 |
| 39 // Slot holds a cached Value and refreshes it when it expires. | 40 // Slot holds a cached Value and refreshes it when it expires. |
| 40 // | 41 // |
| 41 // Only one goroutine will be busy refreshing, all others will see a slightly | 42 // Only one goroutine will be busy refreshing, all others will see a slightly |
| 42 // stale copy of the value during the refresh. | 43 // stale copy of the value during the refresh. |
| 43 type Slot struct { | 44 type Slot struct { |
| 44 » Fetcher Fetcher // used to actually load the value on demand | 45 » Fetcher Fetcher // used to actually load the value on deman d |
| 45 » Timeout time.Duration // how long to allow to fetch, 5 sec by default. | 46 » Timeout time.Duration // how long to allow to fetch, 15 sec by de fault. |
| 46 » Async bool // if true do fetches in background goroutine | 47 » Async bool // if true do fetches in background gorouti ne |
| 48 » RetryFactory retry.Factory // if non-nil, defines how to retry fetch e rrors | |
| 49 » RetryCallback retry.Callback // called before retries, useful in tests | |
|
iannucci
2016/03/01 00:44:21
Is this still used?
Vadim Sh.
2016/03/01 01:24:05
No, but it can be useful in general (e.g. to plug
| |
| 47 | 50 |
| 48 lock sync.Mutex // protects the guts below | 51 lock sync.Mutex // protects the guts below |
| 49 current *Value // currently known value or nil if not fetched | 52 current *Value // currently known value or nil if not fetched |
| 50 currentFetcherCtx context.Context // non-nil if some goroutine is fetchi ng now | 53 currentFetcherCtx context.Context // non-nil if some goroutine is fetchi ng now |
| 51 } | 54 } |
| 52 | 55 |
| 53 // Get returns stored value if it is still fresh. | 56 // Get returns stored value if it is still fresh. |
| 54 // | 57 // |
| 55 // It may return slightly stale copy if some other goroutine is fetching a new | 58 // It may return slightly stale copy if some other goroutine is fetching a new |
| 56 // copy now. If there's no cached copy at all, blocks until it is retrieved | 59 // copy now. If there's no cached copy at all, blocks until it is retrieved |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 71 if s.current != nil && now.Before(s.current.Expiration) { | 74 if s.current != nil && now.Before(s.current.Expiration) { |
| 72 result = *s.current | 75 result = *s.current |
| 73 s.lock.Unlock() | 76 s.lock.Unlock() |
| 74 return | 77 return |
| 75 } | 78 } |
| 76 | 79 |
| 77 // Fetching the value for the first time ever? Do it under the lock beca use | 80 // 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 | 81 // 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. | 82 // initial fetch to complete. They'll all block on s.lock.Lock() above. |
| 80 if s.current == nil { | 83 if s.current == nil { |
| 81 » » result, err = doFetchNoPanic(c, s.Fetcher, Value{}) | 84 » » result, err = fetchWithRetries(s.makeFetcherCtx(c), s.Fetcher, s .RetryFactory, s.RetryCallback, Value{}) |
| 82 if err == nil { | 85 if err == nil { |
| 83 s.current = &result | 86 s.current = &result |
| 84 } | 87 } |
| 85 s.lock.Unlock() | 88 s.lock.Unlock() |
| 86 return | 89 return |
| 87 } | 90 } |
| 88 | 91 |
| 89 // We have a cached copy but it has expired. Maybe some other goroutine is | 92 // 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. | 93 // fetching it already? Returns the cached stale copy if so. |
| 91 if s.currentFetcherCtx != nil { | 94 if s.currentFetcherCtx != nil { |
| 92 result = *s.current | 95 result = *s.current |
| 93 s.lock.Unlock() | 96 s.lock.Unlock() |
| 94 return | 97 return |
| 95 } | 98 } |
| 96 | 99 |
| 97 // No one is fetching the value now, we should do it. Prepare new contex t | 100 // 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. | 101 // that will be used to do the fetch once lock is released. |
| 99 » timeout := 5 * time.Second | 102 » s.currentFetcherCtx = s.makeFetcherCtx(c) |
| 100 » if s.Timeout != 0 { | |
| 101 » » timeout = s.Timeout | |
| 102 » } | |
| 103 » s.currentFetcherCtx, _ = context.WithTimeout(c, timeout) | |
| 104 | 103 |
| 105 // Copy lock-protected guts into local variables before releasing the lo ck. | 104 // Copy lock-protected guts into local variables before releasing the lo ck. |
| 106 currentFetcherCtx := s.currentFetcherCtx | 105 currentFetcherCtx := s.currentFetcherCtx |
| 107 fetchCb := s.Fetcher | 106 fetchCb := s.Fetcher |
| 108 prevVal := *s.current | 107 prevVal := *s.current |
| 109 async := s.Async | 108 async := s.Async |
| 109 retryFactory := s.RetryFactory | |
| 110 retryCallback := s.RetryCallback | |
| 110 | 111 |
| 111 // Release the lock to allow other goroutines to grab stale copy while f etch | 112 // Release the lock to allow other goroutines to grab stale copy while f etch |
| 112 // is in the progress. | 113 // is in the progress. |
| 113 s.lock.Unlock() | 114 s.lock.Unlock() |
| 114 | 115 |
| 115 // fetch finishes the fetch and updates cached value. | 116 // fetch finishes the fetch and updates cached value. |
| 116 fetch := func() (result Value, err error) { | 117 fetch := func() (result Value, err error) { |
| 117 defer func() { | 118 defer func() { |
| 118 s.lock.Lock() | 119 s.lock.Lock() |
| 119 defer s.lock.Unlock() | 120 defer s.lock.Unlock() |
| 120 s.currentFetcherCtx = nil | 121 s.currentFetcherCtx = nil |
| 121 if err == nil { | 122 if err == nil { |
| 122 s.current = &result | 123 s.current = &result |
| 123 } | 124 } |
| 124 }() | 125 }() |
| 125 » » return doFetchNoPanic(currentFetcherCtx, fetchCb, prevVal) | 126 » » return fetchWithRetries(currentFetcherCtx, fetchCb, retryFactory , retryCallback, prevVal) |
| 126 } | 127 } |
| 127 | 128 |
| 128 if async { | 129 if async { |
| 129 // Start async fetch and return stale copy while it is running. | 130 // Start async fetch and return stale copy while it is running. |
| 130 go fetch() | 131 go fetch() |
| 131 return prevVal, nil | 132 return prevVal, nil |
| 132 } | 133 } |
| 133 return fetch() | 134 return fetch() |
| 134 } | 135 } |
| 135 | 136 |
| 136 // doFetchNoPanic calls fetcher callback, trapping panics and validating | 137 // makeFetcherCtx prepares a context to use for fetch operation. |
| 138 // | |
| 139 // Must be called under the lock. | |
| 140 func (s *Slot) makeFetcherCtx(c context.Context) context.Context { | |
| 141 » timeout := 15 * time.Second | |
| 142 » if s.Timeout != 0 { | |
| 143 » » timeout = s.Timeout | |
| 144 » } | |
| 145 » fetcherCtx, _ := clock.WithTimeout(c, timeout) | |
| 146 » return fetcherCtx | |
| 147 } | |
| 148 | |
| 149 // fetchWithRetries calls fetchNoPanic, retrying on errors. | |
| 150 func fetchWithRetries(ctx context.Context, cb Fetcher, factory retry.Factory, re tryCb retry.Callback, prev Value) (result Value, err error) { | |
| 151 » if factory == nil { | |
| 152 » » return fetchNoPanic(ctx, cb, prev) | |
| 153 » } | |
| 154 » err = retry.Retry(clock.Tag(ctx, "retry"), factory, func() error { | |
| 155 » » result, err = fetchNoPanic(ctx, cb, prev) | |
| 156 » » return err | |
| 157 » }, retryCb) | |
| 158 » if err != nil { | |
| 159 » » result = Value{} | |
| 160 » } | |
| 161 » return | |
| 162 } | |
| 163 | |
| 164 // fetchNoPanic calls fetcher callback, trapping panics and validating | |
| 137 // return value. | 165 // return value. |
| 138 func doFetchNoPanic(ctx context.Context, cb Fetcher, prev Value) (result Value, err error) { | 166 func fetchNoPanic(ctx context.Context, cb Fetcher, prev Value) (result Value, er r error) { |
| 139 defer func() { | 167 defer func() { |
| 140 if r := recover(); r != nil { | 168 if r := recover(); r != nil { |
| 141 err = fmt.Errorf("panic caught in lazyslot.Slot Fetcher - %s", r) | 169 err = fmt.Errorf("panic caught in lazyslot.Slot Fetcher - %s", r) |
| 142 return | 170 return |
| 143 } | 171 } |
| 144 switch { | 172 switch { |
| 145 case err == nil && result.Value == nil: | 173 case err == nil && result.Value == nil: |
| 146 err = fmt.Errorf("lazyslot.Slot Fetcher returned nil val ue") | 174 err = fmt.Errorf("lazyslot.Slot Fetcher returned nil val ue") |
| 147 case err != nil: | 175 case err != nil: |
| 148 result = Value{} | 176 result = Value{} |
| 149 } | 177 } |
| 150 }() | 178 }() |
| 151 return cb(ctx, prev) | 179 return cb(ctx, prev) |
| 152 } | 180 } |
| OLD | NEW |