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

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

Issue 1750023002: common/lazyslot: Simplify code a little bit. (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-go@master
Patch Set: 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. 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 }
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