github.com/maypok86/otter@v1.2.1/cache_test.go (about)

     1  // Copyright (c) 2023 Alexey Mayshev. All rights reserved.
     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 otter
    16  
    17  import (
    18  	"container/heap"
    19  	"fmt"
    20  	"math/rand"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/maypok86/otter/internal/xruntime"
    26  )
    27  
    28  func TestCache_Set(t *testing.T) {
    29  	const size = 256
    30  	var mutex sync.Mutex
    31  	m := make(map[DeletionCause]int)
    32  	c, err := MustBuilder[int, int](size).
    33  		WithTTL(time.Minute).
    34  		CollectStats().
    35  		DeletionListener(func(key int, value int, cause DeletionCause) {
    36  			mutex.Lock()
    37  			m[cause]++
    38  			mutex.Unlock()
    39  		}).
    40  		Build()
    41  	if err != nil {
    42  		t.Fatalf("can not create cache: %v", err)
    43  	}
    44  
    45  	for i := 0; i < size; i++ {
    46  		c.Set(i, i)
    47  	}
    48  
    49  	// update
    50  	for i := 0; i < size; i++ {
    51  		c.Set(i, i)
    52  	}
    53  
    54  	parallelism := xruntime.Parallelism()
    55  	var wg sync.WaitGroup
    56  	for i := 0; i < int(parallelism); i++ {
    57  		wg.Add(1)
    58  		go func() {
    59  			defer wg.Done()
    60  
    61  			r := rand.New(rand.NewSource(time.Now().UnixNano()))
    62  			for a := 0; a < 10000; a++ {
    63  				k := r.Int() % 100
    64  				val, ok := c.Get(k)
    65  				if !ok {
    66  					err = fmt.Errorf("expected %d but got nil", k)
    67  					break
    68  				}
    69  				if val != k {
    70  					err = fmt.Errorf("expected %d but got %d", k, val)
    71  					break
    72  				}
    73  			}
    74  		}()
    75  	}
    76  	wg.Wait()
    77  
    78  	if err != nil {
    79  		t.Fatalf("not found key: %v", err)
    80  	}
    81  	ratio := c.Stats().Ratio()
    82  	if ratio != 1.0 {
    83  		t.Fatalf("cache hit ratio should be 1.0, but got %v", ratio)
    84  	}
    85  
    86  	mutex.Lock()
    87  	defer mutex.Unlock()
    88  	if len(m) != 1 || m[Replaced] != size {
    89  		t.Fatalf("cache was supposed to replace %d, but replaced %d entries", size, m[Replaced])
    90  	}
    91  }
    92  
    93  func TestCache_SetIfAbsent(t *testing.T) {
    94  	const size = 100
    95  	c, err := MustBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build()
    96  	if err != nil {
    97  		t.Fatalf("can not create cache: %v", err)
    98  	}
    99  
   100  	for i := 0; i < size; i++ {
   101  		if !c.SetIfAbsent(i, i) {
   102  			t.Fatalf("set was dropped. key: %d", i)
   103  		}
   104  	}
   105  
   106  	for i := 0; i < size; i++ {
   107  		if !c.Has(i) {
   108  			t.Fatalf("key should exists: %d", i)
   109  		}
   110  	}
   111  
   112  	for i := 0; i < size; i++ {
   113  		if c.SetIfAbsent(i, i) {
   114  			t.Fatalf("set wasn't dropped. key: %d", i)
   115  		}
   116  	}
   117  
   118  	c.Clear()
   119  
   120  	cc, err := MustBuilder[int, int](size).WithVariableTTL().CollectStats().Build()
   121  	if err != nil {
   122  		t.Fatalf("can not create cache: %v", err)
   123  	}
   124  
   125  	for i := 0; i < size; i++ {
   126  		if !cc.SetIfAbsent(i, i, time.Hour) {
   127  			t.Fatalf("set was dropped. key: %d", i)
   128  		}
   129  	}
   130  
   131  	for i := 0; i < size; i++ {
   132  		if !cc.Has(i) {
   133  			t.Fatalf("key should exists: %d", i)
   134  		}
   135  	}
   136  
   137  	for i := 0; i < size; i++ {
   138  		if cc.SetIfAbsent(i, i, time.Second) {
   139  			t.Fatalf("set wasn't dropped. key: %d", i)
   140  		}
   141  	}
   142  
   143  	if hits := cc.Stats().Hits(); hits != size {
   144  		t.Fatalf("hit ratio should be 100%%. Hits: %d", hits)
   145  	}
   146  
   147  	cc.Close()
   148  }
   149  
   150  func TestCache_SetWithTTL(t *testing.T) {
   151  	size := 256
   152  	var mutex sync.Mutex
   153  	m := make(map[DeletionCause]int)
   154  	c, err := MustBuilder[int, int](size).
   155  		InitialCapacity(size).
   156  		WithTTL(time.Second).
   157  		DeletionListener(func(key int, value int, cause DeletionCause) {
   158  			mutex.Lock()
   159  			m[cause]++
   160  			mutex.Unlock()
   161  		}).
   162  		Build()
   163  	if err != nil {
   164  		t.Fatalf("can not create builder: %v", err)
   165  	}
   166  
   167  	for i := 0; i < size; i++ {
   168  		c.Set(i, i)
   169  	}
   170  
   171  	time.Sleep(3 * time.Second)
   172  	for i := 0; i < size; i++ {
   173  		if c.Has(i) {
   174  			t.Fatalf("key should be expired: %d", i)
   175  		}
   176  	}
   177  
   178  	time.Sleep(10 * time.Millisecond)
   179  
   180  	if cacheSize := c.Size(); cacheSize != 0 {
   181  		t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0)
   182  	}
   183  
   184  	mutex.Lock()
   185  	if e := m[Expired]; len(m) != 1 || e != size {
   186  		mutex.Unlock()
   187  		t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, e)
   188  	}
   189  	mutex.Unlock()
   190  
   191  	m = make(map[DeletionCause]int)
   192  	cc, err := MustBuilder[int, int](size).
   193  		WithVariableTTL().
   194  		CollectStats().
   195  		DeletionListener(func(key int, value int, cause DeletionCause) {
   196  			mutex.Lock()
   197  			m[cause]++
   198  			mutex.Unlock()
   199  		}).
   200  		Build()
   201  	if err != nil {
   202  		t.Fatalf("can not create builder: %v", err)
   203  	}
   204  
   205  	for i := 0; i < size; i++ {
   206  		cc.Set(i, i, 5*time.Second)
   207  	}
   208  
   209  	time.Sleep(7 * time.Second)
   210  
   211  	for i := 0; i < size; i++ {
   212  		if cc.Has(i) {
   213  			t.Fatalf("key should be expired: %d", i)
   214  		}
   215  	}
   216  
   217  	time.Sleep(10 * time.Millisecond)
   218  
   219  	if cacheSize := cc.Size(); cacheSize != 0 {
   220  		t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0)
   221  	}
   222  	if misses := cc.Stats().Misses(); misses != int64(size) {
   223  		t.Fatalf("c.Stats().Misses() = %d, want = %d", misses, size)
   224  	}
   225  	mutex.Lock()
   226  	defer mutex.Unlock()
   227  	if len(m) != 1 || m[Expired] != size {
   228  		t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, m[Expired])
   229  	}
   230  }
   231  
   232  func TestCache_Delete(t *testing.T) {
   233  	size := 256
   234  	var mutex sync.Mutex
   235  	m := make(map[DeletionCause]int)
   236  	c, err := MustBuilder[int, int](size).
   237  		InitialCapacity(size).
   238  		WithTTL(time.Hour).
   239  		DeletionListener(func(key int, value int, cause DeletionCause) {
   240  			mutex.Lock()
   241  			m[cause]++
   242  			mutex.Unlock()
   243  		}).
   244  		Build()
   245  	if err != nil {
   246  		t.Fatalf("can not create builder: %v", err)
   247  	}
   248  
   249  	for i := 0; i < size; i++ {
   250  		c.Set(i, i)
   251  	}
   252  
   253  	for i := 0; i < size; i++ {
   254  		if !c.Has(i) {
   255  			t.Fatalf("key should exists: %d", i)
   256  		}
   257  	}
   258  
   259  	for i := 0; i < size; i++ {
   260  		c.Delete(i)
   261  	}
   262  
   263  	for i := 0; i < size; i++ {
   264  		if c.Has(i) {
   265  			t.Fatalf("key should not exists: %d", i)
   266  		}
   267  	}
   268  
   269  	time.Sleep(time.Second)
   270  
   271  	mutex.Lock()
   272  	defer mutex.Unlock()
   273  	if len(m) != 1 || m[Explicit] != size {
   274  		t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size, m[Explicit])
   275  	}
   276  }
   277  
   278  func TestCache_DeleteByFunc(t *testing.T) {
   279  	size := 256
   280  	var mutex sync.Mutex
   281  	m := make(map[DeletionCause]int)
   282  	c, err := MustBuilder[int, int](size).
   283  		InitialCapacity(size).
   284  		WithTTL(time.Hour).
   285  		DeletionListener(func(key int, value int, cause DeletionCause) {
   286  			mutex.Lock()
   287  			m[cause]++
   288  			mutex.Unlock()
   289  		}).
   290  		Build()
   291  	if err != nil {
   292  		t.Fatalf("can not create builder: %v", err)
   293  	}
   294  
   295  	for i := 0; i < size; i++ {
   296  		c.Set(i, i)
   297  	}
   298  
   299  	c.DeleteByFunc(func(key int, value int) bool {
   300  		return key%2 == 1
   301  	})
   302  
   303  	c.Range(func(key int, value int) bool {
   304  		if key%2 == 1 {
   305  			t.Fatalf("key should be odd, but got: %d", key)
   306  		}
   307  		return true
   308  	})
   309  
   310  	time.Sleep(time.Second)
   311  
   312  	expected := size / 2
   313  	mutex.Lock()
   314  	defer mutex.Unlock()
   315  	if len(m) != 1 || m[Explicit] != expected {
   316  		t.Fatalf("cache was supposed to delete %d, but deleted %d entries", expected, m[Explicit])
   317  	}
   318  }
   319  
   320  func TestCache_Advanced(t *testing.T) {
   321  	size := 256
   322  	defaultTTL := time.Hour
   323  	c, err := MustBuilder[int, int](size).
   324  		WithTTL(defaultTTL).
   325  		Build()
   326  	if err != nil {
   327  		t.Fatalf("can not create builder: %v", err)
   328  	}
   329  
   330  	for i := 0; i < size; i++ {
   331  		c.Set(i, i)
   332  	}
   333  
   334  	k1 := 4
   335  	v1, ok := c.Extension().GetQuietly(k1)
   336  	if !ok {
   337  		t.Fatalf("not found key %d", k1)
   338  	}
   339  
   340  	e1, ok := c.Extension().GetEntryQuietly(k1)
   341  	if !ok {
   342  		t.Fatalf("not found key %d", k1)
   343  	}
   344  
   345  	e2, ok := c.Extension().GetEntry(k1)
   346  	if !ok {
   347  		t.Fatalf("not found key %d", k1)
   348  	}
   349  
   350  	time.Sleep(time.Second)
   351  
   352  	isValidEntries := e1.Key() == k1 &&
   353  		e1.Value() == v1 &&
   354  		e1.Cost() == 1 &&
   355  		e1 == e2 &&
   356  		e1.TTL() < defaultTTL &&
   357  		!e1.HasExpired()
   358  
   359  	if !isValidEntries {
   360  		t.Fatalf("found not valid entries. e1: %+v, e2: %+v, v1:%d", e1, e2, v1)
   361  	}
   362  
   363  	if _, ok := c.Extension().GetQuietly(size); ok {
   364  		t.Fatalf("found not valid key: %d", size)
   365  	}
   366  	if _, ok := c.Extension().GetEntryQuietly(size); ok {
   367  		t.Fatalf("found not valid key: %d", size)
   368  	}
   369  	if _, ok := c.Extension().GetEntry(size); ok {
   370  		t.Fatalf("found not valid key: %d", size)
   371  	}
   372  }
   373  
   374  func TestCache_Ratio(t *testing.T) {
   375  	var mutex sync.Mutex
   376  	m := make(map[DeletionCause]int)
   377  	c, err := MustBuilder[uint64, uint64](100).
   378  		CollectStats().
   379  		DeletionListener(func(key uint64, value uint64, cause DeletionCause) {
   380  			mutex.Lock()
   381  			m[cause]++
   382  			mutex.Unlock()
   383  		}).
   384  		Build()
   385  	if err != nil {
   386  		t.Fatalf("can not create cache: %v", err)
   387  	}
   388  
   389  	z := rand.NewZipf(rand.New(rand.NewSource(time.Now().UnixNano())), 1.0001, 1, 1000)
   390  
   391  	o := newOptimal(100)
   392  	for i := 0; i < 10000; i++ {
   393  		k := z.Uint64()
   394  
   395  		o.Get(k)
   396  		if !c.Has(k) {
   397  			c.Set(k, k)
   398  		}
   399  	}
   400  
   401  	t.Logf("actual size: %d, capacity: %d", c.Size(), c.Capacity())
   402  	t.Logf("actual: %.2f, optimal: %.2f", c.Stats().Ratio(), o.Ratio())
   403  
   404  	mutex.Lock()
   405  	defer mutex.Unlock()
   406  	t.Logf("evicted: %d", m[Size])
   407  	if len(m) != 1 || m[Size] <= 0 || m[Size] > 5000 {
   408  		t.Fatalf("cache was supposed to evict positive number of entries, but evicted %d entries", m[Size])
   409  	}
   410  }
   411  
   412  type optimal struct {
   413  	capacity uint64
   414  	hits     map[uint64]uint64
   415  	access   []uint64
   416  }
   417  
   418  func newOptimal(capacity uint64) *optimal {
   419  	return &optimal{
   420  		capacity: capacity,
   421  		hits:     make(map[uint64]uint64),
   422  		access:   make([]uint64, 0),
   423  	}
   424  }
   425  
   426  func (o *optimal) Get(key uint64) {
   427  	o.hits[key]++
   428  	o.access = append(o.access, key)
   429  }
   430  
   431  func (o *optimal) Ratio() float64 {
   432  	look := make(map[uint64]struct{}, o.capacity)
   433  	data := &optimalHeap{}
   434  	heap.Init(data)
   435  	hits := 0
   436  	misses := 0
   437  	for _, key := range o.access {
   438  		if _, has := look[key]; has {
   439  			hits++
   440  			continue
   441  		}
   442  		if uint64(data.Len()) >= o.capacity {
   443  			victim := heap.Pop(data)
   444  			delete(look, victim.(*optimalItem).key)
   445  		}
   446  		misses++
   447  		look[key] = struct{}{}
   448  		heap.Push(data, &optimalItem{key, o.hits[key]})
   449  	}
   450  	if hits == 0 && misses == 0 {
   451  		return 0.0
   452  	}
   453  	return float64(hits) / float64(hits+misses)
   454  }
   455  
   456  type optimalItem struct {
   457  	key  uint64
   458  	hits uint64
   459  }
   460  
   461  type optimalHeap []*optimalItem
   462  
   463  func (h optimalHeap) Len() int           { return len(h) }
   464  func (h optimalHeap) Less(i, j int) bool { return h[i].hits < h[j].hits }
   465  func (h optimalHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
   466  
   467  func (h *optimalHeap) Push(x any) {
   468  	*h = append(*h, x.(*optimalItem))
   469  }
   470  
   471  func (h *optimalHeap) Pop() any {
   472  	old := *h
   473  	n := len(old)
   474  	x := old[n-1]
   475  	*h = old[0 : n-1]
   476  	return x
   477  }