k8s.io/client-go@v0.31.1/tools/cache/synctrack/lazy_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package synctrack_test
    18  
    19  import (
    20  	"errors"
    21  	"sync"
    22  	"sync/atomic"
    23  	"testing"
    24  	"time"
    25  
    26  	"k8s.io/client-go/tools/cache/synctrack"
    27  )
    28  
    29  func TestLazy(t *testing.T) {
    30  	var reality int64
    31  	var z synctrack.Lazy[int64]
    32  
    33  	z.Evaluate = func() (int64, error) {
    34  		return atomic.LoadInt64(&reality), nil
    35  	}
    36  
    37  	var wg sync.WaitGroup
    38  
    39  	for i := 0; i < 10; i++ {
    40  		wg.Add(1)
    41  		go func(delay time.Duration) {
    42  			defer wg.Done()
    43  			for i := 0; i < 100; i++ {
    44  				t.Helper()
    45  				set := atomic.AddInt64(&reality, 1)
    46  				z.Notify()
    47  				got, err := z.Get()
    48  				if err != nil {
    49  					t.Errorf("unexpected error: %v", err)
    50  				}
    51  				if got < set {
    52  					t.Errorf("time went backwards. %v vs %v", got, set)
    53  				}
    54  				time.Sleep(delay)
    55  			}
    56  		}((1 + time.Duration(i%3)) * time.Microsecond)
    57  	}
    58  
    59  	wg.Wait()
    60  }
    61  
    62  func TestLazyThroughput(t *testing.T) {
    63  	var reality int64
    64  	var z synctrack.Lazy[int64]
    65  	var totalWait int64
    66  	z.Evaluate = func() (int64, error) {
    67  		got := atomic.LoadInt64(&reality)
    68  		time.Sleep(11 * time.Millisecond)
    69  		return got, nil
    70  	}
    71  
    72  	var wg sync.WaitGroup
    73  	wg.Add(1)
    74  
    75  	go func() {
    76  		defer wg.Done()
    77  		notifies := 0
    78  		tt := time.NewTicker(10 * time.Millisecond)
    79  		for {
    80  			<-tt.C
    81  			atomic.AddInt64(&reality, 1)
    82  			z.Notify()
    83  			notifies++
    84  			if notifies >= 100 {
    85  				tt.Stop()
    86  				return
    87  			}
    88  			wg.Add(1)
    89  			go func() {
    90  				t.Helper()
    91  				defer wg.Done()
    92  				start := time.Now()
    93  				z.Get()
    94  				d := time.Since(start)
    95  				atomic.AddInt64(&totalWait, int64(d))
    96  			}()
    97  		}
    98  	}()
    99  
   100  	wg.Wait()
   101  
   102  	twd := time.Duration(totalWait)
   103  
   104  	if twd > 3*time.Second {
   105  		t.Errorf("total wait was: %v; par would be ~1s", twd)
   106  	}
   107  
   108  }
   109  
   110  // sequence is for controlling the order various lines of code execute in.
   111  // Replaces a bunch of time.Sleep() calls that would certainly be flaky.
   112  type sequence []sync.WaitGroup
   113  
   114  func newSequence(n int) sequence {
   115  	s := make(sequence, n)
   116  	for i := range s {
   117  		s[i].Add(1)
   118  	}
   119  	return s
   120  }
   121  
   122  func (s sequence) Start() { s[0].Done() }
   123  
   124  func (s sequence) Step(n int) {
   125  	s[n].Wait()
   126  	if n+1 < len(s) {
   127  		s[n+1].Done()
   128  	}
   129  }
   130  
   131  // asyncGet runs a goroutine to do the get so it doesn't block.
   132  func asyncGet[T any](t *testing.T, seq sequence, z *synctrack.Lazy[T], pre, post int) func() T {
   133  	var wg sync.WaitGroup
   134  	var val T
   135  	wg.Add(1)
   136  	go func() {
   137  		defer wg.Done()
   138  		t.Helper()
   139  		var err error
   140  		seq.Step(pre)
   141  		val, err = z.Get()
   142  		seq.Step(post)
   143  		if err != nil {
   144  			t.Errorf("unexpected error: %v", err)
   145  		}
   146  	}()
   147  	return func() T { wg.Wait(); return val }
   148  }
   149  
   150  func TestLazySlowEval(t *testing.T) {
   151  	// This tests the case where the first invocation of eval finishes
   152  	// after a subseqent invocation. The old value should not be put into
   153  	// the cache and returned. Nor should eval be called an extra time to
   154  	// correct the old value having been placed into the cache.
   155  
   156  	seq := newSequence(10)
   157  
   158  	var getCount int64
   159  	var z synctrack.Lazy[int64]
   160  
   161  	z.Evaluate = func() (int64, error) {
   162  		count := atomic.AddInt64(&getCount, 1)
   163  		if count == 1 {
   164  			seq.Step(1)
   165  			seq.Step(6)
   166  		} else if count > 2 {
   167  			t.Helper()
   168  			t.Errorf("Eval called extra times. count=%v", count)
   169  		} else {
   170  			seq.Step(4)
   171  		}
   172  		return time.Now().UnixNano(), nil
   173  	}
   174  
   175  	seq.Start()
   176  
   177  	getA := asyncGet(t, seq, &z, 0, 7)
   178  
   179  	seq.Step(2)
   180  	z.Notify()
   181  
   182  	getB := asyncGet(t, seq, &z, 3, 5)
   183  
   184  	getC := asyncGet(t, seq, &z, 8, 9)
   185  
   186  	a, b, c := getA(), getB(), getC()
   187  	if a < b {
   188  		t.Errorf("failed to create the test condition")
   189  	}
   190  	if b != c && c == a {
   191  		t.Errorf("wrong value was cached")
   192  	}
   193  }
   194  
   195  func TestLazySlowEval2(t *testing.T) {
   196  	// This tests the case where the first invocation of eval finishes
   197  	// before a subseqent invocation. The old value should be overwritten.
   198  	// Eval should not be called an extra time to correct the wrong value
   199  	// having been placed into the cache.
   200  
   201  	seq := newSequence(11)
   202  
   203  	var getCount int64
   204  	var z synctrack.Lazy[int64]
   205  
   206  	z.Evaluate = func() (int64, error) {
   207  		count := atomic.AddInt64(&getCount, 1)
   208  		if count == 1 {
   209  			seq.Step(1)
   210  			seq.Step(5)
   211  		} else if count > 2 {
   212  			t.Helper()
   213  			t.Errorf("Eval called extra times. count=%v", count)
   214  		} else {
   215  			seq.Step(4)
   216  			seq.Step(7)
   217  		}
   218  		return time.Now().UnixNano(), nil
   219  	}
   220  
   221  	seq.Start()
   222  
   223  	getA := asyncGet(t, seq, &z, 0, 6)
   224  
   225  	seq.Step(2)
   226  
   227  	z.Notify()
   228  
   229  	getB := asyncGet(t, seq, &z, 3, 8)
   230  
   231  	getC := asyncGet(t, seq, &z, 9, 10)
   232  
   233  	a, b, c := getA(), getB(), getC()
   234  	if a > b {
   235  		t.Errorf("failed to create the test condition")
   236  	}
   237  	if b != c && c == a {
   238  		t.Errorf("wrong value was cached")
   239  	}
   240  }
   241  
   242  func TestLazyOnlyOnce(t *testing.T) {
   243  	// This demonstrates that multiple Gets don't cause multiple Evaluates.
   244  
   245  	seq := newSequence(8)
   246  
   247  	var getCount int64
   248  	var z synctrack.Lazy[int64]
   249  
   250  	z.Evaluate = func() (int64, error) {
   251  		count := atomic.AddInt64(&getCount, 1)
   252  		if count == 1 {
   253  			seq.Step(1)
   254  			seq.Step(4)
   255  		} else if count > 1 {
   256  			t.Helper()
   257  			t.Errorf("Eval called extra times. count=%v", count)
   258  		}
   259  		return time.Now().UnixNano(), nil
   260  	}
   261  
   262  	seq.Start()
   263  
   264  	z.Notify()
   265  
   266  	getA := asyncGet(t, seq, &z, 0, 5)
   267  	getB := asyncGet(t, seq, &z, 2, 6)
   268  	getC := asyncGet(t, seq, &z, 3, 7)
   269  
   270  	a, b, c := getA(), getB(), getC()
   271  	if a > b {
   272  		t.Errorf("failed to create the test condition")
   273  	}
   274  	if b != c && c == a {
   275  		t.Errorf("wrong value was cached")
   276  	}
   277  }
   278  
   279  func TestLazyError(t *testing.T) {
   280  	var succeed bool
   281  	var z synctrack.Lazy[bool]
   282  	z.Evaluate = func() (bool, error) {
   283  		if succeed {
   284  			return true, nil
   285  		} else {
   286  			return false, errors.New("deliberate fail")
   287  		}
   288  	}
   289  
   290  	if _, err := z.Get(); err == nil {
   291  		t.Fatalf("expected error")
   292  	}
   293  	// Note: no notify, proving the error was not cached
   294  	succeed = true
   295  	if _, err := z.Get(); err != nil {
   296  		t.Fatalf("unexpected error")
   297  	}
   298  }