aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarc Pervaz Boocha <marcpervaz@qburst.com>2025-03-03 17:24:17 +0530
committerMarc Pervaz Boocha <marcpervaz@qburst.com>2025-03-03 17:24:17 +0530
commit7f9a513ea1803af327067b7bd84326c63b3d3844 (patch)
treed0fd50db0aa8d4d59a79c22fc9eb9b84e305a261
parentImproved Concurency (diff)
downloadcache-7f9a513ea1803af327067b7bd84326c63b3d3844.tar
cache-7f9a513ea1803af327067b7bd84326c63b3d3844.tar.gz
cache-7f9a513ea1803af327067b7bd84326c63b3d3844.tar.bz2
cache-7f9a513ea1803af327067b7bd84326c63b3d3844.tar.lz
cache-7f9a513ea1803af327067b7bd84326c63b3d3844.tar.xz
cache-7f9a513ea1803af327067b7bd84326c63b3d3844.tar.zst
cache-7f9a513ea1803af327067b7bd84326c63b3d3844.zip
Added documentation and tests
-rw-r--r--README.md151
-rw-r--r--conn_test.go99
-rw-r--r--evict.go4
-rw-r--r--evict_test.go128
-rw-r--r--store_test.go101
5 files changed, 480 insertions, 3 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f4dba3a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,151 @@
+# Cache
+
+## Features
+
+- **In-Memory Cache**: Fast access to cached data.
+
+- **Uses Generics with serialization**: To make it type safe and support several types via msgpack.
+s
+- **File-Backed Storage**: Persistent storage of cache data.
+
+- **Eviction Policies**: Support for FIFO, LRU, LFU, and LTR eviction policies.
+
+- **Concurrency**: Thread-safe operations with the use of locks(mutex). The persistant storage is locked via file locks to avoid issues
+
+- **Periodic Tasks**: Background worker for snapshotting and cleanup of expired entries.
+
+## Installation
+
+To use the Cache Library in your Go project, you can install it using `go get`:
+
+```sh
+go get github.com/marcthe12/cache
+```
+
+## Usage
+
+### Opening a Cache
+
+ To open a file-backed cache, use the `OpenFile` function:
+
+```go
+package main
+
+import (
+ "github.com/marcthe12/cache"
+ "time"
+ "log"
+)
+
+func main() {
+ db, err := cache.OpenFile[string, string]("cache.db", cache.WithPolicy(cache.PolicyLRU))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer db.Close()
+
+ // Set a key-value pair with a TTL of 1 hour
+ if err := db.Set("key", "value", 1*time.Hour); err != nil {
+ log.Fatal(err)
+ }
+
+ // Get a value by key
+ value, ttl, err := db.GetValue("key")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("Value: %s, TTL: %v\n", value, ttl)
+}
+```
+
+To open an in-memory cache, use the `OpenMem` function:
+
+```go
+package main
+
+import (
+ "github.com/marcthe12/cache"
+ "time"
+ "log"
+)
+
+func main() {
+ db, err := cache.OpenMem[string, string](cache.WithPolicy(cache.PolicyLRU))
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer db.Close()
+
+ // Set a key-value pair with a TTL of 1 hour
+ if err := db.Set("key", "value", 1*time.Hour); err != nil {
+ log.Fatal(err)
+ }
+
+ // Get a value by key
+
+ value, ttl, err := db.GetValue("key")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("Value: %s, TTL: %v\n", value, ttl)
+}
+```
+
+More Examples in the ```/examples``` directory
+
+### Eviction Policies
+
+The Cache Library supports the following eviction policies:
+
+- **None**: Does not do any evictions.
+
+- **FIFO (First In, First Out)**: Evicts the oldest entries first.
+
+- **LRU (Least Recently Used)**: Evicts the least recently used entries first.
+
+- **LFU (Least Frequently Used)**: Evicts the least frequently used entries first.
+
+- **LTR (Least Remaining Time)**: Evicts entries with the least remaining time to live first.
+
+You can set the eviction policy when opening the cache using the `WithPolicy` option.
+
+### Configuration Options
+
+The Cache Library supports the following configuration options:
+
+- `WithPolicy`: Sets the eviction policy.
+
+- `WithMaxCost`: Sets the maximum cost for the cache. The Cost is the size of the binary encoded KV pair.
+
+- `SetSnapshotTime`: Sets the interval for taking snapshots of the cache.
+
+- `SetCleanupTime`: Sets the interval for cleaning up expired entries.
+
+### Additional Methods
+
+- `Get`: Retrieves a value from the cache by key and returns its TTL.
+
+- `GetValue`: Retrieves a value from the cache by key and returns the value and its TTL.
+
+- `Set`: Adds a key-value pair to the cache with a specified TTL.
+
+- `Delete`: Removes a key-value pair from the cache.
+
+- `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.
+
+- `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. Note this locks the db duing the factory function which prevent concurent acces to the db during the operation.
+
+## Documentation
+
+ For detailed documentation on the public API, you can use `godoc`:
+
+```sh
+godoc -http=:6060
+```
+
+Then open your browser and navigate to `http://localhost:6060/pkg/github.com/marcthe12/cache`.
diff --git a/conn_test.go b/conn_test.go
index ee2c175..c80072a 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -131,6 +131,105 @@ func TestDBDelete(t *testing.T) {
})
}
+func TestDBUpdateInPlace(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Exists", func(t *testing.T) {
+ t.Parallel()
+
+ store := setupTestDB[string, string](t)
+
+ want := "Value"
+ store.Set("Key", "Initial", 1*time.Hour)
+
+ processFunc := func(v string) (string, error) {
+ return want, nil
+ }
+
+ if err := store.UpdateInPlace("Key", processFunc, 1*time.Hour); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, _, err := store.GetValue("Key")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if want != got {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ })
+
+ t.Run("Not Exists", func(t *testing.T) {
+ t.Parallel()
+
+ store := setupTestDB[string, string](t)
+
+ want := "Value"
+
+ processFunc := func(v string) (string, error) {
+ return want, nil
+ }
+
+ if err := store.UpdateInPlace("Key", processFunc, 1*time.Hour); !errors.Is(err, ErrKeyNotFound) {
+ t.Fatalf("expected error: %v, got: %v", ErrKeyNotFound, err)
+ }
+ })
+}
+
+func TestDBMemoize(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Cache Miss", func(t *testing.T) {
+ t.Parallel()
+
+ store := setupTestDB[string, string](t)
+
+ want := "Value"
+
+ factoryFunc := func() (string, error) {
+ return want, nil
+ }
+
+ got, err := store.Memorize("Key", factoryFunc, 1*time.Hour)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != "Value" {
+ t.Fatalf("expected: %v, got: %v", "Value", got)
+ }
+
+ got, _, err = store.GetValue("Key")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if want != got {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ })
+
+ t.Run("Cache Hit", func(t *testing.T) {
+ t.Parallel()
+
+ store := setupTestDB[string, string](t)
+
+ want := "NewValue"
+
+ store.Set("Key", "Value", 1*time.Hour)
+
+ factoryFunc := func() (string, error) {
+ return want, nil
+ }
+
+ got, err := store.Memorize("Key", factoryFunc, 1*time.Hour)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != "Value" {
+ t.Fatalf("expected: %v, got: %v", "Value", got)
+ }
+ })
+}
+
func BenchmarkDBGet(b *testing.B) {
for n := 1; n <= 10000; n *= 10 {
b.Run(strconv.Itoa(n), func(b *testing.B) {
diff --git a/evict.go b/evict.go
index 6b0aabc..eff626c 100644
--- a/evict.go
+++ b/evict.go
@@ -41,6 +41,8 @@ func pushEvict(node *node, sentinnel *node) {
node.EvictPrev.EvictNext = node
}
+var ErrInvalidPolicy = errors.New("invalid policy")
+
// SetPolicy sets the eviction policy based on the given type.
func (e *evictionPolicy) SetPolicy(y EvictionPolicyType) error {
store := map[EvictionPolicyType]func() evictionStrategies{
@@ -63,7 +65,7 @@ func (e *evictionPolicy) SetPolicy(y EvictionPolicyType) error {
factory, ok := store[y]
if !ok {
- return errors.New("invalid policy")
+ return ErrInvalidPolicy
}
e.evictionStrategies = factory()
diff --git a/evict_test.go b/evict_test.go
index e26315e..d771844 100644
--- a/evict_test.go
+++ b/evict_test.go
@@ -498,8 +498,8 @@ func TestPolicyEvict(t *testing.T) {
policy.OnInsert(nodes[0])
policy.OnInsert(nodes[1])
- nodes[0].Expiration = time.Now().Add(20 * time.Minute)
- policy.OnUpdate(nodes[0])
+ nodes[1].Expiration = time.Now().Add(20 * time.Minute)
+ policy.OnUpdate(nodes[1])
},
expected: func(nodes []*node) *node {
return nodes[1]
@@ -544,3 +544,127 @@ func TestPolicyEvict(t *testing.T) {
})
}
}
+
+func TestSetPolicy(t *testing.T) {
+ t.Parallel()
+
+ type test struct {
+ name string
+ policyType EvictionPolicyType
+ expectedType EvictionPolicyType
+ expectedErr error
+ }
+
+ tests := []test{
+ {
+ name: "PolicyNone",
+ policyType: PolicyNone,
+ expectedType: PolicyNone,
+ expectedErr: nil,
+ },
+ {
+ name: "PolicyFIFO",
+ policyType: PolicyFIFO,
+ expectedType: PolicyFIFO,
+ expectedErr: nil,
+ },
+ {
+ name: "PolicyLRU",
+ policyType: PolicyLRU,
+ expectedType: PolicyLRU,
+ expectedErr: nil,
+ },
+ {
+ name: "PolicyLFU",
+ policyType: PolicyLFU,
+ expectedType: PolicyLFU,
+ expectedErr: nil,
+ },
+ {
+ name: "PolicyLTR",
+ policyType: PolicyLTR,
+ expectedType: PolicyLTR,
+ expectedErr: nil,
+ },
+ {
+ name: "InvalidPolicy",
+ policyType: EvictionPolicyType(999), // Invalid policy type
+ expectedType: PolicyNone, // Default type
+ expectedErr: ErrInvalidPolicy,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ policy := &evictionPolicy{
+ Sentinel: createSentinel(t),
+ ListLock: &sync.RWMutex{},
+ }
+
+ err := policy.SetPolicy(tt.policyType)
+ if err != tt.expectedErr {
+ t.Errorf("expected error %v, got %v", tt.expectedErr, err)
+ }
+
+ if policy.Type != tt.expectedType {
+ t.Errorf("expected policy type %v, got %v", tt.expectedType, policy.Type)
+ }
+ })
+ }
+}
+
+func TestSetPolicyMultipleTimes(t *testing.T) {
+ t.Parallel()
+
+ policy := &evictionPolicy{
+ Sentinel: createSentinel(t),
+ ListLock: &sync.RWMutex{},
+ }
+
+ // Set policy to FIFO
+ err := policy.SetPolicy(PolicyFIFO)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ if policy.Type != PolicyFIFO {
+ t.Errorf("expected policy type %v, got %v", PolicyFIFO, policy.Type)
+ }
+
+ // Set policy to LRU
+ err = policy.SetPolicy(PolicyLRU)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ if policy.Type != PolicyLRU {
+ t.Errorf("expected policy type %v, got %v", PolicyLRU, policy.Type)
+ }
+
+ // Set policy to LFU
+ err = policy.SetPolicy(PolicyLFU)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ if policy.Type != PolicyLFU {
+ t.Errorf("expected policy type %v, got %v", PolicyLFU, policy.Type)
+ }
+
+ // Set policy to LTR
+ err = policy.SetPolicy(PolicyLTR)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ if policy.Type != PolicyLTR {
+ t.Errorf("expected policy type %v, got %v", PolicyLTR, policy.Type)
+ }
+
+ // Set policy to None
+ err = policy.SetPolicy(PolicyNone)
+ if err != nil {
+ t.Errorf("expected no error, got %v", err)
+ }
+ if policy.Type != PolicyNone {
+ t.Errorf("expected policy type %v, got %v", PolicyNone, policy.Type)
+ }
+}
diff --git a/store_test.go b/store_test.go
index f6cccc9..4b9d2f3 100644
--- a/store_test.go
+++ b/store_test.go
@@ -18,6 +18,107 @@ func setupTestStore(tb testing.TB) *store {
return store
}
+func TestNodeIsValid(t *testing.T) {
+ tests := []struct {
+ name string
+ node *node
+ isValid bool
+ }{
+ {
+ name: "Valid node with non-zero expiration",
+ node: &node{Key: []byte("key1"), Value: []byte("value1"), Expiration: time.Now().Add(10 * time.Minute)},
+ isValid: true,
+ },
+ {
+ name: "Valid node with zero expiration",
+ node: &node{Key: []byte("key2"), Value: []byte("value2"), Expiration: time.Time{}},
+ isValid: true,
+ },
+ {
+ name: "Expired node",
+ node: &node{Key: []byte("key3"), Value: []byte("value3"), Expiration: time.Now().Add(-1 * time.Minute)},
+ isValid: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.node.IsValid(); got != tt.isValid {
+ t.Errorf("IsValid() = %v, want %v", got, tt.isValid)
+ }
+ })
+ }
+}
+
+func TestNodeTTL(t *testing.T) {
+ tests := []struct {
+ name string
+ node *node
+ ttl time.Duration
+ }{
+ {
+ name: "Node with non-zero expiration",
+ node: &node{Key: []byte("key1"), Value: []byte("value1"), Expiration: time.Now().Add(10 * time.Minute)},
+ ttl: 10 * time.Minute,
+ },
+ {
+ name: "Node with zero expiration",
+ node: &node{Key: []byte("key2"), Value: []byte("value2"), Expiration: time.Time{}},
+ ttl: 0,
+ },
+ {
+ name: "Expired node",
+ node: &node{Key: []byte("key3"), Value: []byte("value3"), Expiration: time.Now().Add(-1 * time.Minute)},
+ ttl: -1 * time.Minute, // Should be negative or 0, depending on the implementation
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.node.TTL().Round(time.Second); got != tt.ttl {
+ t.Errorf("TTL() = %v, want %v", got, tt.ttl)
+ }
+ })
+ }
+}
+
+func TestNodeCost(t *testing.T) {
+ tests := []struct {
+ name string
+ node *node
+ cost uint64
+ }{
+ {
+ name: "Node with non-empty Key and Value",
+ node: &node{Key: []byte("key1"), Value: []byte("value1")},
+ cost: 10,
+ },
+ {
+ name: "Node with empty Key and Value",
+ node: &node{Key: []byte(""), Value: []byte("")},
+ cost: 0,
+ },
+ {
+ name: "Node with non-empty Key and empty Value",
+ node: &node{Key: []byte("key1"), Value: []byte("")},
+ cost: 4,
+ },
+ {
+ name: "Node with empty Key and non-empty Value",
+ node: &node{Key: []byte(""), Value: []byte("value1")},
+ cost: 6,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.node.Cost(); got != tt.cost {
+ t.Errorf("Cost() = %v, want %v", got, tt.cost)
+ }
+ })
+ }
+}
+
func TestStoreGetSet(t *testing.T) {
t.Parallel()