diff options
-rw-r--r-- | conn.go | 215 | ||||
-rw-r--r-- | conn_test.go | 120 | ||||
-rw-r--r-- | encoding.go | 16 | ||||
-rw-r--r-- | encoding_test.go | 16 | ||||
-rw-r--r-- | evict.go | 2 | ||||
-rw-r--r-- | examples/eviction_policy_persistant/main.go | 94 | ||||
-rw-r--r-- | store.go | 4 |
7 files changed, 359 insertions, 108 deletions
@@ -21,16 +21,22 @@ type cache struct { err error } -// Option is a function type for configuring the db. +// Option is a function type for configuring the cache. type Option func(*cache) error -// openFile opens a file-backed cache database with the given options. -func openFile(filename string, options ...Option) (*cache, error) { - ret, err := openMem(options...) - if err != nil { +// open opens a file-backed cache database with the given options. +func open(filename string, options ...Option) (*cache, error) { + ret := &cache{} + ret.Store.Init() + + if err := ret.SetConfig(options...); err != nil { return nil, err } + if filename == "" { + return ret, nil + } + file, err := lockedfile.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0o666) if err != nil { return nil, err @@ -58,18 +64,6 @@ func openFile(filename string, options ...Option) (*cache, error) { return ret, nil } -// openMem initializes an in-memory cache database with the given options. -func openMem(options ...Option) (*cache, error) { - ret := &cache{} - ret.Store.Init() - - if err := ret.SetConfig(options...); err != nil { - return nil, err - } - - return ret, nil -} - // start begins the background worker for periodic tasks. func (c *cache) start() { c.Stop = make(chan struct{}) @@ -79,7 +73,7 @@ func (c *cache) start() { go c.backgroundWorker() } -// SetConfig applies configuration options to the db. +// SetConfig applies configuration options to the cache. func (c *cache) SetConfig(options ...Option) error { c.Store.Lock.Lock() defer c.Store.Lock.Unlock() @@ -209,34 +203,131 @@ func (c *cache) Clear() { var ErrKeyNotFound = errors.New("key not found") // ErrKeyNotFound is returned when a key is not found in the cache. -// The Cache database. Can be initialized by either OpenFile or OpenMem. Uses per Cache Locks. +// Get retrieves a value from the cache by key and returns its TTL. +func (c *cache) Get(key []byte, value *[]byte) (time.Duration, error) { + v, ttl, err := c.GetValue(key) + *value = v + + return ttl, err +} + +// GetValue retrieves a value from the cache by key and returns the value and its TTL. +func (c *cache) GetValue(key []byte) ([]byte, time.Duration, error) { + if err := c.err; err != nil { + return zero[[]byte](), 0, err + } + + v, ttl, ok := c.Store.Get(key) + if !ok { + return v, 0, ErrKeyNotFound + } + + return v, ttl, nil +} + +// Set adds a key-value pair to the cache with a specified TTL. +func (c *cache) Set(key, value []byte, ttl time.Duration) error { + if err := c.err; err != nil { + return err + } + + c.Store.Set(key, value, ttl) + + return nil +} + +// Delete removes a key-value pair from the cache. +func (c *cache) Delete(key []byte) error { + ok := c.Store.Delete(key) + if !ok { + return ErrKeyNotFound + } + + return nil +} + +// UpdateInPlace retrieves a value from the cache, processes it using the provided function, +// and then sets the result back into the cache with the same key. +func (c *cache) UpdateInPlace(key []byte, processFunc func([]byte) ([]byte, error), ttl time.Duration) error { + if err := c.err; err != nil { + return err + } + + return c.Store.UpdateInPlace(key, processFunc, ttl) +} + +// Memorize attempts to retrieve a value from the cache. If the retrieval fails, +// it sets the result of the factory function into the cache and returns that result. +func (c *cache) Memorize(key []byte, factoryFunc func() ([]byte, error), ttl time.Duration) ([]byte, error) { + if err := c.err; err != nil { + return []byte{}, err + } + + return c.Store.Memorize(key, factoryFunc, ttl) +} + +// The Cache database. Can be initialized by either Open or OpenFile or OpenMem. Uses per Cache Locks. // Cache represents a generic cache database with key-value pairs. type Cache[K any, V any] struct { *cache } -// OpenFile opens a file-backed cache database with the specified options. -func OpenFile[K any, V any](filename string, options ...Option) (Cache[K, V], error) { - ret, err := openFile(filename, options...) +// The CacheRaw database. Can be initialized by either OpenRaw or OpenRawFile or OpenRawMem. Uses per Cache Locks. +// CacheRaw represents a binary cache database with key-value pairs. +type CacheRaw struct { + *cache +} + +// OpenRaw opens a binary cache database with the specified options. If filename is empty then in-memory otherwise file backed. +func OpenRaw(filename string, options ...Option) (CacheRaw, error) { + ret, err := open(filename, options...) if err != nil { - return zero[Cache[K, V]](), err + return zero[CacheRaw](), err } ret.start() - return Cache[K, V]{cache: ret}, nil + return CacheRaw{cache: ret}, nil } -// OpenMem initializes an in-memory cache database with the specified options. -func OpenMem[K any, V any](options ...Option) (Cache[K, V], error) { - ret, err := openMem(options...) +var ErrEmptyFilename = errors.New("cannot open empty filename") + +// OpenRawFile opens a binary file-backed cache database with the specified options. +func OpenRawFile(filename string, options ...Option) (CacheRaw, error) { + if filename == "" { + return zero[CacheRaw](), ErrEmptyFilename + } + + return OpenRaw(filename, options...) +} + +// OpenRawMem initializes a binary in-memory cache database with the specified options. +func OpenRawMem(options ...Option) (CacheRaw, error) { + return OpenRaw("", options...) +} + +// Open opens a cache database with the specified options. If filename is empty then in-memory otherwise file backed. +func Open[K, V any](filename string, options ...Option) (Cache[K, V], error) { + ret, err := OpenRaw(filename, options...) if err != nil { return zero[Cache[K, V]](), err } - ret.start() + return Cache[K, V]{cache: ret.cache}, nil +} + +// OpenFile opens a file-backed cache database with the specified options. +func OpenFile[K, V any](filename string, options ...Option) (Cache[K, V], error) { + if filename == "" { + return zero[Cache[K, V]](), ErrEmptyFilename + } - return Cache[K, V]{cache: ret}, nil + return Open[K, V](filename, options...) +} + +// OpenMem initializes an in-memory cache database with the specified options. +func OpenMem[K, V any](options ...Option) (Cache[K, V], error) { + return Open[K, V]("", options...) } // marshal serializes a value using msgpack. @@ -250,21 +341,17 @@ func unmarshal[T any](data []byte, v *T) error { } // Get retrieves a value from the cache by key and returns its TTL. -func (c *Cache[K, V]) Get(key K, value *V) (time.Duration, error) { +func (c Cache[K, V]) Get(key K, value *V) (time.Duration, error) { keyData, err := marshal(key) if err != nil { return 0, err } - if err := c.err; err != nil { + v, ttl, err := c.cache.GetValue(keyData) + if err != nil { return 0, err } - v, ttl, ok := c.Store.Get(keyData) - if !ok { - return 0, ErrKeyNotFound - } - if v != nil { if err = unmarshal(v, value); err != nil { return 0, err @@ -275,7 +362,7 @@ func (c *Cache[K, V]) Get(key K, value *V) (time.Duration, error) { } // GetValue retrieves a value from the cache by key and returns the value and its TTL. -func (c *Cache[K, V]) GetValue(key K) (V, time.Duration, error) { +func (c Cache[K, V]) GetValue(key K) (V, time.Duration, error) { value := zero[V]() ttl, err := c.Get(key, &value) @@ -283,7 +370,7 @@ func (c *Cache[K, V]) GetValue(key K) (V, time.Duration, error) { } // Set adds a key-value pair to the cache with a specified TTL. -func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) error { +func (c Cache[K, V]) Set(key K, value V, ttl time.Duration) error { keyData, err := marshal(key) if err != nil { return err @@ -294,47 +381,28 @@ func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) error { return err } - if err := c.err; err != nil { - return err - } - - c.Store.Set(keyData, valueData, ttl) - - return nil + return c.cache.Set(keyData, valueData, ttl) } // Delete removes a key-value pair from the cache. -func (c *Cache[K, V]) Delete(key K) error { +func (c Cache[K, V]) Delete(key K) error { keyData, err := marshal(key) if err != nil { return err } - if err := c.err; err != nil { - return err - } - - ok := c.Store.Delete(keyData) - if !ok { - return ErrKeyNotFound - } - - return nil + return c.cache.Delete(keyData) } // UpdateInPlace retrieves a value from the cache, processes it using the provided function, // and then sets the result back into the cache with the same key. -func (c *Cache[K, V]) UpdateInPlace(key K, processFunc func(V) (V, error), ttl time.Duration) error { +func (c Cache[K, V]) UpdateInPlace(key K, processFunc func(V) (V, error), ttl time.Duration) error { keyData, err := marshal(key) if err != nil { return err } - if err := c.err; err != nil { - return err - } - - return c.Store.UpdateInPlace(keyData, func(data []byte) ([]byte, error) { + return c.cache.UpdateInPlace(keyData, func(data []byte) ([]byte, error) { var value V if err := unmarshal(data, &value); err != nil { return nil, err @@ -351,17 +419,13 @@ func (c *Cache[K, V]) UpdateInPlace(key K, processFunc func(V) (V, error), ttl t // Memorize attempts to retrieve a value from the cache. If the retrieval fails, // it sets the result of the factory function into the cache and returns that result. -func (c *Cache[K, V]) Memorize(key K, factoryFunc func() (V, error), ttl time.Duration) (V, error) { +func (c Cache[K, V]) Memorize(key K, factoryFunc func() (V, error), ttl time.Duration) (V, error) { keyData, err := marshal(key) if err != nil { return zero[V](), err } - if err := c.err; err != nil { - return zero[V](), err - } - - data, err := c.Store.Memorize(keyData, func() ([]byte, error) { + data, err := c.cache.Memorize(keyData, func() ([]byte, error) { value, err := factoryFunc() if err != nil { return nil, err @@ -380,3 +444,18 @@ func (c *Cache[K, V]) Memorize(key K, factoryFunc func() (V, error), ttl time.Du return value, nil } + +type Cacher[K any, V any] interface { + Clear() + Close() error + Cost() uint64 + Delete(key K) error + Error() error + Flush() error + Get(key K, value *V) (time.Duration, error) + GetValue(key K) (V, time.Duration, error) + Memorize(key K, factoryFunc func() (V, error), ttl time.Duration) (V, error) + Set(key K, value V, ttl time.Duration) error + SetConfig(options ...Option) error + UpdateInPlace(key K, processFunc func(V) (V, error), ttl time.Duration) error +} diff --git a/conn_test.go b/conn_test.go index 47bdde4..cfcad13 100644 --- a/conn_test.go +++ b/conn_test.go @@ -7,7 +7,7 @@ import ( "time" ) -func setupTestDB[K any, V any](tb testing.TB) *Cache[K, V] { +func setupTestCache[K, V any](tb testing.TB) *Cache[K, V] { tb.Helper() db, err := OpenMem[K, V]() @@ -24,13 +24,91 @@ func setupTestDB[K any, V any](tb testing.TB) *Cache[K, V] { return &db } -func TestDBGetSet(t *testing.T) { +func TestCacheSetConfig(t *testing.T) { + tests := []struct { + name string + options []Option + wantErr bool + expectedPolicy EvictionPolicyType + expectedMaxCost uint64 + snapshotTime time.Duration + cleanupTime time.Duration + }{ + { + name: "Set all valid options", + options: []Option{ + WithPolicy(PolicyLRU), + WithMaxCost(10000), + SetSnapshotTime(2 * time.Minute), + SetCleanupTime(30 * time.Second), + }, + wantErr: false, + expectedPolicy: PolicyLRU, + expectedMaxCost: 10000, + snapshotTime: 2 * time.Minute, + cleanupTime: 30 * time.Second, + }, + { + name: "Invalid policy returns error", + options: []Option{ + WithPolicy(-1), + }, + wantErr: true, + }, + { + name: "Set only max cost", + options: []Option{ + WithMaxCost(2048), + }, + wantErr: false, + expectedMaxCost: 2048, + }, + { + name: "Set only snapshot and cleanup", + options: []Option{ + SetSnapshotTime(15 * time.Second), + SetCleanupTime(1 * time.Minute), + }, + wantErr: false, + snapshotTime: 15 * time.Second, + cleanupTime: 1 * time.Minute, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := setupTestCache[string, string](t) + + err := c.SetConfig(tt.options...) + if (err != nil) != tt.wantErr { + t.Fatalf("SetConfig() error = %v, wantErr = %v", err, tt.wantErr) + } + + if !tt.wantErr { + if c.Store.Policy.Type != tt.expectedPolicy { + t.Errorf("Expected policy %v, got %v", tt.expectedPolicy, c.Store.Policy.Type) + } + if tt.expectedMaxCost != 0 && c.Store.MaxCost != tt.expectedMaxCost { + t.Errorf("Expected MaxCost %d, got %d", tt.expectedMaxCost, c.Store.MaxCost) + } + if tt.snapshotTime != 0 && c.Store.SnapshotTicker.GetDuration() != tt.snapshotTime { + t.Errorf("Expected SnapshotTime %v, got %v", tt.snapshotTime, c.Store.SnapshotTicker.GetDuration()) + } + if tt.cleanupTime != 0 && c.Store.CleanupTicker.GetDuration() != tt.cleanupTime { + t.Errorf("Expected CleanupTime %v, got %v", tt.cleanupTime, c.Store.CleanupTicker.GetDuration()) + } + } + }) + } +} + +func TestCacheGetSet(t *testing.T) { t.Parallel() t.Run("Exists", func(t *testing.T) { t.Parallel() - db := setupTestDB[string, string](t) + db := setupTestCache[string, string](t) want := "Value" @@ -55,7 +133,7 @@ func TestDBGetSet(t *testing.T) { t.Run("Not Exists", func(t *testing.T) { t.Parallel() - db := setupTestDB[string, string](t) + db := setupTestCache[string, string](t) if _, _, err := db.GetValue("Key"); !errors.Is(err, ErrKeyNotFound) { t.Fatalf("expected error: %v, got: %v", ErrKeyNotFound, err) @@ -65,7 +143,7 @@ func TestDBGetSet(t *testing.T) { t.Run("Update", func(t *testing.T) { t.Parallel() - db := setupTestDB[string, string](t) + db := setupTestCache[string, string](t) if err := db.Set("Key", "Other", 0); err != nil { t.Fatalf("expected no error, got: %v", err) @@ -89,7 +167,7 @@ func TestDBGetSet(t *testing.T) { t.Run("Key Expiry", func(t *testing.T) { t.Parallel() - db := setupTestDB[string, string](t) + db := setupTestCache[string, string](t) if err := db.Set("Key", "Value", 500*time.Millisecond); err != nil { t.Fatalf("unexpected error: %v", err) @@ -103,13 +181,13 @@ func TestDBGetSet(t *testing.T) { }) } -func TestDBDelete(t *testing.T) { +func TestCacheDelete(t *testing.T) { t.Parallel() t.Run("Exists", func(t *testing.T) { t.Parallel() - db := setupTestDB[string, string](t) + db := setupTestCache[string, string](t) want := "Value" if err := db.Set("Key", want, 0); err != nil { @@ -128,7 +206,7 @@ func TestDBDelete(t *testing.T) { t.Run("Not Exists", func(t *testing.T) { t.Parallel() - db := setupTestDB[string, string](t) + db := setupTestCache[string, string](t) if err := db.Delete("Key"); !errors.Is(err, ErrKeyNotFound) { t.Fatalf("expected error: %v, got: %v", ErrKeyNotFound, err) @@ -136,13 +214,13 @@ func TestDBDelete(t *testing.T) { }) } -func TestDBUpdateInPlace(t *testing.T) { +func TestCacheUpdateInPlace(t *testing.T) { t.Parallel() t.Run("Exists", func(t *testing.T) { t.Parallel() - store := setupTestDB[string, string](t) + store := setupTestCache[string, string](t) want := "Value" @@ -171,7 +249,7 @@ func TestDBUpdateInPlace(t *testing.T) { t.Run("Not Exists", func(t *testing.T) { t.Parallel() - store := setupTestDB[string, string](t) + store := setupTestCache[string, string](t) want := "Value" @@ -185,13 +263,13 @@ func TestDBUpdateInPlace(t *testing.T) { }) } -func TestDBMemoize(t *testing.T) { +func TestCacheMemoize(t *testing.T) { t.Parallel() t.Run("Cache Miss", func(t *testing.T) { t.Parallel() - store := setupTestDB[string, string](t) + store := setupTestCache[string, string](t) want := "Value" @@ -221,7 +299,7 @@ func TestDBMemoize(t *testing.T) { t.Run("Cache Hit", func(t *testing.T) { t.Parallel() - store := setupTestDB[string, string](t) + store := setupTestCache[string, string](t) want := "NewValue" @@ -244,10 +322,10 @@ func TestDBMemoize(t *testing.T) { }) } -func BenchmarkDBGet(b *testing.B) { +func BenchmarkCacheGet(b *testing.B) { for n := 1; n <= 100000; n *= 10 { b.Run(strconv.Itoa(n), func(b *testing.B) { - db := setupTestDB[int, int](b) + db := setupTestCache[int, int](b) for i := range n { if err := db.Set(i, i, 0); err != nil { b.Fatalf("unexpected error: %v", err) @@ -265,10 +343,10 @@ func BenchmarkDBGet(b *testing.B) { } } -func BenchmarkDBSet(b *testing.B) { +func BenchmarkCacheSet(b *testing.B) { for n := 1; n <= 100000; n *= 10 { b.Run(strconv.Itoa(n), func(b *testing.B) { - db := setupTestDB[int, int](b) + db := setupTestCache[int, int](b) for i := range n - 1 { if err := db.Set(i, i, 0); err != nil { b.Fatalf("unexpected error: %v", err) @@ -286,10 +364,10 @@ func BenchmarkDBSet(b *testing.B) { } } -func BenchmarkDBDelete(b *testing.B) { +func BenchmarkCacheDelete(b *testing.B) { for n := 1; n <= 100000; n *= 10 { b.Run(strconv.Itoa(n), func(b *testing.B) { - db := setupTestDB[int, int](b) + db := setupTestCache[int, int](b) for i := range n - 1 { if err := db.Set(i, i, 0); err != nil { b.Fatalf("unexpected error: %v", err) diff --git a/encoding.go b/encoding.go index a8778ab..48d0d57 100644 --- a/encoding.go +++ b/encoding.go @@ -231,12 +231,14 @@ func (d *decoder) DecodeStore(s *store) error { return nil } -func (s *store) Snapshot(w io.WriteSeeker) error { +func (s *store) Snapshot(w io.Writer) error { s.Lock.RLock() defer s.Lock.RUnlock() - if _, err := w.Seek(0, io.SeekStart); err != nil { - return err + if seeker, ok := w.(io.Seeker); ok { + if _, err := seeker.Seek(0, io.SeekStart); err != nil { + return err + } } wr := newEncoder(w) @@ -249,9 +251,11 @@ func (s *store) Snapshot(w io.WriteSeeker) error { return wr.Flush() } -func (s *store) LoadSnapshot(r io.ReadSeeker) error { - if _, err := r.Seek(0, io.SeekStart); err != nil { - return err +func (s *store) LoadSnapshot(r io.Reader) error { + if seeker, ok := r.(io.Seeker); ok { + if _, err := seeker.Seek(0, io.SeekStart); err != nil { + return err + } } d := newDecoder(r) diff --git a/encoding_test.go b/encoding_test.go index 43abb46..037a4fd 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -252,7 +252,7 @@ func TestEncodeDecodeNode(t *testing.T) { } } -func TestEncodeDecodeStrorage(t *testing.T) { +func TestStoreSnapshot(t *testing.T) { t.Parallel() tests := []struct { @@ -297,7 +297,6 @@ func TestEncodeDecodeStrorage(t *testing.T) { t.Parallel() var buf bytes.Buffer - e := newEncoder(&buf) want := setupTestStore(t) want.MaxCost = uint64(tt.maxCost) @@ -310,18 +309,15 @@ func TestEncodeDecodeStrorage(t *testing.T) { want.Set([]byte(k), []byte(v), 0) } - if err := e.EncodeStore(want); err != nil { + if err := want.Snapshot(&buf); err != nil { t.Errorf("unexpected error: %v", err) } - if err := e.Flush(); err != nil { - t.Errorf("unexpected error: %v", err) - } + reader := bytes.NewReader(buf.Bytes()) - decoder := newDecoder(bytes.NewReader(buf.Bytes())) got := setupTestStore(t) - if err := decoder.DecodeStore(got); err != nil { + if err := got.LoadSnapshot(reader); err != nil { t.Errorf("unexpected error: %v", err) } @@ -399,7 +395,7 @@ func BenchmarkStoreSnapshot(b *testing.B) { b.Fatalf("unexpected error: %v", err) } - b.SetBytes(int64(fileInfo.Size())) + b.SetBytes(fileInfo.Size()) b.ReportAllocs() for b.Loop() { @@ -433,7 +429,7 @@ func BenchmarkStoreLoadSnapshot(b *testing.B) { b.Fatalf("unexpected error: %v", err) } - b.SetBytes(int64(fileInfo.Size())) + b.SetBytes(fileInfo.Size()) b.ReportAllocs() for b.Loop() { @@ -34,7 +34,7 @@ type evictionPolicy struct { } // pushEvict adds a node to the eviction list. -func pushEvict(node *node, sentinnel *node) { +func pushEvict(node, sentinnel *node) { node.EvictPrev = sentinnel node.EvictNext = node.EvictPrev.EvictNext node.EvictNext.EvictPrev = node diff --git a/examples/eviction_policy_persistant/main.go b/examples/eviction_policy_persistant/main.go new file mode 100644 index 0000000..fc7cc32 --- /dev/null +++ b/examples/eviction_policy_persistant/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/marcthe12/cache" +) + +func main() { + // Create an in-memory cache with LRU eviction policy + db, err := cache.Open[int, int]( + "cache.db", + cache.WithPolicy(cache.PolicyLRU), + cache.WithMaxCost(20), + cache.SetCleanupTime(1*time.Second), + ) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + defer func() { + err := db.Close() + if err != nil { + fmt.Println("Error:", err) + } + }() + + fmt.Println("Loaded Cache") + // Check which keys are present + for n := range 4 { + key := n + 1 + value, _, err := db.GetValue(key) + + if err != nil { + fmt.Printf("Key %d not found\n", key) + } else { + fmt.Printf("Key %d found with value: %d\n", key, value) + } + } + + fmt.Println("Set Values") + // Set values + if err := db.Set(1, -1, 10*time.Second); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + if err := db.Set(2, -2, 10*time.Second); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + if err := db.Set(3, -3, 10*time.Second); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + // Access some keys + if _, _, err := db.GetValue(1); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + if _, _, err := db.GetValue(2); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + fmt.Println("Add Key 4") + // Add another key to trigger eviction + if err := db.Set(4, -4, 0); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + fmt.Println("Sleep") + time.Sleep(2 * time.Second) + + fmt.Println("Resume") + // Check which keys are present + for n := range 4 { + key := n + 1 + value, _, err := db.GetValue(key) + + if err != nil { + fmt.Printf("Key %d not found\n", key) + } else { + fmt.Printf("Key %d found with value: %d\n", key, value) + } + } +} @@ -228,7 +228,7 @@ func (s *store) Evict() bool { } // insert adds a new key-value pair to the store. -func (s *store) insert(key []byte, value []byte, ttl time.Duration) { +func (s *store) insert(key, value []byte, ttl time.Duration) { idx, hash := lookupIdx(s, key) bucket := &s.Bucket[idx] @@ -264,7 +264,7 @@ func (s *store) insert(key []byte, value []byte, ttl time.Duration) { } // Set adds or updates a key-value pair in the store with locking. -func (s *store) Set(key []byte, value []byte, ttl time.Duration) { +func (s *store) Set(key, value []byte, ttl time.Duration) { s.Lock.Lock() defer s.Lock.Unlock() |