| 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 will | 8 // The defining property of the implementation is that only one goroutine will |
| 9 // block when refreshing such object, while all others will use a slightly stale | 9 // block when refreshing such object, while all others will use a slightly stale |
| 10 // cached copy. | 10 // cached copy. |
| 11 package lazyslot | 11 package lazyslot |
| 12 | 12 |
| 13 import ( | 13 import ( |
| 14 "sync" | 14 "sync" |
| 15 "time" | 15 "time" |
| 16 | 16 |
| 17 "golang.org/x/net/context" | 17 "golang.org/x/net/context" |
| 18 | 18 |
| 19 "github.com/luci/luci-go/common/clock" | 19 "github.com/luci/luci-go/common/clock" |
| 20 "github.com/luci/luci-go/common/retry" |
| 20 ) | 21 ) |
| 21 | 22 |
| 22 // 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. |
| 23 type Value struct { | 24 type Value struct { |
| 24 // Value is whatever fetcher returned. | 25 // Value is whatever fetcher returned. |
| 25 Value interface{} | 26 Value interface{} |
| 26 // Expiration is time when this value expires and should be refetched. | 27 // Expiration is time when this value expires and should be refetched. |
| 27 Expiration time.Time | 28 Expiration time.Time |
| 28 } | 29 } |
| 29 | 30 |
| 30 // Fetcher knows how to load a new value. | 31 // Fetcher knows how to load a new value. |
| 31 // | 32 // |
| 32 // If it returns no errors, it MUST return non-nil Value.Value or Slot.Get will | 33 // If it returns no errors, it MUST return non-nil Value.Value or Slot.Get will |
| 33 // panic. | 34 // panic. |
| 34 type Fetcher func(c context.Context, prev Value) (Value, error) | 35 type Fetcher func(c context.Context, prev Value) (Value, error) |
| 35 | 36 |
| 36 // Slot holds a cached Value and refreshes it when it expires. | 37 // Slot holds a cached Value and refreshes it when it expires. |
| 37 // | 38 // |
| 38 // Only one goroutine will be busy refreshing, all others will see a slightly | 39 // Only one goroutine will be busy refreshing, all others will see a slightly |
| 39 // stale copy of the value during the refresh. | 40 // stale copy of the value during the refresh. |
| 40 type Slot struct { | 41 type Slot struct { |
| 41 » Fetcher Fetcher // used to actually load the value on demand | 42 » Fetcher Fetcher // used to actually load the value on deman
d |
| 42 » Timeout time.Duration // how long to allow to fetch, 5 sec by default. | 43 » Timeout time.Duration // how long to allow to fetch, 15 sec by de
fault. |
| 44 » RetryFactory retry.Factory // if non-nil, defines how to retry fetch e
rrors |
| 45 » RetryCallback retry.Callback // called before retries, useful in tests |
| 43 | 46 |
| 44 lock sync.Mutex // protects the guts below | 47 lock sync.Mutex // protects the guts below |
| 45 current *Value // currently known value or nil if not
fetched | 48 current *Value // currently known value or nil if not
fetched |
| 46 currentFetcherCtx context.Context // non-nil if some goroutine is fetchi
ng now | 49 currentFetcherCtx context.Context // non-nil if some goroutine is fetchi
ng now |
| 47 } | 50 } |
| 48 | 51 |
| 49 // Get returns stored value if it is still fresh. | 52 // Get returns stored value if it is still fresh. |
| 50 // | 53 // |
| 51 // It may return slightly stale copy if some other goroutine is fetching a new | 54 // It may return slightly stale copy if some other goroutine is fetching a new |
| 52 // copy now. If there's no cached copy at all, blocks until it is retrieved. | 55 // copy now. If there's no cached copy at all, blocks until it is retrieved. |
| 53 // | 56 // |
| 54 // Returns an error only when Fetcher returns an error. Panics if fetcher | 57 // Returns an error only when Fetcher returns an error. Panics if fetcher |
| 55 // doesn't produce a value, and doesn't return an error. | 58 // doesn't produce a value, and doesn't return an error. |
| 56 func (s *Slot) Get(c context.Context) (result Value, err error) { | 59 func (s *Slot) Get(c context.Context) (result Value, err error) { |
| 57 // state is populate in the anonymous function below. | 60 // state is populate in the anonymous function below. |
| 58 var state struct { | 61 var state struct { |
| 59 » » C context.Context | 62 » » C context.Context |
| 60 » » Fetcher Fetcher | 63 » » Fetcher Fetcher |
| 61 » » PrevValue Value | 64 » » PrevValue Value |
| 65 » » RetryFactory retry.Factory |
| 66 » » RetryCallback retry.Callback |
| 62 } | 67 } |
| 63 | 68 |
| 64 result, err, done := func() (result Value, err error, done bool) { | 69 result, err, done := func() (result Value, err error, done bool) { |
| 65 now := clock.Now(c) | 70 now := clock.Now(c) |
| 66 | 71 |
| 67 // This lock protects the guts of the slot and makes sure only o
ne goroutine | 72 // This lock protects the guts of the slot and makes sure only o
ne goroutine |
| 68 // is doing an initial fetch. | 73 // is doing an initial fetch. |
| 69 s.lock.Lock() | 74 s.lock.Lock() |
| 70 defer s.lock.Unlock() | 75 defer s.lock.Unlock() |
| 71 | 76 |
| 72 // A cached value exists and it is still fresh? Return it right
away. | 77 // A cached value exists and it is still fresh? Return it right
away. |
| 73 if s.current != nil && now.Before(s.current.Expiration) { | 78 if s.current != nil && now.Before(s.current.Expiration) { |
| 74 result = *s.current | 79 result = *s.current |
| 75 done = true | 80 done = true |
| 76 return | 81 return |
| 77 } | 82 } |
| 78 | 83 |
| 79 // Fetching the value for the first time ever? Do it under the l
ock because | 84 // Fetching the value for the first time ever? Do it under the l
ock because |
| 80 // there's nothing to return yet. All goroutines would have to w
ait for this | 85 // there's nothing to return yet. All goroutines would have to w
ait for this |
| 81 // initial fetch to complete. They'll all block on s.lock.Lock()
above. | 86 // initial fetch to complete. They'll all block on s.lock.Lock()
above. |
| 82 if s.current == nil { | 87 if s.current == nil { |
| 83 » » » result, err = doFetch(c, s.Fetcher, Value{}) | 88 » » » result, err = fetchWithRetries(s.makeFetcherCtx(c), s.Fe
tcher, s.RetryFactory, s.RetryCallback, Value{}) |
| 84 if err == nil { | 89 if err == nil { |
| 85 s.current = &result | 90 s.current = &result |
| 86 } | 91 } |
| 87 done = true | 92 done = true |
| 88 return | 93 return |
| 89 } | 94 } |
| 90 | 95 |
| 91 // We have a cached copy but it has expired. Maybe some other go
routine is | 96 // We have a cached copy but it has expired. Maybe some other go
routine is |
| 92 // fetching it already? Returns the cached stale copy if so. | 97 // fetching it already? Returns the cached stale copy if so. |
| 93 if s.currentFetcherCtx != nil { | 98 if s.currentFetcherCtx != nil { |
| 94 result = *s.current | 99 result = *s.current |
| 95 done = true | 100 done = true |
| 96 return | 101 return |
| 97 } | 102 } |
| 98 | 103 |
| 99 // No one is fetching the value now, we should do it. Prepare a
new context | 104 // No one is fetching the value now, we should do it. Prepare a
new context |
| 100 // that will be used to do the fetch once lock is released. | 105 // that will be used to do the fetch once lock is released. |
| 101 » » timeout := 5 * time.Second | 106 » » s.currentFetcherCtx = s.makeFetcherCtx(c) |
| 102 » » if s.Timeout != 0 { | |
| 103 » » » timeout = s.Timeout | |
| 104 » » } | |
| 105 » » s.currentFetcherCtx, _ = context.WithTimeout(c, timeout) | |
| 106 | 107 |
| 107 // Copy lock-protected guts into local variables before releasin
g the lock. | 108 // Copy lock-protected guts into local variables before releasin
g the lock. |
| 108 state.C = s.currentFetcherCtx | 109 state.C = s.currentFetcherCtx |
| 109 state.Fetcher = s.Fetcher | 110 state.Fetcher = s.Fetcher |
| 110 state.PrevValue = *s.current | 111 state.PrevValue = *s.current |
| 112 state.RetryFactory = s.RetryFactory |
| 113 state.RetryCallback = s.RetryCallback |
| 111 return | 114 return |
| 112 }() | 115 }() |
| 113 if done { | 116 if done { |
| 114 return | 117 return |
| 115 } | 118 } |
| 116 | 119 |
| 117 // Finish the fetch and update the cached value. | 120 // Finish the fetch and update the cached value. |
| 118 return func() (result Value, err error) { | 121 return func() (result Value, err error) { |
| 119 defer func() { | 122 defer func() { |
| 120 s.lock.Lock() | 123 s.lock.Lock() |
| 121 defer s.lock.Unlock() | 124 defer s.lock.Unlock() |
| 122 s.currentFetcherCtx = nil | 125 s.currentFetcherCtx = nil |
| 123 // result.Value is not nil iff fetch succeeded and didn'
t panic. | 126 // result.Value is not nil iff fetch succeeded and didn'
t panic. |
| 124 if result.Value != nil { | 127 if result.Value != nil { |
| 125 s.current = &result | 128 s.current = &result |
| 126 } | 129 } |
| 127 }() | 130 }() |
| 128 » » return doFetch(state.C, state.Fetcher, state.PrevValue) | 131 » » return fetchWithRetries(state.C, state.Fetcher, state.RetryFacto
ry, state.RetryCallback, state.PrevValue) |
| 129 }() | 132 }() |
| 130 } | 133 } |
| 131 | 134 |
| 135 // makeFetcherCtx prepares a context to use for fetch operation. |
| 136 // |
| 137 // Must be called under the lock. |
| 138 func (s *Slot) makeFetcherCtx(c context.Context) context.Context { |
| 139 timeout := 15 * time.Second |
| 140 if s.Timeout != 0 { |
| 141 timeout = s.Timeout |
| 142 } |
| 143 fetcherCtx, _ := clock.WithTimeout(c, timeout) |
| 144 return fetcherCtx |
| 145 } |
| 146 |
| 147 // fetchWithRetries calls doFetch, retrying on errors. |
| 148 func fetchWithRetries(ctx context.Context, cb Fetcher, factory retry.Factory, re
tryCb retry.Callback, prev Value) (result Value, err error) { |
| 149 if factory == nil { |
| 150 return doFetch(ctx, cb, prev) |
| 151 } |
| 152 err = retry.Retry(clock.Tag(ctx, "retry"), factory, func() error { |
| 153 result, err = doFetch(ctx, cb, prev) |
| 154 return err |
| 155 }, retryCb) |
| 156 if err != nil { |
| 157 result = Value{} |
| 158 } |
| 159 return |
| 160 } |
| 161 |
| 132 // doFetch calls fetcher callback and validates return value. | 162 // doFetch calls fetcher callback and validates return value. |
| 133 func doFetch(ctx context.Context, cb Fetcher, prev Value) (result Value, err err
or) { | 163 func doFetch(ctx context.Context, cb Fetcher, prev Value) (result Value, err err
or) { |
| 134 result, err = cb(ctx, prev) | 164 result, err = cb(ctx, prev) |
| 135 switch { | 165 switch { |
| 136 case err == nil && result.Value == nil: | 166 case err == nil && result.Value == nil: |
| 137 panic("lazyslot.Slot Fetcher returned nil value") | 167 panic("lazyslot.Slot Fetcher returned nil value") |
| 138 case err != nil: | 168 case err != nil: |
| 139 result = Value{} | 169 result = Value{} |
| 140 } | 170 } |
| 141 return | 171 return |
| 142 } | 172 } |
| OLD | NEW |