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

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: don't trap panics 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 will
9 // block when refreshing such object, while all others will use a slightly stale
10 // cached copy.
9 package lazyslot 11 package lazyslot
10 12
11 import ( 13 import (
12 "sync" 14 "sync"
13 "time" 15 "time"
14 16
15 "golang.org/x/net/context" 17 "golang.org/x/net/context"
16 18
17 "github.com/luci/luci-go/common/clock" 19 "github.com/luci/luci-go/common/clock"
18 ) 20 )
19 21
20 // Value is what's stored in a Slot. It is treated as immutable value. 22 // Value is what's stored in a Slot. It is treated as immutable value.
21 type Value struct { 23 type Value struct {
22 // Value is whatever fetcher returned. 24 // Value is whatever fetcher returned.
23 Value interface{} 25 Value interface{}
24 // Expiration is time when this value expires and should be refetched. 26 // Expiration is time when this value expires and should be refetched.
25 Expiration time.Time 27 Expiration time.Time
26 } 28 }
27 29
28 // Fetcher knows how to load new value. 30 // Fetcher knows how to load a new value.
31 //
32 // If it returns no errors, it MUST return non-nil Value.Value or Slot.Get will
33 // panic.
29 type Fetcher func(c context.Context, prev Value) (Value, error) 34 type Fetcher func(c context.Context, prev Value) (Value, error)
30 35
31 // Slot holds a cached Value and refreshes it when it expires. Only one 36 // Slot holds a cached Value and refreshes it when it expires.
32 // goroutine will be busy refreshing, all others will see a slightly stale 37 //
33 // copy of the value during the refresh. 38 // Only one goroutine will be busy refreshing, all others will see a slightly
39 // stale copy of the value during the refresh.
34 type Slot struct { 40 type Slot struct {
35 Fetcher Fetcher // used to actually load the value on demand 41 Fetcher Fetcher // used to actually load the value on demand
36 Timeout time.Duration // how long to allow to fetch, 5 sec by default. 42 Timeout time.Duration // how long to allow to fetch, 5 sec by default.
37 Async bool // if true do fetches in background goroutine
38 43
39 » lock sync.Mutex // protects the guts below 44 » lock sync.Mutex // protects the guts below
40 » current *Value // currently known value or nil if not fe tched 45 » current *Value // currently known value or nil if not fetched
41 » currentFetcher context.Context // non nil if some goroutine is fetching now 46 » currentFetcherCtx context.Context // non-nil if some goroutine is fetchi ng now
42 } 47 }
43 48
44 // Peek returns currently cached value if there's one or zero Value{} if not. 49 // Get returns stored value if it is still fresh.
45 // It doesn't try to fetch a value. 50 //
46 func (s *Slot) Peek() Value { 51 // It may return slightly stale copy if some other goroutine is fetching a new
47 » if s.current == nil { 52 // copy now. If there's no cached copy at all, blocks until it is retrieved.
48 » » return Value{} 53 //
54 // Returns an error only when Fetcher returns an error. Panics if fetcher
55 // doesn't produce a value, and doesn't return an error.
56 func (s *Slot) Get(c context.Context) (result Value, err error) {
57 » // state is populate in the anonymous function below.
58 » var state struct {
59 » » C context.Context
60 » » Fetcher Fetcher
61 » » PrevValue Value
49 } 62 }
50 return *s.current
51 }
52 63
53 // Get returns stored value if it is still fresh. It may return slightly stale 64 » result, err, done := func() (result Value, err error, done bool) {
54 // copy if some other goroutine is fetching a new copy now. If there's no cached 65 » » now := clock.Now(c)
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)
59 66
60 » // Set in the local function below, used it fetch is needed. 67 » » // This lock protects the guts of the slot and makes sure only o ne goroutine
61 » var ( 68 » » // is doing an initial fetch.
62 » » ctx context.Context
63 » » fetchCb Fetcher
64 » » prevVal Value
65 » » async bool
66 » )
67
68 » // If done is true, val and err are returned right away.
69 » done, val, err := func() (bool, Value, error) {
70 s.lock.Lock() 69 s.lock.Lock()
71 defer s.lock.Unlock() 70 defer s.lock.Unlock()
72 71
73 » » // Still fresh? Return right away. 72 » » // A cached value exists and it is still fresh? Return it right away.
74 if s.current != nil && now.Before(s.current.Expiration) { 73 if s.current != nil && now.Before(s.current.Expiration) {
75 » » » return true, *s.current, nil 74 » » » result = *s.current
75 » » » done = true
76 » » » return
76 } 77 }
77 78
78 // Fetching the value for the first time ever? Do it under the l ock because 79 // 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 // 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 // initial fetch to complete. They'll all block on s.lock.Lock() above.
81 if s.current == nil { 82 if s.current == nil {
82 » » » val, err := s.Fetcher(c, Value{}) 83 » » » result, err = doFetch(c, s.Fetcher, Value{})
83 » » » if err != nil { 84 » » » if err == nil {
84 » » » » return true, Value{}, err 85 » » » » s.current = &result
85 } 86 }
86 » » » s.current = &val 87 » » » done = true
87 » » » return true, val, nil 88 » » » return
88 } 89 }
89 90
90 » » // We have a cached copy and it has expired. Maybe some other go routine is 91 » » // We have a cached copy but it has expired. Maybe some other go routine is
91 » » // fetching it already? Returns the stale copy if so. 92 » » // fetching it already? Returns the cached stale copy if so.
92 » » if s.currentFetcher != nil { 93 » » if s.currentFetcherCtx != nil {
93 » » » return true, *s.current, nil 94 » » » result = *s.current
95 » » » done = true
96 » » » return
94 } 97 }
95 98
96 » » // No one is fetching the value now, we should do it. Release th e lock while 99 » » // No one is fetching the value now, we should do it. Prepare a new context
97 » » // fetching to allow other goroutines to grab the stale copy. 100 » » // that will be used to do the fetch once lock is released.
98 timeout := 5 * time.Second 101 timeout := 5 * time.Second
99 if s.Timeout != 0 { 102 if s.Timeout != 0 {
100 timeout = s.Timeout 103 timeout = s.Timeout
101 } 104 }
102 » » s.currentFetcher, _ = context.WithTimeout(c, timeout) 105 » » s.currentFetcherCtx, _ = context.WithTimeout(c, timeout)
103 » » ctx = s.currentFetcher 106
104 » » fetchCb = s.Fetcher 107 » » // Copy lock-protected guts into local variables before releasin g the lock.
105 » » prevVal = *s.current 108 » » state.C = s.currentFetcherCtx
106 » » async = s.Async 109 » » state.Fetcher = s.Fetcher
107 » » return false, Value{}, nil 110 » » state.PrevValue = *s.current
111 » » return
108 }() 112 }()
109 if done { 113 if done {
110 » » return val, err 114 » » return
111 } 115 }
112 116
113 » fetch := func() (val Value, err error) { 117 » // Finish the fetch and update the cached value.
118 » return func() (result Value, err error) {
114 defer func() { 119 defer func() {
115 s.lock.Lock() 120 s.lock.Lock()
116 defer s.lock.Unlock() 121 defer s.lock.Unlock()
117 » » » s.currentFetcher = nil 122 » » » s.currentFetcherCtx = nil
118 » » » if err == nil && val.Value != nil { 123 » » » // result.Value is not nil iff fetch succeeded and didn' t panic.
119 » » » » s.current = &val 124 » » » if result.Value != nil {
125 » » » » s.current = &result
120 } 126 }
121 }() 127 }()
122 » » return fetchCb(ctx, prevVal) 128 » » return doFetch(state.C, state.Fetcher, state.PrevValue)
129 » }()
130 }
131
132 // doFetch calls fetcher callback and validates return value.
133 func doFetch(ctx context.Context, cb Fetcher, prev Value) (result Value, err err or) {
134 » result, err = cb(ctx, prev)
135 » switch {
136 » case err == nil && result.Value == nil:
137 » » panic("lazyslot.Slot Fetcher returned nil value")
138 » case err != nil:
139 » » result = Value{}
123 } 140 }
124 141 » return
125 » if async {
126 » » go fetch()
127 » » return prevVal, nil
128 » }
129 » return fetch()
130 } 142 }
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