Skip to content

Commit 95268ec

Browse files
authored
Merge pull request #6 from praserx/feat/added-exists-and-delete-functions
feat: added exists and delete functions
2 parents d37ab92 + 8860c8d commit 95268ec

2 files changed

Lines changed: 126 additions & 23 deletions

File tree

cache.go

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var (
1414
ErrFullMemory = errors.New("cannot create new record: memory is full")
1515
)
1616

17-
// Constans below are used for shard section identification.
17+
// Constants below are used for shard section identification.
1818
const (
1919
// SMSH - Small Shards section
2020
SMSH = iota + 1
@@ -38,7 +38,7 @@ type AtomicCache struct {
3838
// Lookup structure used for global index.
3939
lookup map[string]LookupRecord
4040

41-
// Shards lookup tables which contains information about shards sections.
41+
// Shards lookup tables which contain information about shard sections.
4242
smallShards, mediumShards, largeShards ShardsLookup
4343

4444
// Size of byte array used for memory allocation at small shard section.
@@ -63,8 +63,8 @@ type AtomicCache struct {
6363
// Garbage collector counter for starter.
6464
GcCounter uint32
6565

66-
// Buffer contains all unattended cache set requests. It has a maximum site
67-
// which is equal to MaxRecords value.
66+
// Buffer contains all unattended cache set requests. It has a maximum size
67+
// which is equal to the MaxRecords value.
6868
buffer []BufferItem
6969
}
7070

@@ -79,25 +79,25 @@ type ShardsLookup struct {
7979
shardsAvail []int
8080
}
8181

82-
// LookupRecord represents item in lookup table. One record contains index of
83-
// shard and record. So we can determine which shard access and which record of
84-
// shard to get. Record also contains expiration time.
82+
// LookupRecord represents an item in the lookup table. One record contains the index of
83+
// the shard and record. So we can determine which shard to access and which record of
84+
// the shard to get. Record also contains expiration time.
8585
type LookupRecord struct {
8686
RecordIndex int
8787
ShardIndex int
8888
ShardSection int
8989
Expiration time.Time
9090
}
9191

92-
// BufferItem is used for buffer, which contains all unattended cache set
93-
// request.
92+
// BufferItem is used for the buffer, which contains all unattended cache set
93+
// requests.
9494
type BufferItem struct {
9595
Key string
9696
Data []byte
9797
Expire time.Duration
9898
}
9999

100-
// New initialize whole cache memory with one allocated shard.
100+
// New initializes the whole cache memory with one allocated shard.
101101
func New(opts ...Option) *AtomicCache {
102102
var options = &Options{
103103
RecordSizeSmall: 512,
@@ -138,8 +138,8 @@ func New(opts ...Option) *AtomicCache {
138138
return cache
139139
}
140140

141-
// initShardsSection provides shards sections initialization. So the cache has
142-
// one shard in each section at the begging.
141+
// initShardsSection provides shard section initialization. So the cache has
142+
// one shard in each section at the beginning.
143143
func initShardsSection(shardsSection *ShardsLookup, maxShards, maxRecords, recordSize int) {
144144
var shardIndex int
145145

@@ -153,10 +153,12 @@ func initShardsSection(shardsSection *ShardsLookup, maxShards, maxRecords, recor
153153
shardsSection.shards[shardIndex] = NewShard(maxRecords, recordSize)
154154
}
155155

156-
// Set store data to cache memory. If key/record is already in memory, then data
157-
// are replaced. If not, it checks if there are some allocated shard with empty
158-
// space for data. If there is no empty space, new shard is allocated. Otherwise
159-
// some valid record (FIFO queue) is deleted and new one is stored.
156+
// Set stores data to cache memory. If the key/record is already in memory, then data
157+
// are replaced. If not, it checks if there is an allocated shard with empty
158+
// space for data. If there is no empty space, a new shard is allocated.
159+
// Remarks:
160+
// - If expiration time is set to 0 then maximum expiration time is used (48 hours).
161+
// - If expiration time is KeepTTL, then current expiration time is preserved.
160162
func (a *AtomicCache) Set(key string, data []byte, expire time.Duration) error {
161163
// Reject if data is too large for any shard
162164
if len(data) > int(a.RecordSizeLarge) {
@@ -283,6 +285,48 @@ func (a *AtomicCache) Get(key string) ([]byte, error) {
283285
return nil, ErrNotFound
284286
}
285287

288+
// Exists checks if record is present in cache memory. It returns true if record
289+
// is present, otherwise false.
290+
func (a *AtomicCache) Exists(key string) bool {
291+
a.RLock()
292+
val, ok := a.lookup[key]
293+
a.RUnlock()
294+
if !ok {
295+
return false
296+
}
297+
// Check expiration
298+
if time.Now().After(val.Expiration) {
299+
return false
300+
}
301+
return true
302+
}
303+
304+
// Delete removes record from cache memory. If record is not found, then error
305+
// is returned. It also releases memory used by record in shard.
306+
// If shard ends up empty, it is released.
307+
func (a *AtomicCache) Delete(key string) error {
308+
a.Lock()
309+
defer a.Unlock()
310+
311+
val, ok := a.lookup[key]
312+
if !ok {
313+
return ErrNotFound
314+
}
315+
316+
shardSection := a.getShardsSectionByID(val.ShardSection)
317+
// Check if the shard at val.ShardIndex is nil. This is a defensive check to
318+
// handle cases where the shard might have been released or not initialized
319+
// due to concurrent modifications or unexpected states.
320+
if shardSection.shards[val.ShardIndex] != nil {
321+
shardSection.shards[val.ShardIndex].Free(val.RecordIndex)
322+
a.releaseShard(val.ShardSection, val.ShardIndex)
323+
delete(a.lookup, key)
324+
return nil
325+
}
326+
327+
return ErrNotFound
328+
}
329+
286330
// releaseShard release shard if there is no record in memory. It returns true
287331
// if shard was released. The function requires the shard section ID and
288332
// shard ID on input.
@@ -353,9 +397,9 @@ func (a *AtomicCache) getEmptyShard(shardSectionID int) (int, bool) {
353397
return shardIndex, true
354398
}
355399

356-
// getShardsSectionBySize returns shards section lookup structure and section
357-
// identifier as a second value. The function requires the data size value on
358-
// input. If data are bigger than allowed value, then nil and 0 is returned.
400+
// getShardsSectionBySize returns the shard section lookup structure and section
401+
// identifier as a second value. The function requires the data size value as input.
402+
// If data are bigger than the allowed value, then nil and 0 are returned.
359403
// This method is not thread safe and additional locks are required.
360404
func (a *AtomicCache) getShardsSectionBySize(dataSize int) (*ShardsLookup, int) {
361405
if dataSize <= int(a.RecordSizeSmall) {
@@ -412,10 +456,9 @@ func (a *AtomicCache) getExprTime(expire time.Duration) time.Time {
412456
return time.Now().Add(expire)
413457
}
414458

415-
// collectGarbage provides garbage collect. It goes throught lookup table and
416-
// checks expiration time. If shard end up empty, then garbage collect release
417-
// him, but only if there is more than one shard in charge (we always have one
418-
// active shard).
459+
// collectGarbage provides garbage collection. It goes through the lookup table and
460+
// checks expiration time. If a shard ends up empty, then garbage collection releases
461+
// it, but only if there is more than one shard in use (there is always at least one active shard).
419462
func (a *AtomicCache) collectGarbage() {
420463
a.Lock()
421464
for k, v := range a.lookup {

cache_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,66 @@ func TestCacheKeepTTL(t *testing.T) {
178178
}
179179
}
180180

181+
func TestCacheExists(t *testing.T) {
182+
cache := New()
183+
key := "exists-key"
184+
data := []byte("exists-data")
185+
186+
// Should not exist before set
187+
if cache.Exists(key) {
188+
t.Errorf("Exists returned true for unset key")
189+
}
190+
191+
// Set and check exists
192+
if err := cache.Set(key, data, 10*time.Second); err != nil {
193+
t.Fatalf("Set error: %s", err)
194+
}
195+
if !cache.Exists(key) {
196+
t.Errorf("Exists returned false for set key")
197+
}
198+
199+
// Delete and check exists
200+
if err := cache.Delete(key); err != nil {
201+
t.Fatalf("Delete error: %s", err)
202+
}
203+
if cache.Exists(key) {
204+
t.Errorf("Exists returned true after Delete")
205+
}
206+
207+
// Never-set key
208+
if cache.Exists("never-existed") {
209+
t.Errorf("Exists returned true for never-set key")
210+
}
211+
}
212+
213+
func TestCacheDelete(t *testing.T) {
214+
cache := New()
215+
key := "del-key"
216+
data := []byte("to-delete")
217+
218+
// Set and then delete
219+
if err := cache.Set(key, data, 0); err != nil {
220+
t.Fatalf("Set error: %s", err)
221+
}
222+
if err := cache.Delete(key); err != nil {
223+
t.Errorf("Delete error: %s", err)
224+
}
225+
// Should not be able to get deleted key
226+
if _, err := cache.Get(key); err == nil {
227+
t.Errorf("Expected error on Get after Delete, got nil")
228+
}
229+
230+
// Deleting again should return ErrNotFound
231+
if err := cache.Delete(key); err != ErrNotFound {
232+
t.Errorf("Expected ErrNotFound on double Delete, got %v", err)
233+
}
234+
235+
// Deleting a never-set key should return ErrNotFound
236+
if err := cache.Delete("never-existed"); err != ErrNotFound {
237+
t.Errorf("Expected ErrNotFound for never-set key, got %v", err)
238+
}
239+
}
240+
181241
func benchmarkCacheNew(recordCount int, b *testing.B) {
182242
b.ReportAllocs()
183243

0 commit comments

Comments
 (0)