github.com/jxskiss/gopkg/v2@v2.14.9-0.20240514120614-899f3e7952b4/exp/kvutil/model_cache_test.go (about)

     1  package kvutil
     2  
     3  import (
     4  	"context"
     5  	"strconv"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  
    11  	"github.com/jxskiss/gopkg/v2/easy"
    12  	"github.com/jxskiss/gopkg/v2/perf/lru"
    13  )
    14  
    15  var (
    16  	testModelList = []*TestModel{
    17  		{
    18  			IntId: 111,
    19  			StrId: "aaa",
    20  		},
    21  		{
    22  			IntId: 112,
    23  			StrId: "aab",
    24  		},
    25  	}
    26  	testModelMapInt = map[int64]*TestModel{
    27  		113: {
    28  			IntId: 113,
    29  			StrId: "aac",
    30  		},
    31  	}
    32  	testModelMapStr = map[string]*TestModel{
    33  		"aac": {
    34  			IntId: 113,
    35  			StrId: "aac",
    36  		},
    37  	}
    38  	testIntIds       = []int64{111, 112, 113}
    39  	testStrIds       = []string{"aaa", "aab", "aac"}
    40  	testDeleteIntIds = []int64{111, 112}
    41  	testDeleteStrIds = []string{"aab", "aac"}
    42  )
    43  
    44  func TestCache(t *testing.T) {
    45  	mcInt := makeTestingCache("TestIntCache",
    46  		func(m *TestModel) int64 {
    47  			return m.IntId
    48  		})
    49  	mcStr := makeTestingCache("TestStrCache",
    50  		func(m *TestModel) string {
    51  			return m.StrId
    52  		})
    53  
    54  	ctx := context.Background()
    55  	var modelList []*TestModel
    56  	var modelIntMap map[int64]*TestModel
    57  	var modelStrMap map[string]*TestModel
    58  	var err error
    59  
    60  	modelList, err = mcInt.MGetSlice(ctx, testIntIds)
    61  	assert.Nil(t, err)
    62  	assert.Len(t, modelList, 0)
    63  
    64  	modelList, err = mcStr.MGetSlice(ctx, testStrIds)
    65  	assert.Nil(t, err)
    66  	assert.Len(t, modelList, 0)
    67  
    68  	modelIntMap, err = mcInt.MGetMap(ctx, testIntIds)
    69  	assert.Nil(t, err)
    70  	assert.Len(t, modelIntMap, 0)
    71  
    72  	modelStrMap, err = mcStr.MGetMap(ctx, testStrIds)
    73  	assert.Nil(t, err)
    74  	assert.Len(t, modelStrMap, 0)
    75  
    76  	// we can populate cache using either a list or a map
    77  	err = mcInt.MSetSlice(ctx, testModelList, 0)
    78  	assert.Nil(t, err)
    79  	err = mcInt.MSetMap(ctx, testModelMapInt, 0)
    80  	assert.Nil(t, err)
    81  
    82  	err = mcStr.MSetSlice(ctx, testModelList, 0)
    83  	assert.Nil(t, err)
    84  	err = mcStr.MSetMap(ctx, testModelMapStr, 0)
    85  	assert.Nil(t, err)
    86  
    87  	modelList, err = mcInt.MGetSlice(ctx, testIntIds)
    88  	assert.Nil(t, err)
    89  	assert.Len(t, modelList, 3)
    90  
    91  	modelList, err = mcStr.MGetSlice(ctx, testStrIds)
    92  	assert.Nil(t, err)
    93  	assert.Len(t, modelList, 3)
    94  
    95  	modelIntMap, err = mcInt.MGetMap(ctx, testIntIds)
    96  	assert.Nil(t, err)
    97  	assert.Len(t, modelIntMap, 3)
    98  
    99  	modelStrMap, err = mcStr.MGetMap(ctx, testStrIds)
   100  	assert.Nil(t, err)
   101  	assert.Len(t, modelStrMap, 3)
   102  
   103  	err = mcInt.Delete(ctx, testDeleteIntIds...)
   104  	assert.Nil(t, err)
   105  	assert.Len(t, mcInt.config.Storage(ctx).(*memoryStorage).data, 1)
   106  
   107  	err = mcStr.Delete(ctx, testDeleteStrIds...)
   108  	assert.Nil(t, err)
   109  	assert.Len(t, mcStr.config.Storage(ctx).(*memoryStorage).data, 1)
   110  }
   111  
   112  func TestCacheWithLRUCache(t *testing.T) {
   113  	mc := makeTestingCache("TestCacheWithLRUCache",
   114  		func(m *TestModel) int64 {
   115  			return m.IntId
   116  		})
   117  	mc.config.LRUCache = lru.NewCache[int64, *TestModel](10)
   118  	mc.config.LRUExpiration = time.Minute
   119  
   120  	ctx := context.Background()
   121  	var modelList []*TestModel
   122  	var modelMap map[int64]*TestModel
   123  	var err error
   124  
   125  	modelList, err = mc.MGetSlice(ctx, testIntIds)
   126  	assert.Nil(t, err)
   127  	assert.Len(t, modelList, 0)
   128  
   129  	err = mc.MSetSlice(ctx, testModelList, 0)
   130  	assert.Nil(t, err)
   131  	assert.Len(t, mc.config.Storage(ctx).(*memoryStorage).data, 2)
   132  
   133  	got1, exists1, expired1 := mc.config.LRUCache.Get(111)
   134  	assert.NotNil(t, got1)
   135  	assert.True(t, exists1 && !expired1)
   136  
   137  	got2, exists2, expired2 := mc.config.LRUCache.Get(112)
   138  	assert.NotNil(t, got2)
   139  	assert.True(t, exists2 && !expired2)
   140  
   141  	got3, exists3, expired3 := mc.config.LRUCache.Get(113)
   142  	assert.Nil(t, got3)
   143  	assert.False(t, exists3 || expired3)
   144  
   145  	modelList, err = mc.MGetSlice(ctx, testIntIds)
   146  	assert.Nil(t, err)
   147  	assert.Len(t, modelList, 2)
   148  
   149  	modelMap, err = mc.MGetMap(ctx, testIntIds)
   150  	assert.Nil(t, err)
   151  	assert.Len(t, modelMap, 2)
   152  
   153  	err = mc.Delete(ctx, testIntIds...)
   154  	assert.Nil(t, err)
   155  	assert.Len(t, mc.config.Storage(ctx).(*memoryStorage).data, 0)
   156  	got1, exists1, expired1 = mc.config.LRUCache.Get(111)
   157  	assert.Nil(t, got1)
   158  	assert.False(t, exists1 || expired1)
   159  }
   160  
   161  func TestCacheWithLoader(t *testing.T) {
   162  	mc := makeTestingCache("TestCacheWithLoader",
   163  		func(m *TestModel) int64 {
   164  			return m.IntId
   165  		})
   166  	mc.config.LRUCache = lru.NewShardedCache[int64, *TestModel](4, 30)
   167  	mc.config.LRUExpiration = time.Second
   168  	mc.config.Loader = testLoaderFunc
   169  	mc.config.CacheExpiration = time.Hour
   170  
   171  	ctx := context.Background()
   172  	var modelList []*TestModel
   173  	var modelMap map[int64]*TestModel
   174  	var err error
   175  
   176  	modelList, err = mc.MGetSlice(ctx, []int64{111, 112, 113})
   177  	assert.Nil(t, err)
   178  	assert.Len(t, modelList, 2)
   179  	assert.Equal(t, 2, mc.config.LRUCache.Len())
   180  	_, exists := mc.config.LRUCache.GetNotStale(int64(111))
   181  	assert.True(t, exists)
   182  	_, exists = mc.config.LRUCache.GetNotStale(int64(112))
   183  	assert.False(t, exists)
   184  	_, exists = mc.config.LRUCache.GetNotStale(int64(113))
   185  	assert.True(t, exists)
   186  
   187  	time.Sleep(mc.config.LRUExpiration)
   188  	_, exists = mc.config.LRUCache.GetNotStale(int64(111))
   189  	assert.False(t, exists)
   190  	_, exists = mc.config.LRUCache.GetNotStale(int64(112))
   191  	assert.False(t, exists)
   192  	_, exists = mc.config.LRUCache.GetNotStale(int64(113))
   193  	assert.False(t, exists)
   194  
   195  	moreIntIds := []int64{111, 112, 113, 114, 115, 116, 117}
   196  	modelMap, err = mc.MGetMap(ctx, moreIntIds)
   197  	assert.Nil(t, err)
   198  	assert.Len(t, modelMap, 4)
   199  	assert.Equal(t, 4, mc.config.LRUCache.Len())
   200  	assert.ElementsMatch(t, []int64{111, 113, 115, 117}, easy.Keys(modelMap, nil))
   201  
   202  	cacheKeys := make([]string, len(moreIntIds))
   203  	for i, id := range moreIntIds {
   204  		cacheKeys[i] = mc.config.KeyFunc(id)
   205  	}
   206  	fromCache, err := mc.config.Storage(ctx).MGet(ctx, cacheKeys...)
   207  	assert.Nil(t, err)
   208  	assert.Len(t, fromCache, len(cacheKeys))
   209  	valid := easy.Filter(func(_ int, elem []byte) bool { return len(elem) > 0 }, fromCache)
   210  	assert.Len(t, valid, 4)
   211  }
   212  
   213  func TestCacheSingleKeyValue(t *testing.T) {
   214  	mc := makeTestingCache("TestCacheSingleKeyValue",
   215  		func(m *TestModel) int64 {
   216  			return m.IntId
   217  		})
   218  	mc.config.LRUCache = lru.NewCache[int64, *TestModel](5)
   219  	mc.config.LRUExpiration = time.Second
   220  	mc.config.Loader = testLoaderFunc
   221  	mc.config.CacheExpiration = time.Hour
   222  
   223  	ctx := context.Background()
   224  	val111, err := mc.Get(ctx, 111)
   225  	assert.Nil(t, err)
   226  	assert.Equal(t, int64(111), val111.IntId)
   227  	_, exists := mc.config.LRUCache.GetNotStale(111)
   228  	assert.True(t, exists)
   229  
   230  	mc.config.LRUCache.Delete(111)
   231  	val111, err = mc.Get(ctx, 111)
   232  	assert.Nil(t, err)
   233  	assert.Equal(t, int64(111), val111.IntId)
   234  	_, exists = mc.config.LRUCache.GetNotStale(111)
   235  	assert.True(t, exists)
   236  
   237  	val112, err := mc.Get(ctx, 112)
   238  	assert.Equal(t, ErrDataNotFound, err)
   239  	assert.Nil(t, val112)
   240  	_, exists = mc.config.LRUCache.GetNotStale(112)
   241  	assert.False(t, exists)
   242  
   243  	err = mc.Delete(ctx, 111)
   244  	assert.Nil(t, err)
   245  	_, exists = mc.config.LRUCache.GetNotStale(111)
   246  	assert.False(t, exists)
   247  }
   248  
   249  func makeTestingCache[K comparable, V Model](testName string, idFunc func(V) K) *Cache[K, V] {
   250  	kf := KeyFactory{}
   251  	return NewCache(&CacheConfig[K, V]{
   252  		Storage:         testClientFunc(testName),
   253  		IDFunc:          idFunc,
   254  		KeyFunc:         kf.NewKey("test_model:{id}"),
   255  		MGetBatchSize:   2,
   256  		MSetBatchSize:   2,
   257  		DeleteBatchSize: 2,
   258  	})
   259  }
   260  
   261  func testClientFunc(testName string) func(ctx context.Context) Storage {
   262  	data := make(map[string][]byte)
   263  	return func(ctx context.Context) Storage {
   264  		return &memoryStorage{data: data}
   265  	}
   266  }
   267  
   268  type memoryStorage struct {
   269  	data map[string][]byte
   270  }
   271  
   272  func (m *memoryStorage) MGet(ctx context.Context, keys ...string) ([][]byte, error) {
   273  	out := make([][]byte, 0, len(keys))
   274  	for _, k := range keys {
   275  		out = append(out, m.data[k])
   276  	}
   277  	return out, nil
   278  }
   279  
   280  func (m *memoryStorage) MSet(ctx context.Context, kvPairs []KVPair, expiration time.Duration) error {
   281  	for _, kv := range kvPairs {
   282  		m.data[kv.K] = kv.V
   283  	}
   284  	return nil
   285  }
   286  
   287  func (m *memoryStorage) Delete(ctx context.Context, keys ...string) error {
   288  	for _, k := range keys {
   289  		delete(m.data, k)
   290  	}
   291  	return nil
   292  }
   293  
   294  func clearMemoryStorage(ctx context.Context, cliFunc func(ctx context.Context) Storage) {
   295  	stor := cliFunc(ctx).(*memoryStorage)
   296  	stor.data = make(map[string][]byte)
   297  }
   298  
   299  func getMemoryStorage(ctx context.Context, cliFunc func(ctx context.Context) Storage) *memoryStorage {
   300  	stor := cliFunc(ctx).(*memoryStorage)
   301  	return stor
   302  }
   303  
   304  type TestModel struct {
   305  	IntId int64
   306  	StrId string
   307  }
   308  
   309  func (m *TestModel) MarshalBinary() ([]byte, error) {
   310  	var buf []byte
   311  	buf = append(buf, []byte(strconv.FormatInt(m.IntId, 10))...)
   312  	buf = append(buf, []byte(m.StrId)...)
   313  	return buf, nil
   314  }
   315  
   316  func (m *TestModel) UnmarshalBinary(b []byte) error {
   317  	m.IntId, _ = strconv.ParseInt(string(b[:3]), 10, 64)
   318  	m.StrId = string(b[3:6])
   319  	return nil
   320  }
   321  
   322  func testLoaderFunc(ctx context.Context, ids []int64) (map[int64]*TestModel, error) {
   323  	out := make(map[int64]*TestModel, len(ids))
   324  	for _, id := range ids {
   325  		if id%2 == 0 {
   326  			continue
   327  		}
   328  		out[id] = &TestModel{
   329  			IntId: id,
   330  			StrId: strconv.FormatInt(id, 10),
   331  		}
   332  	}
   333  	return out, nil
   334  }