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  }