go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/caching/lazyslot/lazyslot_test.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package lazyslot 16 17 import ( 18 "context" 19 "errors" 20 "sync" 21 "testing" 22 "time" 23 24 "go.chromium.org/luci/common/clock/testclock" 25 26 . "github.com/smartystreets/goconvey/convey" 27 ) 28 29 func TestLazySlot(t *testing.T) { 30 Convey("Blocking mode works", t, func() { 31 c, clk := newContext() 32 33 lock := sync.Mutex{} 34 counter := 0 35 36 fetcher := func(prev any) (any, time.Duration, error) { 37 lock.Lock() 38 defer lock.Unlock() 39 counter++ 40 return counter, time.Second, nil 41 } 42 43 s := Slot{} 44 45 // Initial fetch. 46 So(s.current, ShouldBeNil) 47 v, err := s.Get(c, fetcher) 48 So(err, ShouldBeNil) 49 So(v.(int), ShouldEqual, 1) 50 51 // Still fresh. 52 v, err = s.Get(c, fetcher) 53 So(err, ShouldBeNil) 54 So(v.(int), ShouldEqual, 1) 55 56 // Expires and refreshed. 57 clk.Add(5 * time.Second) 58 v, err = s.Get(c, fetcher) 59 So(err, ShouldBeNil) 60 So(v.(int), ShouldEqual, 2) 61 }) 62 63 Convey("Initial failed fetch causes errors", t, func() { 64 c, _ := newContext() 65 66 s := Slot{} 67 68 // Initial failed fetch. 69 failErr := errors.New("fail") 70 _, err := s.Get(c, func(prev any) (any, time.Duration, error) { 71 return nil, 0, failErr 72 }) 73 So(err, ShouldEqual, failErr) 74 75 // Subsequence successful fetch. 76 val, err := s.Get(c, func(prev any) (any, time.Duration, error) { 77 return 1, 0, nil 78 }) 79 So(err, ShouldBeNil) 80 So(val, ShouldResemble, 1) 81 }) 82 83 Convey("Returns stale copy while fetching", t, func(conv C) { 84 c, clk := newContext() 85 86 // Put initial value. 87 s := Slot{} 88 v, err := s.Get(c, func(prev any) (any, time.Duration, error) { 89 return 1, time.Second, nil 90 }) 91 So(err, ShouldBeNil) 92 So(v.(int), ShouldEqual, 1) 93 94 fetching := make(chan bool) 95 resume := make(chan bool) 96 fetcher := func(prev any) (any, time.Duration, error) { 97 fetching <- true 98 <-resume 99 return 2, time.Second, nil 100 } 101 102 // Make it expire. Start blocking fetch of the new value. 103 clk.Add(5 * time.Second) 104 wg := sync.WaitGroup{} 105 wg.Add(1) 106 go func() { 107 defer wg.Done() 108 v, err := s.Get(c, fetcher) 109 conv.So(err, ShouldBeNil) 110 conv.So(v.(int), ShouldEqual, 2) 111 }() 112 113 // Wait until we hit the body of the fetcher callback. 114 <-fetching 115 116 // Concurrent Get() returns stale copy right away (does not deadlock). 117 v, err = s.Get(c, fetcher) 118 So(err, ShouldBeNil) 119 So(v.(int), ShouldEqual, 1) 120 121 // Wait until another goroutine finishes the fetch. 122 resume <- true 123 wg.Wait() 124 125 // Returns new value now. 126 v, err = s.Get(c, fetcher) 127 So(err, ShouldBeNil) 128 So(v.(int), ShouldEqual, 2) 129 }) 130 131 Convey("Recovers from panic", t, func() { 132 c, clk := newContext() 133 134 // Initial value. 135 s := Slot{} 136 v, err := s.Get(c, func(prev any) (any, time.Duration, error) { 137 return 1, time.Second, nil 138 }) 139 So(err, ShouldBeNil) 140 So(v.(int), ShouldEqual, 1) 141 142 // Make it expire. Start panicking fetch. 143 clk.Add(5 * time.Second) 144 So(func() { 145 s.Get(c, func(prev any) (any, time.Duration, error) { 146 panic("omg") 147 }) 148 }, ShouldPanicWith, "omg") 149 150 // Doesn't deadlock. 151 v, err = s.Get(c, func(prev any) (any, time.Duration, error) { 152 return 2, time.Second, nil 153 }) 154 So(err, ShouldBeNil) 155 So(v.(int), ShouldEqual, 2) 156 }) 157 158 Convey("Nil value is allowed", t, func() { 159 c, clk := newContext() 160 s := Slot{} 161 162 // Initial nil fetch. 163 val, err := s.Get(c, func(prev any) (any, time.Duration, error) { 164 return nil, time.Second, nil 165 }) 166 So(err, ShouldBeNil) 167 So(val, ShouldBeNil) 168 169 // Some time later this nil expires and we fetch something else. 170 clk.Add(2 * time.Second) 171 val, err = s.Get(c, func(prev any) (any, time.Duration, error) { 172 So(prev, ShouldBeNil) 173 return 1, time.Second, nil 174 }) 175 So(err, ShouldBeNil) 176 So(val, ShouldResemble, 1) 177 }) 178 179 Convey("Zero expiration means never expires", t, func() { 180 c, clk := newContext() 181 s := Slot{} 182 183 // Initial fetch. 184 val, err := s.Get(c, func(prev any) (any, time.Duration, error) { 185 return 1, 0, nil 186 }) 187 So(err, ShouldBeNil) 188 So(val, ShouldResemble, 1) 189 190 // Many years later still cached. 191 clk.Add(200000 * time.Hour) 192 val, err = s.Get(c, func(prev any) (any, time.Duration, error) { 193 return 2, time.Second, nil 194 }) 195 So(err, ShouldBeNil) 196 So(val, ShouldResemble, 1) 197 }) 198 199 Convey("ExpiresImmediately means expires at the same instant", t, func() { 200 c, _ := newContext() 201 s := Slot{} 202 203 // Initial fetch. 204 val, err := s.Get(c, func(prev any) (any, time.Duration, error) { 205 return 1, ExpiresImmediately, nil 206 }) 207 So(err, ShouldBeNil) 208 So(val, ShouldResemble, 1) 209 210 // No time moved, but refetch still happened. 211 val, err = s.Get(c, func(prev any) (any, time.Duration, error) { 212 return 2, time.Second, nil 213 }) 214 So(err, ShouldBeNil) 215 So(val, ShouldResemble, 2) 216 }) 217 218 Convey("Retries failed refetch later", t, func() { 219 c, clk := newContext() 220 221 var errorToReturn error 222 var valueToReturn int 223 224 fetchCalls := 0 225 fetcher := func(prev any) (any, time.Duration, error) { 226 fetchCalls++ 227 return valueToReturn, time.Minute, errorToReturn 228 } 229 230 s := Slot{} 231 232 // Initial fetch. 233 valueToReturn = 1 234 errorToReturn = nil 235 val, err := s.Get(c, fetcher) 236 So(val, ShouldResemble, 1) 237 So(err, ShouldBeNil) 238 So(fetchCalls, ShouldEqual, 1) 239 240 // Cached copy is good after 30 sec. 241 clk.Add(30 * time.Second) 242 valueToReturn = 2 243 errorToReturn = nil 244 val, err = s.Get(c, fetcher) 245 So(val, ShouldResemble, 1) // still cached copy 246 So(err, ShouldBeNil) 247 So(fetchCalls, ShouldEqual, 1) 248 249 // After 31 the cache copy expires, we attempt to update it, but something 250 // goes horribly wrong. Get(...) returns the old copy. 251 clk.Add(31 * time.Second) 252 valueToReturn = 3 253 errorToReturn = errors.New("omg") 254 val, err = s.Get(c, fetcher) 255 So(val, ShouldResemble, 1) // still cached copy 256 So(err, ShouldBeNil) 257 So(fetchCalls, ShouldEqual, 2) // attempted to fetch 258 259 // 1 sec later still using old copy, because retry is scheduled for later. 260 clk.Add(time.Second) 261 valueToReturn = 4 262 errorToReturn = nil 263 val, err = s.Get(c, fetcher) 264 So(val, ShouldResemble, 1) // still cached copy 265 So(err, ShouldBeNil) 266 So(fetchCalls, ShouldEqual, 2) 267 268 // 5 sec later fetched is attempted, and it succeeds. 269 clk.Add(5 * time.Second) 270 valueToReturn = 5 271 errorToReturn = nil 272 val, err = s.Get(c, fetcher) 273 So(val, ShouldResemble, 5) // new copy 274 So(err, ShouldBeNil) 275 So(fetchCalls, ShouldEqual, 3) 276 }) 277 } 278 279 func newContext() (context.Context, testclock.TestClock) { 280 return testclock.UseTime(context.Background(), time.Unix(1442270520, 0)) 281 }