Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Makefile for cache2go

.PHONY: all build clean test coverage fmt lint

# Build settings
GOCMD = go
GOBUILD = $(GOCMD) build
GOCLEAN = $(GOCMD) clean
GOTEST = $(GOCMD) test
GOGET = $(GOCMD) get
GOMOD = $(GOCMD) mod
GOFMT = $(GOCMD) fmt
GOLINT = golangci-lint

# Project name
PROJECT_NAME = cache2go

# Build all binary
all: build

# Build the project
build:
$(GOBUILD) -v ./...

# Clean build files
clean:
$(GOCLEAN)
rm -f coverage.out

# Run tests
test:
$(GOTEST) -v ./...

# Run tests with race detection
test-race:
$(GOTEST) -race -v ./...

# Run tests with coverage
coverage:
$(GOTEST) -coverprofile=coverage.out -covermode=atomic ./...
$(GOCMD) tool cover -html=coverage.out

# Format code
fmt:
$(GOFMT) ./...

# Run linter
lint:
$(GOLINT) run

# Tidy modules
tidy:
$(GOMOD) tidy

# Update dependencies
deps:
$(GOGET) -u ./...
$(GOMOD) tidy

# Run SIEVE example
example-sieve:
$(GOCMD) run examples/sieve_example/sieve_example.go
5 changes: 3 additions & 2 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ func Cache(table string) *CacheTable {
// Double check whether the table exists or not.
if !ok {
t = &CacheTable{
name: table,
items: make(map[interface{}]*CacheItem),
name: table,
items: make(map[interface{}]*CacheItem),
evictionPolicy: EvictionNone,
}
cache[table] = t
}
Expand Down
76 changes: 73 additions & 3 deletions cachetable.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ type CacheTable struct {
addedItem []func(item *CacheItem)
// Callback method triggered before deleting an item from the cache.
aboutToDeleteItem []func(item *CacheItem)

// Eviction policy
evictionPolicy EvictionPolicy
// SIEVE cache implementation (only used when evictionPolicy is EvictionSIEVE)
sieveCache *SIEVECache
// Maximum capacity (0 means unlimited)
maxCapacity int
}

// Count returns how many items are currently stored in the cache.
Expand All @@ -46,6 +53,33 @@ func (table *CacheTable) Count() int {
return len(table.items)
}

// SetEvictionPolicy sets the eviction policy and maximum capacity for the cache table.
// If policy is EvictionSIEVE, it initializes a SIEVE cache with the given capacity and sample size.
// If capacity is 0, the cache will have unlimited capacity (not recommended with SIEVE).
// If sampleSize is 0, a default sample size of 8 will be used for SIEVE.
func (table *CacheTable) SetEvictionPolicy(policy EvictionPolicy, capacity int, sampleSize int) {
table.Lock()
defer table.Unlock()

table.evictionPolicy = policy
table.maxCapacity = capacity

// Initialize SIEVE cache if that's the chosen policy
if policy == EvictionSIEVE {
if capacity <= 0 {
capacity = 1024 // Default to 1024 if no capacity specified
}
table.sieveCache = NewSIEVECache(capacity, sampleSize)

// Pre-populate SIEVE cache with existing items
for _, item := range table.items {
table.sieveCache.Add(item)
}
} else {
table.sieveCache = nil
}
}

// Foreach all items
func (table *CacheTable) Foreach(trans func(key interface{}, item *CacheItem)) {
table.RLock()
Expand Down Expand Up @@ -76,7 +110,7 @@ func (table *CacheTable) SetAddedItemCallback(f func(*CacheItem)) {
table.addedItem = append(table.addedItem, f)
}

//AddAddedItemCallback appends a new callback to the addedItem queue
// AddAddedItemCallback appends a new callback to the addedItem queue
func (table *CacheTable) AddAddedItemCallback(f func(*CacheItem)) {
table.Lock()
defer table.Unlock()
Expand Down Expand Up @@ -203,6 +237,13 @@ func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data inter

// Add item to cache.
table.Lock()

// We need to update sieveCache before calling addInternal
// since addInternal will unlock the mutex
if table.evictionPolicy == EvictionSIEVE && table.sieveCache != nil {
table.sieveCache.Add(item)
}

table.addInternal(item)

return item
Expand Down Expand Up @@ -243,9 +284,19 @@ func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) {
// Delete an item from the cache.
func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
table.Lock()
defer table.Unlock()

return table.deleteInternal(key)
// If using SIEVE eviction, remove from SIEVE cache as well
if table.evictionPolicy == EvictionSIEVE && table.sieveCache != nil {
table.sieveCache.Delete(key)
}

item, ok := table.items[key]
if !ok {
table.Unlock()
return nil, ErrKeyNotFound
}

return table.deleteInternal(item)
}

// Exists returns whether an item exists in the cache. Unlike the Value method
Expand All @@ -269,6 +320,11 @@ func (table *CacheTable) NotFoundAdd(key interface{}, lifeSpan time.Duration, da
return false
}

// If using SIEVE eviction, remove from SIEVE cache as well
if table.evictionPolicy == EvictionSIEVE && table.sieveCache != nil {
table.sieveCache.Delete(key)
}

item := NewCacheItem(key, lifeSpan, data)
table.addInternal(item)

Expand All @@ -279,6 +335,15 @@ func (table *CacheTable) NotFoundAdd(key interface{}, lifeSpan time.Duration, da
// pass additional arguments to your DataLoader callback function.
func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
table.RLock()

// If using SIEVE eviction, update the access time in SIEVE cache
if table.evictionPolicy == EvictionSIEVE && table.sieveCache != nil {
if item, exists := table.sieveCache.Get(key); exists {
// Item exists in SIEVE cache, update will be handled by Get()
_ = item
}
}

r, ok := table.items[key]
loadData := table.loadData
table.RUnlock()
Expand Down Expand Up @@ -315,6 +380,11 @@ func (table *CacheTable) Flush() {
if table.cleanupTimer != nil {
table.cleanupTimer.Stop()
}

// If using SIEVE eviction, flush SIEVE cache as well
if table.evictionPolicy == EvictionSIEVE && table.sieveCache != nil {
table.sieveCache.Flush()
}
}

// CacheItemPair maps key to access counter
Expand Down
18 changes: 18 additions & 0 deletions eviction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Simple caching library with expiration capabilities
* Copyright (c) 2013-2017, Christian Muehlhaeuser <muesli@gmail.com>
*
* For license see LICENSE.txt
*/

package cache2go

// EvictionPolicy defines the cache eviction policy.
type EvictionPolicy int

const (
// EvictionNone uses no eviction policy (keep items until they expire).
EvictionNone EvictionPolicy = iota
// EvictionSIEVE uses the SIEVE eviction algorithm.
EvictionSIEVE
)
72 changes: 72 additions & 0 deletions examples/sieve_example/sieve_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"fmt"
"github.com/muesli/cache2go"
"math/rand"
"time"
)

// Keys & values in cache2go can be of arbitrary types, e.g. a struct.
type myStruct struct {
text string
moreData []byte
}

func main() {
// In newer Go versions (1.20+), we don't need to seed the random number generator
// For Go < 1.20, uncomment the line below:
// rand.Seed(time.Now().UnixNano())

// Accessing a new cache table for the first time will create it.
cache := cache2go.Cache("myCache")

// Enable SIEVE eviction with a capacity of 100 items and a sample size of 8
cache.SetEvictionPolicy(cache2go.EvictionSIEVE, 100, 8)
fmt.Println("Created cache with SIEVE eviction policy, capacity: 100, sample size: 8")

// Add 150 items to the cache (more than capacity)
for i := 0; i < 150; i++ {
val := myStruct{fmt.Sprintf("This is test item #%d", i), []byte{}}
cache.Add(fmt.Sprintf("key-%d", i), 5*time.Minute, &val)
}

// Check how many items are in the cache
// Should be 100 (or less if some expired by chance)
fmt.Printf("Cache count after adding 150 items: %d (should be around 100 due to capacity limit)\n", cache.Count())

// Now perform some random accesses to simulate real-world usage
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key-%d", rand.Intn(150))
res, err := cache.Value(key)
if err == nil {
// Just access the item to update its access time in the SIEVE cache
_ = res.Data().(*myStruct).text
}
}

// Add 50 more items
for i := 150; i < 200; i++ {
val := myStruct{fmt.Sprintf("This is test item #%d", i), []byte{}}
cache.Add(fmt.Sprintf("key-%d", i), 5*time.Minute, &val)
}

// Check how many items are in the cache again
fmt.Printf("Cache count after adding 50 more items: %d (should still be around 100)\n", cache.Count())

// Check for a few specific keys to see which ones were kept in the cache
// Items that were accessed more frequently are more likely to be in the cache
for i := 0; i < 200; i += 20 {
key := fmt.Sprintf("key-%d", i)
_, err := cache.Value(key)
if err == nil {
fmt.Printf("Key %s is still in the cache\n", key)
} else {
fmt.Printf("Key %s was evicted from the cache\n", key)
}
}

// Flush the cache
cache.Flush()
fmt.Printf("Cache count after flush: %d\n", cache.Count())
}
Loading