Skip to content
Open
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
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A simple and efficient cache implementation in Go that implements the S3FIFO (Si
- Easy to understand and maintain
- Minimal dependencies
- **Generic Implementation**: Written in Go with generics support for type-safe caching of any comparable key type
- **Backend Loading Support**: Optional loader function to fetch values from a backend when not found in cache

## Installation

Expand All @@ -20,12 +21,14 @@ go get github.com/aryehlev/cache-go

## Usage

### Basic Cache

```go
package main

import (
"fmt"

"github.com/aryehlev/cache-go"
)

Expand All @@ -52,6 +55,49 @@ func main() {
}
```

### Cache with Backend Loader

```go
package main

import (
"fmt"
"database/sql"

"github.com/aryehlev/cache-go"
)

func main() {
// Example database connection
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
defer db.Close()

// Create a loader function that fetches values from the database
loader := func(key string) (int, bool) {
var value int
err := db.QueryRow("SELECT value FROM data WHERE key = ?", key).Scan(&value)
if err != nil {
return 0, false // Not found or error
}
return value, true
}

// Create a new cache with size 1000 and the loader function
cache, err := cache_go.NewWithLoader[string, int](1000, loader)
if err != nil {
panic(err)
}

// Get a value - will fetch from database if not in cache
if value, ok := cache.Get("key1"); ok {
fmt.Printf("Value: %d\n", value)
}
}
```

## How S3FIFO Works

The S3FIFO algorithm divides the cache into three segments:
Expand All @@ -71,4 +117,3 @@ visit the [official S3FIFO website](https://s3fifo.com/).
## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

50 changes: 50 additions & 0 deletions loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cache_go

// LoaderFunc is a function type that loads a value for a key.
// It returns the loaded value and a boolean indicating whether the value was found.
type LoaderFunc[K comparable, V any] func(key K) (V, bool)

// CacheWithLoader extends the Cache with a loader function that can fetch values from a backend.
type CacheWithLoader[K comparable, V any] struct {
*Cache[K, V]
loader LoaderFunc[K, V]
}

// NewWithLoader creates a new cache with the specified size and loader function.
func NewWithLoader[K comparable, V any](size uint, loader LoaderFunc[K, V]) (*CacheWithLoader[K, V], error) {
cache, err := New[K, V](size)
if err != nil {
return nil, err
}

return &CacheWithLoader[K, V]{
Cache: cache,
loader: loader,
}, nil
}

// Get retrieves a value from the cache. If the value is not in the cache,
// it attempts to load it using the loader function.
func (cl *CacheWithLoader[K, V]) Get(key K) (V, bool) {

// If not in cache, try to load from backend
if cl.loader != nil {
value, found := cl.loader(key)
if found {
// Store the loaded value in the cache
cl.Cache.Set(key, value)
return value, true
}
}

// Try to get from cache first
value, found := cl.Cache.Get(key)
if found {
return value, true
}


// Return zero value if not found
var zero V
return zero, false
}
Comment on lines +28 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical issue: Incorrect order of operations in Get method.

The current implementation checks the loader before checking the cache, which defeats the purpose of caching and will cause performance issues. The cache should be checked first, and the loader should only be called on cache misses.

Apply this diff to fix the logic:

 func (cl *CacheWithLoader[K, V]) Get(key K) (V, bool) {
+	// Try to get from cache first
+	value, found := cl.Cache.Get(key)
+	if found {
+		return value, true
+	}
 
 	// If not in cache, try to load from backend
 	if cl.loader != nil {
 		value, found := cl.loader(key)
 		if found {
 			// Store the loaded value in the cache
 			cl.Cache.Set(key, value)
 			return value, true
 		}
 	}
 
-	// Try to get from cache first
-	value, found := cl.Cache.Get(key)
-	if found {
-		return value, true
-	}
-
-
 	// Return zero value if not found
 	var zero V
 	return zero, false
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (cl *CacheWithLoader[K, V]) Get(key K) (V, bool) {
// If not in cache, try to load from backend
if cl.loader != nil {
value, found := cl.loader(key)
if found {
// Store the loaded value in the cache
cl.Cache.Set(key, value)
return value, true
}
}
// Try to get from cache first
value, found := cl.Cache.Get(key)
if found {
return value, true
}
// Return zero value if not found
var zero V
return zero, false
}
func (cl *CacheWithLoader[K, V]) Get(key K) (V, bool) {
// Try to get from cache first
value, found := cl.Cache.Get(key)
if found {
return value, true
}
// If not in cache, try to load from backend
if cl.loader != nil {
value, found := cl.loader(key)
if found {
// Store the loaded value in the cache
cl.Cache.Set(key, value)
return value, true
}
}
// Return zero value if not found
var zero V
return zero, false
}
🤖 Prompt for AI Agents
In loader.go around lines 28 to 50, the Get method incorrectly calls the loader
before checking the cache, which undermines caching efficiency. To fix this,
first attempt to retrieve the value from the cache using cl.Cache.Get; if found,
return it immediately. Only if the cache lookup fails, call cl.loader to load
the value, store it in the cache if found, and then return it. This ensures the
cache is prioritized and the loader is used only on cache misses.

68 changes: 68 additions & 0 deletions loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cache_go

import (
"testing"
)

func TestCacheWithLoader(t *testing.T) {
// Create a simple in-memory backend
backend := map[string]int{
"key1": 42,
"key2": 84,
}

// Create a loader function that fetches from the backend
loader := func(key string) (int, bool) {
value, found := backend[key]
return value, found
}

// Create a cache with the loader
cache, err := NewWithLoader[string, int](100, loader)
if err != nil {
t.Fatalf("Failed to create cache: %v", err)
}

// Test getting a value that's not in the cache but is in the backend
value, found := cache.Get("key1")
if !found {
t.Errorf("Expected to find key1 via loader, but it wasn't found")
}
if value != 42 {
t.Errorf("Expected value 42 for key1, got %d", value)
}

// Test that the value is now cached
// We can verify this by checking if the value is still accessible
// even if we remove it from the backend
delete(backend, "key1")
value, found = cache.Get("key1")
if !found {
t.Errorf("Expected to find key1 in cache after loading, but it wasn't found")
}
if value != 42 {
t.Errorf("Expected value 42 for key1 from cache, got %d", value)
}

// Test getting a value that's not in the cache or backend
value, found = cache.Get("nonexistent")
if found {
t.Errorf("Expected not to find nonexistent key, but it was found with value %d", value)
}

// Test that the loader is not used when a value is already in the cache
// We'll set a value directly in the cache
cache.Set("key3", 123)

// Then add a different value to the backend
backend["key3"] = 456

// When we get the value, it should come from the cache, not the backend
value, found = cache.Get("key3")
if !found {
t.Errorf("Expected to find key3 in cache, but it wasn't found")
}
if value != 123 {
t.Errorf("Expected value 123 for key3 from cache, got %d (should not have used loader)", value)
}
}