Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(384)

Side by Side Diff: common/lazyslot/lazyslot.go

Issue 1748933002: Retry RPC deadlines when fetching GAE settings. (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-go@simplify-lazyslot
Patch Set: use clock tags to properly advance time in test Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | common/lazyslot/lazyslot_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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 }
OLDNEW
« no previous file with comments | « no previous file | common/lazyslot/lazyslot_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698