From 1b7f0342255022242c8cbf42582a58653e57cc64 Mon Sep 17 00:00:00 2001 From: PraserX Date: Tue, 22 Jul 2025 14:22:07 +0200 Subject: [PATCH 1/3] feat: add Exists and Delete methods for cache management - Implement Exists method to check if a record is present in cache. - Implement Delete method to remove records from cache and handle errors appropriately. - Update tests to cover new functionality for existence checks and deletion. --- cache.go | 42 ++++++++++++++++++++++++++++++++++++ cache_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/cache.go b/cache.go index 7510c47..6c69364 100644 --- a/cache.go +++ b/cache.go @@ -157,6 +157,9 @@ func initShardsSection(shardsSection *ShardsLookup, maxShards, maxRecords, recor // are replaced. If not, it checks if there are some allocated shard with empty // space for data. If there is no empty space, new shard is allocated. Otherwise // some valid record (FIFO queue) is deleted and new one is stored. +// Remarks: +// - If expiration time is set to 0 then maximum expiration time is used (48 hours). +// - If expiration time is KeepTTL, then current expiration time is preserved. func (a *AtomicCache) Set(key string, data []byte, expire time.Duration) error { // Reject if data is too large for any shard if len(data) > int(a.RecordSizeLarge) { @@ -283,6 +286,45 @@ func (a *AtomicCache) Get(key string) ([]byte, error) { return nil, ErrNotFound } +// Exists checks if record is present in cache memory. It returns true if record +// is present, otherwise false. +func (a *AtomicCache) Exists(key string) bool { + a.RLock() + val, ok := a.lookup[key] + a.RUnlock() + if !ok { + return false + } + // Check expiration + if time.Now().After(val.Expiration) { + return false + } + return true +} + +// Delete removes record from cache memory. If record is not found, then error +// is returned. It also releases memory used by record in shard. +// If shard ends up empty, it is released. +func (a *AtomicCache) Delete(key string) error { + a.Lock() + defer a.Unlock() + + val, ok := a.lookup[key] + if !ok { + return ErrNotFound + } + + shardSection := a.getShardsSectionByID(val.ShardSection) + if shardSection.shards[val.ShardIndex] != nil { + shardSection.shards[val.ShardIndex].Free(val.RecordIndex) + a.releaseShard(val.ShardSection, val.ShardIndex) + delete(a.lookup, key) + return nil + } + + return ErrNotFound +} + // releaseShard release shard if there is no record in memory. It returns true // if shard was released. The function requires the shard section ID and // shard ID on input. diff --git a/cache_test.go b/cache_test.go index c33b967..d0d3e68 100644 --- a/cache_test.go +++ b/cache_test.go @@ -178,6 +178,66 @@ func TestCacheKeepTTL(t *testing.T) { } } +func TestCacheExists(t *testing.T) { + cache := New() + key := "exists-key" + data := []byte("exists-data") + + // Should not exist before set + if cache.Exists(key) { + t.Errorf("Exists returned true for unset key") + } + + // Set and check exists + if err := cache.Set(key, data, 10*time.Second); err != nil { + t.Fatalf("Set error: %s", err) + } + if !cache.Exists(key) { + t.Errorf("Exists returned false for set key") + } + + // Delete and check exists + if err := cache.Delete(key); err != nil { + t.Fatalf("Delete error: %s", err) + } + if cache.Exists(key) { + t.Errorf("Exists returned true after Delete") + } + + // Never-set key + if cache.Exists("never-existed") { + t.Errorf("Exists returned true for never-set key") + } +} + +func TestCacheDelete(t *testing.T) { + cache := New() + key := "del-key" + data := []byte("to-delete") + + // Set and then delete + if err := cache.Set(key, data, 0); err != nil { + t.Fatalf("Set error: %s", err) + } + if err := cache.Delete(key); err != nil { + t.Errorf("Delete error: %s", err) + } + // Should not be able to get deleted key + if _, err := cache.Get(key); err == nil { + t.Errorf("Expected error on Get after Delete, got nil") + } + + // Deleting again should return ErrNotFound + if err := cache.Delete(key); err != ErrNotFound { + t.Errorf("Expected ErrNotFound on double Delete, got %v", err) + } + + // Deleting a never-set key should return ErrNotFound + if err := cache.Delete("never-existed"); err != ErrNotFound { + t.Errorf("Expected ErrNotFound for never-set key, got %v", err) + } +} + func benchmarkCacheNew(recordCount int, b *testing.B) { b.ReportAllocs() From ae7cf2ac58fd02bd3d3a85de9c9c6881deb62d8c Mon Sep 17 00:00:00 2001 From: PraserX Date: Tue, 22 Jul 2025 14:26:35 +0200 Subject: [PATCH 2/3] chore: correct typos and improve comments in cache.go --- cache.go | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/cache.go b/cache.go index 6c69364..e07fff5 100644 --- a/cache.go +++ b/cache.go @@ -14,7 +14,7 @@ var ( ErrFullMemory = errors.New("cannot create new record: memory is full") ) -// Constans below are used for shard section identification. +// Constants below are used for shard section identification. const ( // SMSH - Small Shards section SMSH = iota + 1 @@ -38,7 +38,7 @@ type AtomicCache struct { // Lookup structure used for global index. lookup map[string]LookupRecord - // Shards lookup tables which contains information about shards sections. + // Shards lookup tables which contain information about shard sections. smallShards, mediumShards, largeShards ShardsLookup // Size of byte array used for memory allocation at small shard section. @@ -63,8 +63,8 @@ type AtomicCache struct { // Garbage collector counter for starter. GcCounter uint32 - // Buffer contains all unattended cache set requests. It has a maximum site - // which is equal to MaxRecords value. + // Buffer contains all unattended cache set requests. It has a maximum size + // which is equal to the MaxRecords value. buffer []BufferItem } @@ -79,9 +79,9 @@ type ShardsLookup struct { shardsAvail []int } -// LookupRecord represents item in lookup table. One record contains index of -// shard and record. So we can determine which shard access and which record of -// shard to get. Record also contains expiration time. +// LookupRecord represents an item in the lookup table. One record contains the index of +// the shard and record. So we can determine which shard to access and which record of +// the shard to get. Record also contains expiration time. type LookupRecord struct { RecordIndex int ShardIndex int @@ -89,15 +89,15 @@ type LookupRecord struct { Expiration time.Time } -// BufferItem is used for buffer, which contains all unattended cache set -// request. +// BufferItem is used for the buffer, which contains all unattended cache set +// requests. type BufferItem struct { Key string Data []byte Expire time.Duration } -// New initialize whole cache memory with one allocated shard. +// New initializes the whole cache memory with one allocated shard. func New(opts ...Option) *AtomicCache { var options = &Options{ RecordSizeSmall: 512, @@ -138,8 +138,8 @@ func New(opts ...Option) *AtomicCache { return cache } -// initShardsSection provides shards sections initialization. So the cache has -// one shard in each section at the begging. +// initShardsSection provides shard section initialization. So the cache has +// one shard in each section at the beginning. func initShardsSection(shardsSection *ShardsLookup, maxShards, maxRecords, recordSize int) { var shardIndex int @@ -153,10 +153,9 @@ func initShardsSection(shardsSection *ShardsLookup, maxShards, maxRecords, recor shardsSection.shards[shardIndex] = NewShard(maxRecords, recordSize) } -// Set store data to cache memory. If key/record is already in memory, then data -// are replaced. If not, it checks if there are some allocated shard with empty -// space for data. If there is no empty space, new shard is allocated. Otherwise -// some valid record (FIFO queue) is deleted and new one is stored. +// Set stores data to cache memory. If the key/record is already in memory, then data +// are replaced. If not, it checks if there is an allocated shard with empty +// space for data. If there is no empty space, a new shard is allocated. // Remarks: // - If expiration time is set to 0 then maximum expiration time is used (48 hours). // - If expiration time is KeepTTL, then current expiration time is preserved. @@ -395,9 +394,9 @@ func (a *AtomicCache) getEmptyShard(shardSectionID int) (int, bool) { return shardIndex, true } -// getShardsSectionBySize returns shards section lookup structure and section -// identifier as a second value. The function requires the data size value on -// input. If data are bigger than allowed value, then nil and 0 is returned. +// getShardsSectionBySize returns the shard section lookup structure and section +// identifier as a second value. The function requires the data size value as input. +// If data are bigger than the allowed value, then nil and 0 are returned. // This method is not thread safe and additional locks are required. func (a *AtomicCache) getShardsSectionBySize(dataSize int) (*ShardsLookup, int) { if dataSize <= int(a.RecordSizeSmall) { @@ -454,10 +453,9 @@ func (a *AtomicCache) getExprTime(expire time.Duration) time.Time { return time.Now().Add(expire) } -// collectGarbage provides garbage collect. It goes throught lookup table and -// checks expiration time. If shard end up empty, then garbage collect release -// him, but only if there is more than one shard in charge (we always have one -// active shard). +// collectGarbage provides garbage collection. It goes through the lookup table and +// checks expiration time. If a shard ends up empty, then garbage collection releases +// it, but only if there is more than one shard in use (there is always at least one active shard). func (a *AtomicCache) collectGarbage() { a.Lock() for k, v := range a.lookup { From 8860c8d8202b8e4c138c955eed527d5afcc90dae Mon Sep 17 00:00:00 2001 From: Praser <24474722+praserx@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:37:06 +0200 Subject: [PATCH 3/3] docs: added condition explanation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cache.go b/cache.go index e07fff5..b185671 100644 --- a/cache.go +++ b/cache.go @@ -314,6 +314,9 @@ func (a *AtomicCache) Delete(key string) error { } shardSection := a.getShardsSectionByID(val.ShardSection) + // Check if the shard at val.ShardIndex is nil. This is a defensive check to + // handle cases where the shard might have been released or not initialized + // due to concurrent modifications or unexpected states. if shardSection.shards[val.ShardIndex] != nil { shardSection.shards[val.ShardIndex].Free(val.RecordIndex) a.releaseShard(val.ShardSection, val.ShardIndex)