├── .github └── workflows │ └── tests.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── basic └── basic.go ├── cache ├── cache.go ├── config.go └── metrics.go ├── engine └── engine.go ├── fifo └── fifo.go ├── go.mod ├── go.sum ├── lfu └── lfu.go ├── lru └── lru.go └── tests ├── benchmark_test.go ├── cache_test.go ├── fifo_test.go ├── lfu_test.go └── lru_test.go /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout Code 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup Go 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: 1.23.5 16 | 17 | - name: Install Dependencies 18 | run: go mod tidy 19 | 20 | - name: Run Tests 21 | run: go test ./tests -v 22 | 23 | - name: Run Benchmarks 24 | run: go test -bench=. -benchmem ./tests 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🛠️ Contributing to EasyCache 2 | 3 | Thank you for your interest in contributing to EasyCache! 🎉 4 | We welcome all contributions, whether it's bug reports, feature requests, or pull requests. 5 | 6 | --- 7 | 8 | ## 📌 Reporting Issues 9 | 10 | If you found a bug or have a question, please open an **[Issue](https://github.com/hugocarreira/easycache/issues)**. 11 | Make sure to include: 12 | ✅ A **clear description** of the problem. 13 | ✅ Steps to **reproduce the issue** (if applicable). 14 | ✅ Your **Go version** and **system details**. 15 | 16 | Before opening a new issue, **check if it hasn't been reported** already. 17 | 18 | --- 19 | 20 | ## 💡 Requesting a Feature 21 | 22 | We love new ideas! 🚀 If you have a suggestion, open an **Issue** with: 23 | ✅ A **detailed explanation** of the feature. 24 | ✅ Why this feature is useful. 25 | ✅ Example use cases. 26 | 27 | ## 🔄 Submitting a Pull Request (PR) 28 | 29 | ✅ Open your pull request against `master`. 30 | ✅ Create a Pull Request with a clear description of your changes. 31 | 32 | 33 | ### 🧪 Running Tests & Benchmarks 34 | 35 | Before submitting code, ensure all tests pass: 36 | 37 | ```sh 38 | go test ./tests -v 39 | ``` 40 | 41 | ```sh 42 | go test -bench=. -benchmem ./tests 43 | ``` 44 | 45 | #### 🚀 Performance Benchmarks 46 | 47 | We ran performance benchmarks on EasyCache to measure the efficiency of `Set()`, `Get()`, `Delete()`, and eviction policies (`FIFO`, `LRU`, `LFU`). 48 | 49 | | Benchmark | Iterations | Time per operation | Memory used | Allocations per op | 50 | |--------------------------|------------|--------------------|-------------|--------------------| 51 | | **`BenchmarkCacheSet`** | 2,936,356 | **408.4 ns/op** | **122 B/op** | **5 allocs/op** | 52 | | **`BenchmarkCacheGet`** | 39,143,538 | **30.79 ns/op** | **0 B/op** | **0 allocs/op** | 53 | | **`BenchmarkCacheDelete`**| 5,376,940 | **223.3 ns/op** | **96 B/op** | **3 allocs/op** | 54 | | **`BenchmarkFIFOEviction`** | 3,065,480 | **391.7 ns/op** | **122 B/op** | **5 allocs/op** | 55 | | **`BenchmarkLRUEviction`** | 3,045,759 | **402.1 ns/op** | **122 B/op** | **5 allocs/op** | 56 | | **`BenchmarkLFUEviction`** | 2,916,150 | **394.3 ns/op** | **88 B/op** | **4 allocs/op** | 57 | 58 | **Tested on:** 59 | - **Go Version:** 1.23.5 60 | - **Cache Configuration:** `MaxSize = 10,000`, `TTL = 60s` 61 | 62 | --- 63 | 64 | ##### 🛠️ Running the Benchmarks 65 | 66 | To run the benchmarks yourself, use: 67 | 68 | ```sh 69 | go test -bench=. -benchmem ./tests 70 | ``` 71 | 72 | ## 🎯 Contributing Best Practices 73 | 74 | ✅ Be respectful – We welcome contributions from everyone! 75 | ✅ Keep discussions focused – Avoid unrelated topics in issues/PRs. 76 | ✅ Improve documentation – Even small fixes help! 77 | ✅ Test your code – Ensure everything works before submitting. 78 | 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hugo Carreira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🚀 EasyCache - A simple way to use in-memory cache in Golang 2 | 3 | [![Go Version](https://img.shields.io/badge/go-1.23.5-blue)](https://golang.org/) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/hugocarreira/easycache)](https://pkg.go.dev/github.com/hugocarreira/easycache) 5 | [![Build Status](https://github.com/hugocarreira/easycache/actions/workflows/tests.yml/badge.svg)](https://github.com/hugocarreira/easycache/actions) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/hugocarreira/easycache)](https://goreportcard.com/report/github.com/hugocarreira/easycache) 7 | [![Release](https://img.shields.io/github/v/release/hugocarreira/easycache.svg?style=flat-square)](https://hugocarreira/easycache/releases) 8 | [![License](https://img.shields.io/github/license/hugocarreira/easycache)](LICENSE) 9 | 10 | 11 | EasyCache is a **high-performance, in-memory caching library** for Go, supporting multiple **eviction policies** like **FIFO, LRU, LFU**, and **TTL-based expiration**. It is **thread-safe**, lightweight, and provides **built-in metrics**. 12 | 13 | --- 14 | 15 | ## ⚡ Installation 16 | 17 | To install EasyCache, run: 18 | 19 | ```sh 20 | go get github.com/hugocarreira/easycache 21 | ``` 22 | 23 | ## ❓ Why EasyCache? 24 | 25 | There are several caching solutions available, so why choose EasyCache? 26 | 27 | ✅ **Lightweight** – Minimal dependencies and optimized for performance. 28 | ✅ **Multiple eviction policies** – Supports FIFO, LRU, LFU, and TTL-based caching. 29 | ✅ **Thread-safe** – Uses `sync.RWMutex` to handle concurrent access. 30 | ✅ **Memory-efficient** – Allows memory usage limits and automatic cleanup. 31 | ✅ **Built-in metrics** – Track hits, misses, and evictions for performance insights. 32 | 33 | 34 | ## 🛠️ Basic Usage 35 | 36 | Here's how to use EasyCache in your Go project: 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | "fmt" 43 | "time" 44 | 45 | "github.com/hugocarreira/easycache/cache" 46 | ) 47 | 48 | func main() { 49 | // Create a new cache with Basic eviction policy 50 | c := cache.New(&cache.Config{ 51 | MaxSize: 5, 52 | TTL: 30 * time.Second, 53 | EvictionPolicy: cache.Basic, 54 | Metrics: false, 55 | MemoryLimits: 0, 56 | MemoryCheckInterval: 0, 57 | CleanupInterval: 10 * time.Second, 58 | }) 59 | 60 | // Add items to the cache 61 | c.Set("A", "Item A") 62 | c.Set("B", "Item B") 63 | 64 | // Retrieve an item 65 | value, found := c.Get("A") 66 | if found { 67 | fmt.Println("Cache hit:", value) // Output: Cache hit: Item A 68 | } else { 69 | fmt.Println("Cache miss") 70 | } 71 | 72 | // Check if a key exists 73 | fmt.Println("Has key 'B'?", c.Has("B")) // Output: true 74 | 75 | // Delete an item 76 | c.Delete("A") 77 | 78 | // Check cache length 79 | fmt.Println("Cache size:", c.Len()) // Output: 1 80 | } 81 | 82 | ``` 83 | 84 | ## ⚙️ Cache Policies 85 | 86 | EasyCache supports **four different eviction policies**: 87 | 88 | | Policy | Description | 89 | |---------|------------| 90 | | `Basic` | A simple TTL-based cache with no eviction policy. Items are removed only when they expire. | 91 | | `FIFO` | First-In, First-Out. The oldest item is removed when the cache is full. | 92 | | `LRU` | Least Recently Used. The least recently accessed item is removed when the cache is full. | 93 | | `LFU` | Least Frequently Used. The item with the fewest accesses is removed when the cache is full. | 94 | 95 | ### 🛠️ Basic Cache (TTL-based) 96 | 97 | The **Basic** cache is a simple TTL-based cache with no eviction policy. 98 | Items are **only removed when they expire** based on their **TTL (Time-To-Live)**. 99 | 100 | #### **Example:** 101 | ```go 102 | package main 103 | 104 | import ( 105 | "time" 106 | "github.com/hugocarreira/easycache/cache" 107 | ) 108 | 109 | func main() { 110 | // Create a Basic cache with TTL-based expiration 111 | c := cache.New(&cache.Config{ 112 | EvictionPolicy: cache.Basic, 113 | TTL: 30 * time.Second, // Items expire after 30 seconds 114 | CleanupInterval: 10 * time.Second, // Cleanup runs every 10 seconds 115 | }) 116 | 117 | // Add item to the cache 118 | c.Set("session1", "user123") 119 | 120 | // Get item from cache 121 | value, found := c.Set("session1") 122 | } 123 | ``` 124 | 125 | ### 🔄 FIFO Cache (First-In, First-Out) 126 | 127 | The **FIFO (First-In, First-Out)** cache evicts the **oldest item** when the cache reaches its maximum size. 128 | This policy ensures that the **first item added is the first one to be removed**, regardless of access frequency. 129 | 130 | #### **Example:** 131 | ```go 132 | package main 133 | 134 | import ( 135 | "github.com/hugocarreira/easycache/cache" 136 | ) 137 | 138 | func main() { 139 | // Create a FIFO cache with a maximum of 2 items 140 | c := cache.New(&cache.Config{ 141 | EvictionPolicy: cache.FIFO, 142 | MaxSize: 2, // Cache holds up to 2 items 143 | }) 144 | 145 | // Add items to the cache 146 | c.Set("A", "Item A") 147 | 148 | // Adding a second item causes "A" to be evicted 149 | c.Set("D", "Item D") 150 | } 151 | ``` 152 | 153 | ### 🔄 LRU Cache (Least Recently Used) 154 | 155 | The **LRU (Least Recently Used)** cache removes the **least recently accessed item** when the cache reaches its maximum size. 156 | This policy ensures that frequently used items stay in the cache while older, less-used items are evicted. 157 | 158 | #### **Example:** 159 | ```go 160 | package main 161 | 162 | import ( 163 | "github.com/hugocarreira/easycache/cache" 164 | ) 165 | 166 | func main() { 167 | // Create an LRU cache with a maximum of 3 items 168 | c := cache.New(&cache.Config{ 169 | EvictionPolicy: cache.LRU, 170 | MaxSize: 3, // Cache holds up to 3 items 171 | }) 172 | 173 | // Add items to the cache 174 | c.Set("A", "Item A") 175 | c.Set("B", "Item B") 176 | c.Set("C", "Item C") 177 | 178 | // Access "A" to mark it as recently used 179 | c.Get("A") 180 | 181 | // Adding a fourth item causes "B" to be evicted (least recently used) 182 | c.Set("D", "Item D") 183 | } 184 | ``` 185 | 186 | ### 🔄 LFU Cache (Least Frequently Used) 187 | 188 | The **LFU (Least Frequently Used)** cache removes the **least accessed item** when the cache reaches its maximum size. 189 | This policy ensures that frequently accessed items stay in the cache, while items with the lowest usage count are evicted first. 190 | 191 | #### **Example:** 192 | ```go 193 | package main 194 | 195 | import ( 196 | "github.com/hugocarreira/easycache/cache" 197 | ) 198 | 199 | func main() { 200 | // Create an LFU cache with a maximum of 3 items 201 | c := cache.New(&cache.Config{ 202 | EvictionPolicy: cache.LFU, 203 | MaxSize: 3, // Cache holds up to 3 items 204 | }) 205 | 206 | // Add items to the cache 207 | c.Set("A", "Item A") 208 | c.Set("B", "Item B") 209 | c.Set("C", "Item C") 210 | 211 | // Access "A" twice and "B" once 212 | c.Get("A") 213 | c.Get("A") 214 | c.Get("B") 215 | 216 | // Adding a fourth item causes "C" to be evicted (least frequently used) 217 | c.Set("D", "Item D") 218 | } 219 | ``` 220 | 221 | 222 | ## 🧹 Memory Management & Cleanup 223 | 224 | EasyCache provides **automatic memory cleanup** to remove expired items and prevent excessive memory usage. 225 | This is useful for **TTL-based caches** (`Basic`) and for scenarios where memory constraints are important. 226 | 227 | --- 228 | 229 | ### 🔄 Expired Items Cleanup (TTL-based) 230 | For **Basic (TTL-based) caches**, items are **removed automatically** when they expire. 231 | The **`CleanupInterval`** parameter defines how often expired items are removed. 232 | 233 | #### **Example:** 234 | ```go 235 | c := cache.New(&cache.Config{ 236 | EvictionPolicy: cache.Basic, 237 | TTL: 30 * time.Second, // Items expire after 30s 238 | CleanupInterval: 10 * time.Second, // Cleanup runs every 10s 239 | }) 240 | ``` 241 | 242 | ### 🔄 Memory Usage Monitoring & Cleanup 243 | EasyCache allows automatic memory checks to prevent the cache from exceeding a defined memory limit. 244 | 245 | The MemoryLimits parameter sets a max memory usage (in bytes), 246 | and the MemoryCheckInterval defines how often memory is checked. 247 | 248 | #### **Example:** 249 | ```go 250 | c := cache.New(&cache.Config{ 251 | EvictionPolicy: cache.Basic, 252 | MemoryLimits: 100 * 1024 * 1024, // 100 MB limit 253 | MemoryCheckInterval: 30 * time.Second, // Check memory every 30s 254 | }) 255 | ``` 256 | 257 | ## 💡 Contributing 258 | 259 | Please see [`CONTRIBUTING`](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. 260 | 261 | -------------------------------------------------------------------------------- /basic/basic.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/hugocarreira/easycache/engine" 8 | ) 9 | 10 | // Basic is a simple in-memory cache with TTL-based expiration. 11 | // 12 | // Unlike FIFO, LRU, or LFU caches, Basic does not implement any eviction 13 | // policy based on usage patterns. Items are only removed when they expire 14 | // based on their TTL (Time-To-Live). If no TTL is set, items remain in the cache indefinitely. 15 | // 16 | // This cache is useful for scenarios where automatic expiration is needed 17 | // but eviction based on frequency or recency of access is not required. 18 | type Basic struct { 19 | data map[string]*cacheItem 20 | lock sync.RWMutex 21 | maxSize int 22 | ttl time.Duration 23 | cleanupInterval time.Duration 24 | } 25 | 26 | type cacheItem struct { 27 | key string 28 | value any 29 | expiresAt time.Time 30 | } 31 | 32 | func New(maxSize int, ttl, cleanupInterval time.Duration) engine.Engine { 33 | c := &Basic{ 34 | data: make(map[string]*cacheItem), 35 | maxSize: maxSize, 36 | ttl: ttl, 37 | cleanupInterval: cleanupInterval, 38 | } 39 | 40 | go c.startCleanup() 41 | return c 42 | } 43 | 44 | func (c *Basic) Get(key string) (any, bool) { 45 | c.lock.RLock() 46 | defer c.lock.RUnlock() 47 | 48 | item, exists := c.data[key] 49 | if !exists || time.Now().After(item.expiresAt) { 50 | delete(c.data, key) 51 | return nil, false 52 | } 53 | 54 | return item.value, true 55 | } 56 | 57 | func (c *Basic) Set(key string, value any) { 58 | c.lock.Lock() 59 | defer c.lock.Unlock() 60 | 61 | c.data[key] = &cacheItem{ 62 | key: key, 63 | value: value, 64 | expiresAt: time.Now().Add(c.ttl), 65 | } 66 | } 67 | 68 | func (c *Basic) SetWithTTL(key string, value any, expiresAt time.Time) { 69 | c.lock.Lock() 70 | defer c.lock.Unlock() 71 | 72 | c.data[key] = &cacheItem{ 73 | key: key, 74 | value: value, 75 | expiresAt: expiresAt, 76 | } 77 | } 78 | 79 | func (c *Basic) Delete(key string) { 80 | c.lock.Lock() 81 | defer c.lock.Unlock() 82 | 83 | delete(c.data, key) 84 | } 85 | 86 | func (c *Basic) Has(key string) bool { 87 | c.lock.RLock() 88 | defer c.lock.RUnlock() 89 | 90 | item, exists := c.data[key] 91 | if !exists { 92 | return false 93 | } 94 | 95 | if time.Now().After(item.expiresAt) { 96 | return false 97 | } 98 | 99 | return true 100 | } 101 | 102 | func (c *Basic) Len() int { 103 | c.lock.RLock() 104 | defer c.lock.RUnlock() 105 | 106 | count := 0 107 | now := time.Now() 108 | for _, item := range c.data { 109 | if item.expiresAt.After(now) { 110 | count++ 111 | } 112 | } 113 | 114 | return count 115 | } 116 | 117 | func (c *Basic) Evict() { 118 | c.lock.Lock() 119 | defer c.lock.Unlock() 120 | 121 | now := time.Now() 122 | for key, item := range c.data { 123 | if item.expiresAt.Before(now) { 124 | delete(c.data, key) 125 | } 126 | } 127 | } 128 | 129 | func (c *Basic) IsExpirable() bool { 130 | return true 131 | } 132 | 133 | func (c *Basic) IsExpired(key string) bool { 134 | c.lock.Lock() 135 | defer c.lock.Unlock() 136 | 137 | item, exists := c.data[key] 138 | if !exists { 139 | return true 140 | } 141 | 142 | return time.Now().After(item.expiresAt) 143 | } 144 | 145 | func (c *Basic) startCleanup() { 146 | ticker := time.NewTicker(c.cleanupInterval) 147 | defer ticker.Stop() 148 | 149 | for range ticker.C { 150 | c.cleanupExpiredItems() 151 | } 152 | } 153 | 154 | func (c *Basic) cleanupExpiredItems() { 155 | for { 156 | time.Sleep(time.Second) 157 | c.lock.Lock() 158 | now := time.Now() 159 | for key, item := range c.data { 160 | if item.expiresAt.Before(now) { 161 | delete(c.data, key) 162 | } 163 | } 164 | c.lock.Unlock() 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "time" 7 | 8 | "github.com/hugocarreira/easycache/basic" 9 | "github.com/hugocarreira/easycache/fifo" 10 | "github.com/hugocarreira/easycache/lfu" 11 | "github.com/hugocarreira/easycache/lru" 12 | 13 | "github.com/hugocarreira/easycache/engine" 14 | ) 15 | 16 | // EvictionPolicy defines the possible cache eviction strategies. 17 | // 18 | // The eviction policy determines how items are removed when the cache reaches 19 | // its maximum size. The available policies are: 20 | // 21 | // - Basic: No automatic eviction, items are removed only when they expire (TTL-based). 22 | // - FIFO: First-In, First-Out eviction; the oldest item is removed first. 23 | // - LRU: Least Recently Used eviction; the least accessed item is removed first. 24 | // - LFU: Least Frequently Used eviction; the item with the fewest accesses is removed first. 25 | type EvictionPolicy int 26 | 27 | const ( 28 | Basic EvictionPolicy = iota 29 | FIFO 30 | LRU 31 | LFU 32 | ) 33 | 34 | // Cache is the main structure that manages an in-memory key-value store 35 | // with different eviction policies and optional TTL-based expiration. 36 | // 37 | // It acts as a wrapper around specific caching strategies such as FIFO, LRU, LFU, 38 | // or a simple TTL-based cache. The eviction policy is defined in the CacheConfig. 39 | // 40 | // The Cache structure provides thread-safe access with read/write locks and 41 | // includes built-in metrics for monitoring performance. 42 | type Cache struct { 43 | // lock ensures thread-safe access to the cache data. 44 | lock sync.RWMutex 45 | 46 | // engine represents the selected cache strategy (FIFO, LRU, LFU, or Basic). 47 | // It implements the CacheInterface to allow dynamic eviction policies. 48 | engine engine.Engine 49 | 50 | // Config holds the configuration settings, such as eviction policy, 51 | // max size, and TTL (if applicable). 52 | config *Config 53 | 54 | // metrics tracks cache statistics, including hits and misses. 55 | metrics *Metrics 56 | } 57 | 58 | func New(cfg *Config) *Cache { 59 | if cfg == nil { 60 | cfg = defaultConfig() 61 | } 62 | 63 | if cfg.CleanupInterval <= 0 { 64 | cfg.CleanupInterval = 10 * time.Second 65 | } 66 | 67 | c := &Cache{ 68 | config: cfg, 69 | metrics: NewMetrics(), 70 | } 71 | 72 | switch cfg.EvictionPolicy { 73 | case LRU: 74 | c.engine = lru.New(cfg.MaxSize) 75 | case FIFO: 76 | c.engine = fifo.New(cfg.MaxSize) 77 | case LFU: 78 | c.engine = lfu.New(cfg.MaxSize) 79 | default: 80 | c.engine = basic.New(cfg.MaxSize, cfg.TTL, cfg.CleanupInterval) 81 | } 82 | 83 | go c.startCheckMemoryUsage() 84 | 85 | return c 86 | } 87 | 88 | // startCheckMemoryUsage periodically monitors the cache's memory usage. 89 | // 90 | // If memory limits are set in CacheConfig, this function runs at the configured 91 | // interval (`MemoryCheckInterval`). When memory usage exceeds `MemoryLimits`, 92 | // the cache triggers cleanup to free up space 93 | func (c *Cache) startCheckMemoryUsage() { 94 | if c.config.MemoryLimits == 0 { 95 | return 96 | } 97 | 98 | if c.config.MemoryCheckInterval <= 0 { 99 | return 100 | } 101 | 102 | ticker := time.NewTicker(c.config.MemoryCheckInterval) 103 | defer ticker.Stop() 104 | 105 | maxMem := uint64(c.config.MemoryLimits) * 1024 * 1024 106 | 107 | for range ticker.C { 108 | var mem runtime.MemStats 109 | runtime.ReadMemStats(&mem) 110 | memAlloc := mem.Alloc / 1024 / 1024 111 | if memAlloc > maxMem { 112 | c.lock.Lock() 113 | c.engine.Evict() 114 | c.lock.Unlock() 115 | } 116 | } 117 | } 118 | 119 | // Get retrieves a value from the cache by its key. 120 | // 121 | // If the key exists and has not expired, the function returns the value and true. 122 | // If the key does not exist or has expired (in case of TTL-based eviction), 123 | // the function returns nil and false. Additionally, cache hit/miss metrics 124 | // are updated accordingly. 125 | func (c *Cache) Get(key string) (any, bool) { 126 | elem, exists := c.engine.Get(key) 127 | 128 | if !exists { 129 | if c.config.Metrics { 130 | c.metrics.IncrementMisses() 131 | } 132 | return nil, false 133 | } 134 | 135 | if c.engine.IsExpirable() { 136 | if c.engine.IsExpired(key) { 137 | c.lock.RUnlock() 138 | c.lock.Lock() 139 | go c.engine.Delete(key) 140 | c.lock.Unlock() 141 | 142 | if c.config.Metrics { 143 | c.metrics.IncrementMisses() 144 | } 145 | return nil, false 146 | } 147 | } 148 | 149 | if c.config.Metrics { 150 | c.metrics.IncrementHits() 151 | } 152 | 153 | return elem, true 154 | } 155 | 156 | // Set stores a key-value pair in the cache. 157 | // 158 | // If the key already exists, its value is updated. If the cache has a size limit 159 | // (`MaxSize`) and is full, the eviction policy (FIFO, LRU, LFU) is applied to remove an item 160 | // before inserting the new one. If TTL is enabled, the item will expire after the configured duration. 161 | func (c *Cache) Set(key string, value string) { 162 | if c.engine.IsExpirable() { 163 | expiration := time.Now().Add(c.config.TTL) 164 | c.engine.SetWithTTL(key, value, expiration) 165 | 166 | if c.config.Metrics { 167 | c.metrics.IncrementHits() 168 | } 169 | 170 | return 171 | } 172 | 173 | if c.engine.Has(key) { 174 | c.engine.Set(key, value) 175 | 176 | if c.config.Metrics { 177 | c.metrics.IncrementHits() 178 | } 179 | 180 | return 181 | } 182 | 183 | if c.config.MaxSize > 0 && c.Len() >= c.config.MaxSize { 184 | c.engine.Evict() 185 | } 186 | 187 | c.engine.Set(key, value) 188 | 189 | if c.config.Metrics { 190 | c.metrics.IncrementHits() 191 | } 192 | } 193 | 194 | // Delete removes a key-value pair from the cache. 195 | // 196 | // If the key exists, it is removed from both the primary storage and any 197 | // auxiliary structures (e.g., linked lists for LRU/FIFO or heaps for LFU). 198 | // If the key does not exist, the function does nothing. 199 | func (c *Cache) Delete(key string) { 200 | c.engine.Delete(key) 201 | } 202 | 203 | // Has checks whether a given key exists in the cache. 204 | // 205 | // Returns true if the key is present and has not expired (for TTL-based caches). 206 | // If the key does not exist or has expired, it returns false. 207 | func (c *Cache) Has(key string) bool { 208 | return c.engine.Has(key) 209 | } 210 | 211 | // Len returns the number of items currently stored in the cache. 212 | // 213 | // For TTL-based caches, only non-expired items are counted. In other eviction 214 | // policies (FIFO, LRU, LFU), it returns the total number of stored items. 215 | func (c *Cache) Len() int { 216 | return c.engine.Len() 217 | } 218 | 219 | func (c *Cache) Evict() { 220 | c.engine.Evict() 221 | } 222 | 223 | // Metrics returns a pointer to the cache's metrics instance. 224 | // 225 | // The metrics track cache performance, including hits and misses. 226 | // If metrics are disabled in the configuration, this function may return nil. 227 | func (c *Cache) Metrics() *Metrics { 228 | return c.metrics 229 | } 230 | -------------------------------------------------------------------------------- /cache/config.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "time" 4 | 5 | // Config defines the configuration settings for the cache. 6 | // 7 | // This struct allows customization of eviction policies, memory limits, TTL, 8 | // and other performance-related parameters. 9 | type Config struct { 10 | // EvictionPolicy determines the cache's item removal strategy (FIFO, LRU, LFU, or Basic). 11 | EvictionPolicy EvictionPolicy 12 | 13 | // MaxSize defines the maximum number of items the cache can hold before evicting entries. 14 | // A value of 0 means there is no limit. 15 | MaxSize int 16 | 17 | // TTL (Time-To-Live) specifies the duration before an item expires. 18 | // If set to 0, items will not expire automatically. 19 | TTL time.Duration 20 | 21 | // CleanupInterval defines how often expired items are removed from the cache. 22 | // This is only applicable if TTL-based expiration is enabled. 23 | CleanupInterval time.Duration 24 | 25 | // MemoryLimits specifies the maximum memory usage (in bytes) before triggering cache cleanup. 26 | // A value of 0 means memory usage is not restricted. 27 | MemoryLimits uint64 28 | 29 | // MemoryCheckInterval sets the frequency at which memory usage is checked. 30 | MemoryCheckInterval time.Duration 31 | 32 | // Metrics indicates whether cache statistics (hits, misses, evictions) should be collected. 33 | Metrics bool 34 | } 35 | 36 | // defaultConfig returns a Config with default settings. 37 | // 38 | // This configuration uses the Basic eviction policy, a default TTL of 60 seconds, 39 | // and a cleanup interval of 10 seconds. Metrics and memory limits are disabled by default. 40 | func defaultConfig() *Config { 41 | return &Config{ 42 | EvictionPolicy: Basic, 43 | MaxSize: 0, 44 | TTL: 60 * time.Second, 45 | CleanupInterval: 120 * time.Second, 46 | MemoryLimits: 0, 47 | MemoryCheckInterval: 0, 48 | Metrics: false, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cache/metrics.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync/atomic" 4 | 5 | // Metrics provides tracking for cache performance statistics. 6 | // 7 | // This struct collects and stores various cache metrics, including: 8 | // - Hits: Number of successful key lookups. 9 | // - Misses: Number of failed key lookups (key not found or expired). 10 | // 11 | // Metrics help monitor cache efficiency and can be used for performance tuning. 12 | type Metrics struct { 13 | hits int64 14 | misses int64 15 | } 16 | 17 | func NewMetrics() *Metrics { 18 | return &Metrics{ 19 | hits: 0, 20 | misses: 0, 21 | } 22 | } 23 | 24 | func (m *Metrics) IncrementHits() { 25 | atomic.AddInt64(&m.hits, 1) 26 | } 27 | 28 | func (m *Metrics) IncrementMisses() { 29 | atomic.AddInt64(&m.misses, 1) 30 | } 31 | 32 | func (m *Metrics) Hits() int64 { 33 | return atomic.LoadInt64(&m.hits) 34 | } 35 | 36 | func (m *Metrics) Misses() int64 { 37 | return atomic.LoadInt64(&m.misses) 38 | } 39 | 40 | func (m *Metrics) HitRate() float64 { 41 | hits := m.Hits() 42 | misses := m.Misses() 43 | 44 | if hits == 0 && misses == 0 { 45 | return 0 46 | } 47 | 48 | return float64(hits) / float64(hits+misses) 49 | } 50 | 51 | func (m *Metrics) MissRate() float64 { 52 | return 1 - m.HitRate() 53 | } 54 | 55 | func (m *Metrics) GetMetrics() *Metrics { 56 | return m 57 | } 58 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "time" 4 | 5 | // Engine defines the core behavior of a cache system. 6 | // 7 | // This interface abstracts different caching strategies, including FIFO, LRU, LFU, and TTL-based caches. 8 | // Implementations of this interface determine how items are stored, retrieved, and evicted. 9 | type Engine interface { 10 | // Get retrieves a value from the cache by its key. 11 | // Returns (value, true) if the key exists, otherwise returns (nil, false). 12 | Get(key string) (any, bool) 13 | 14 | // Set stores a key-value pair in the cache. 15 | // If the key already exists, its value is updated. 16 | Set(key string, value any) 17 | 18 | // SetWithTTL stores a key-value pair in the cache with an expiration time. 19 | // This method is only relevant for TTL-based caches. 20 | SetWithTTL(key string, value any, expiresAt time.Time) 21 | 22 | // Delete removes a key-value pair from the cache. 23 | Delete(key string) 24 | 25 | // Has checks whether a given key exists in the cache. 26 | // Returns true if the key is present and has not expired (for TTL-based caches). 27 | Has(key string) bool 28 | 29 | // Len returns the number of items currently stored in the cache. 30 | Len() int 31 | 32 | // IsExpirable returns true if the cache supports TTL-based expiration. 33 | IsExpirable() bool 34 | 35 | // IsExpired checks whether a specific key has expired. 36 | IsExpired(key string) bool 37 | 38 | // Evict removes an item from the cache based on the eviction policy (FIFO, LRU, LFU). 39 | Evict() 40 | } 41 | -------------------------------------------------------------------------------- /fifo/fifo.go: -------------------------------------------------------------------------------- 1 | package fifo 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | "time" 7 | 8 | "github.com/hugocarreira/easycache/engine" 9 | ) 10 | 11 | // FIFO (First-In, First-Out) is a cache implementation that removes 12 | // the oldest item when the cache reaches its maximum capacity. 13 | // 14 | // This eviction policy ensures that the first item added is the first one to be removed, 15 | // regardless of how frequently or recently it was accessed. 16 | // 17 | // FIFO is useful for scenarios where older data should be discarded in favor of newer data, 18 | // such as caching queue-like structures. 19 | type FIFO struct { 20 | maxSize int 21 | data map[string]*list.Element 22 | evictionList *list.List 23 | lock sync.RWMutex 24 | } 25 | 26 | type cacheItem struct { 27 | key string 28 | value any 29 | } 30 | 31 | func New(maxSize int) engine.Engine { 32 | return &FIFO{ 33 | maxSize: maxSize, 34 | data: make(map[string]*list.Element), 35 | evictionList: list.New(), 36 | } 37 | } 38 | 39 | func (c *FIFO) Get(key string) (any, bool) { 40 | c.lock.Lock() 41 | defer c.lock.Unlock() 42 | 43 | elem, exists := c.data[key] 44 | if !exists { 45 | return nil, false 46 | } 47 | 48 | return elem.Value.(*cacheItem).value, true 49 | } 50 | 51 | func (c *FIFO) Set(key string, value any) { 52 | c.lock.Lock() 53 | defer c.lock.Unlock() 54 | 55 | if elem, exists := c.data[key]; exists { 56 | elem.Value.(*cacheItem).value = value 57 | return 58 | } 59 | 60 | item := &cacheItem{key: key, value: value} 61 | elem := c.evictionList.PushBack(item) 62 | c.data[key] = elem 63 | } 64 | 65 | func (c *FIFO) SetWithTTL(key string, value any, expiresAt time.Time) { 66 | c.Set(key, value) 67 | } 68 | 69 | func (c *FIFO) Delete(key string) { 70 | c.lock.Lock() 71 | defer c.lock.Unlock() 72 | 73 | elem, exists := c.data[key] 74 | if !exists { 75 | return 76 | } 77 | 78 | c.evictionList.Remove(elem) 79 | delete(c.data, key) 80 | } 81 | 82 | func (c *FIFO) Has(key string) bool { 83 | c.lock.RLock() 84 | defer c.lock.RUnlock() 85 | 86 | _, exists := c.data[key] 87 | return exists 88 | } 89 | 90 | func (c *FIFO) Len() int { 91 | c.lock.RLock() 92 | defer c.lock.RUnlock() 93 | return len(c.data) 94 | } 95 | 96 | func (c *FIFO) IsExpirable() bool { 97 | return false 98 | } 99 | 100 | func (c *FIFO) IsExpired(key string) bool { 101 | return false 102 | } 103 | 104 | func (c *FIFO) Evict() { 105 | c.lock.Lock() 106 | defer c.lock.Unlock() 107 | 108 | if len(c.data) == 0 { 109 | return 110 | } 111 | 112 | elem := c.evictionList.Front() 113 | if elem != nil { 114 | item := elem.Value.(*cacheItem) 115 | delete(c.data, item.key) 116 | c.evictionList.Remove(elem) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hugocarreira/easycache 2 | 3 | go 1.23.5 4 | 5 | require github.com/stretchr/testify v1.10.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /lfu/lfu.go: -------------------------------------------------------------------------------- 1 | package lfu 2 | 3 | import ( 4 | "container/heap" 5 | "time" 6 | 7 | "github.com/hugocarreira/easycache/engine" 8 | ) 9 | 10 | // LFU (Least Frequently Used) is a cache implementation that removes 11 | // the least accessed item when the cache reaches its maximum capacity. 12 | // 13 | // Each item in the cache maintains a usage counter that increments every time the item is accessed. 14 | // When eviction is necessary, the item with the lowest usage count is removed. 15 | // 16 | // LFU is useful for scenarios where frequently accessed items should be retained 17 | // while less important data is discarded. 18 | type LFU struct { 19 | maxSize int 20 | data map[string]*cacheItem 21 | lfuHeap *lfuHeap 22 | } 23 | 24 | type cacheItem struct { 25 | key string 26 | value any 27 | frequency int 28 | index int 29 | } 30 | 31 | func New(maxSize int) engine.Engine { 32 | l := &lfuHeap{} 33 | heap.Init(l) 34 | 35 | return &LFU{ 36 | maxSize: maxSize, 37 | data: make(map[string]*cacheItem), 38 | lfuHeap: l, 39 | } 40 | } 41 | 42 | func (c *LFU) Get(key string) (any, bool) { 43 | item, exists := c.data[key] 44 | if !exists { 45 | return nil, false 46 | } 47 | 48 | item.frequency++ 49 | heap.Fix(c.lfuHeap, item.index) 50 | 51 | return item.value, true 52 | } 53 | 54 | func (c *LFU) Set(key string, value any) { 55 | if item, exists := c.data[key]; exists { 56 | item.value = value 57 | item.frequency++ 58 | heap.Fix(c.lfuHeap, item.index) 59 | return 60 | } 61 | 62 | item := &cacheItem{key: key, value: value, frequency: 1} 63 | heap.Push(c.lfuHeap, item) 64 | item.index = c.lfuHeap.Len() - 1 65 | c.data[key] = item 66 | } 67 | 68 | func (c *LFU) SetWithTTL(key string, value any, expiresAt time.Time) { 69 | c.Set(key, value) 70 | } 71 | 72 | func (c *LFU) Delete(key string) { 73 | item, exists := c.data[key] 74 | if !exists { 75 | return 76 | } 77 | 78 | heap.Remove(c.lfuHeap, item.index) 79 | delete(c.data, key) 80 | } 81 | 82 | func (c *LFU) Has(key string) bool { 83 | _, exists := c.data[key] 84 | return exists 85 | } 86 | 87 | func (c *LFU) Len() int { 88 | return len(c.data) 89 | } 90 | 91 | func (c *LFU) IsExpirable() bool { 92 | return false 93 | } 94 | 95 | func (c *LFU) IsExpired(key string) bool { 96 | return false 97 | } 98 | 99 | func (c *LFU) Evict() { 100 | if len(c.data) == 0 { 101 | return 102 | } 103 | 104 | item := heap.Pop(c.lfuHeap).(*cacheItem) 105 | delete(c.data, item.key) 106 | } 107 | 108 | type lfuHeap []*cacheItem 109 | 110 | func (l lfuHeap) Len() int { 111 | return len(l) 112 | } 113 | 114 | func (l lfuHeap) Less(i, j int) bool { 115 | return l[i].frequency < l[j].frequency 116 | } 117 | 118 | func (l lfuHeap) Swap(i, j int) { 119 | l[i], l[j] = l[j], l[i] 120 | l[i].index = i 121 | l[j].index = j 122 | } 123 | 124 | func (l *lfuHeap) Push(x any) { 125 | n := len(*l) 126 | item := x.(*cacheItem) 127 | item.index = n 128 | *l = append(*l, item) 129 | } 130 | 131 | func (l *lfuHeap) Pop() any { 132 | old := *l 133 | n := len(old) 134 | item := old[n-1] 135 | *l = old[0 : n-1] 136 | return item 137 | } 138 | -------------------------------------------------------------------------------- /lru/lru.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | "time" 7 | 8 | "github.com/hugocarreira/easycache/engine" 9 | ) 10 | 11 | // LRU (Least Recently Used) is a cache implementation that removes 12 | // the least recently accessed item when the cache reaches its maximum capacity. 13 | // 14 | // Each time an item is accessed, it is moved to the front of an internal 15 | // list, marking it as the most recently used. When eviction is necessary, 16 | // the item at the end of the list (the least recently used) is removed. 17 | // 18 | // LRU is useful for scenarios where recently accessed data should be prioritized, 19 | // such as web page caching or session management. 20 | type LRU struct { 21 | maxSize int 22 | data map[string]*list.Element 23 | evictionList *list.List 24 | lock sync.RWMutex 25 | } 26 | 27 | type cacheItem struct { 28 | key string 29 | value any 30 | } 31 | 32 | func New(maxSize int) engine.Engine { 33 | return &LRU{ 34 | maxSize: maxSize, 35 | data: make(map[string]*list.Element), 36 | evictionList: list.New(), 37 | } 38 | } 39 | 40 | func (c *LRU) Get(key string) (any, bool) { 41 | c.lock.Lock() 42 | defer c.lock.Unlock() 43 | 44 | elem, exists := c.data[key] 45 | if !exists { 46 | return nil, false 47 | } 48 | 49 | c.evictionList.MoveToFront(elem) 50 | value := elem.Value.(*cacheItem).value 51 | 52 | return value, true 53 | } 54 | 55 | func (c *LRU) Set(key string, value any) { 56 | c.lock.Lock() 57 | defer c.lock.Unlock() 58 | 59 | if elem, exists := c.data[key]; exists { 60 | c.evictionList.MoveToFront(elem) 61 | elem.Value.(*cacheItem).value = value 62 | return 63 | } 64 | 65 | item := &cacheItem{key: key, value: value} 66 | elem := c.evictionList.PushFront(item) 67 | c.data[key] = elem 68 | } 69 | 70 | func (c *LRU) SetWithTTL(key string, value any, expiresAt time.Time) { 71 | c.Set(key, value) 72 | } 73 | 74 | func (c *LRU) Delete(key string) { 75 | c.lock.Lock() 76 | defer c.lock.Unlock() 77 | 78 | elem, exists := c.data[key] 79 | if !exists { 80 | return 81 | } 82 | 83 | delete(c.data, key) 84 | c.evictionList.Remove(elem) 85 | } 86 | 87 | func (c *LRU) Has(key string) bool { 88 | c.lock.RLock() 89 | defer c.lock.RUnlock() 90 | 91 | _, exists := c.data[key] 92 | return exists 93 | } 94 | 95 | func (c *LRU) Len() int { 96 | c.lock.RLock() 97 | defer c.lock.RUnlock() 98 | 99 | return len(c.data) 100 | } 101 | 102 | func (c *LRU) Evict() { 103 | c.lock.Lock() 104 | defer c.lock.Unlock() 105 | 106 | if len(c.data) == 0 { 107 | return 108 | } 109 | 110 | elem := c.evictionList.Back() 111 | if elem != nil { 112 | item := elem.Value.(*cacheItem) 113 | delete(c.data, item.key) 114 | c.evictionList.Remove(elem) 115 | } 116 | } 117 | 118 | func (c *LRU) IsExpirable() bool { 119 | return false 120 | } 121 | 122 | func (c *LRU) IsExpired(key string) bool { 123 | return false 124 | } 125 | -------------------------------------------------------------------------------- /tests/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/hugocarreira/easycache/cache" 9 | ) 10 | 11 | var testCache *cache.Cache 12 | 13 | // Setup cache for benchmarks 14 | func init() { 15 | testCache = cache.New(&cache.Config{ 16 | EvictionPolicy: cache.LRU, 17 | MaxSize: 10000, 18 | TTL: 60 * time.Second, 19 | }) 20 | } 21 | 22 | // Benchmark for `Set()` 23 | func BenchmarkCacheSet(b *testing.B) { 24 | for i := 0; i < b.N; i++ { 25 | key := fmt.Sprintf("key-%d", i) 26 | testCache.Set(key, "value") 27 | } 28 | } 29 | 30 | // Benchmark for `Get()` 31 | func BenchmarkCacheGet(b *testing.B) { 32 | testCache.Set("existing-key", "value") 33 | 34 | b.ResetTimer() 35 | for i := 0; i < b.N; i++ { 36 | testCache.Get("existing-key") 37 | } 38 | } 39 | 40 | // Benchmark for `Delete()` 41 | func BenchmarkCacheDelete(b *testing.B) { 42 | testCache.Set("delete-key", "value") 43 | 44 | b.ResetTimer() 45 | for i := 0; i < b.N; i++ { 46 | testCache.Delete("delete-key") 47 | testCache.Set("delete-key", "value") 48 | } 49 | } 50 | 51 | // BenchmarkFIFO 52 | func BenchmarkFIFOEviction(b *testing.B) { 53 | c := cache.New(&cache.Config{ 54 | EvictionPolicy: cache.FIFO, 55 | MaxSize: 10000, 56 | }) 57 | 58 | for i := 0; i < b.N; i++ { 59 | key := fmt.Sprintf("key-%d", i) 60 | c.Set(key, "value") 61 | 62 | if i >= 10000 { 63 | c.Evict() 64 | } 65 | } 66 | } 67 | 68 | // BenchmarkLRU 69 | func BenchmarkLRUEviction(b *testing.B) { 70 | c := cache.New(&cache.Config{ 71 | EvictionPolicy: cache.LRU, 72 | MaxSize: 10000, 73 | }) 74 | 75 | for i := 0; i < b.N; i++ { 76 | key := fmt.Sprintf("key-%d", i) 77 | c.Set(key, "value") 78 | 79 | if i%10 == 0 { 80 | c.Get(key) 81 | } 82 | 83 | if i >= 10000 { 84 | c.Evict() 85 | } 86 | } 87 | } 88 | 89 | // BenchmarkLFU 90 | func BenchmarkLFUEviction(b *testing.B) { 91 | c := cache.New(&cache.Config{ 92 | EvictionPolicy: cache.LFU, 93 | MaxSize: 10000, 94 | }) 95 | 96 | for i := 0; i < b.N; i++ { 97 | key := fmt.Sprintf("key-%d", i) 98 | c.Set(key, "value") 99 | 100 | if i%5 == 0 { 101 | c.Get(key) 102 | c.Get(key) 103 | } 104 | 105 | if i >= 10000 { 106 | c.Evict() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/cache_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/hugocarreira/easycache/cache" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | // CacheTestSuite defines the test structure 13 | type CacheTestSuite struct { 14 | suite.Suite 15 | c *cache.Cache 16 | } 17 | 18 | // Setup before each test 19 | func (suite *CacheTestSuite) SetupTest() { 20 | suite.c = cache.New(&cache.Config{ 21 | EvictionPolicy: cache.Basic, 22 | MaxSize: 3, 23 | TTL: 60 * time.Second, 24 | }) 25 | } 26 | 27 | // Test `Set()` e `Get()` 28 | func (suite *CacheTestSuite) TestSetGet() { 29 | suite.c.Set("A", "Item A") 30 | suite.c.Set("B", "Item B") 31 | 32 | val, found := suite.c.Get("A") 33 | assert.True(suite.T(), found) 34 | assert.Equal(suite.T(), "Item A", val) 35 | 36 | val, found = suite.c.Get("B") 37 | assert.True(suite.T(), found) 38 | assert.Equal(suite.T(), "Item B", val) 39 | 40 | _, found = suite.c.Get("X") 41 | assert.False(suite.T(), found) 42 | } 43 | 44 | // Test `Delete()` 45 | func (suite *CacheTestSuite) TestDelete() { 46 | suite.c.Set("A", "Item A") 47 | suite.c.Delete("A") 48 | 49 | _, found := suite.c.Get("A") 50 | assert.False(suite.T(), found) 51 | } 52 | 53 | // Test `Has()` 54 | func (suite *CacheTestSuite) TestHas() { 55 | suite.c.Set("A", "Item A") 56 | 57 | assert.True(suite.T(), suite.c.Has("A")) 58 | assert.False(suite.T(), suite.c.Has("B")) 59 | } 60 | 61 | // Test `Len()` 62 | func (suite *CacheTestSuite) TestLen() { 63 | suite.c.Set("A", "Item A") 64 | suite.c.Set("B", "Item B") 65 | 66 | assert.Equal(suite.T(), 2, suite.c.Len()) 67 | } 68 | 69 | // Run the test suite 70 | func TestCacheTestSuite(t *testing.T) { 71 | suite.Run(t, new(CacheTestSuite)) 72 | } 73 | -------------------------------------------------------------------------------- /tests/fifo_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hugocarreira/easycache/cache" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // FIFOTestSuite defines the test structure 12 | type FIFOTestSuite struct { 13 | suite.Suite 14 | c *cache.Cache 15 | } 16 | 17 | // Setup before each test 18 | func (suite *FIFOTestSuite) SetupTest() { 19 | suite.c = cache.New(&cache.Config{ 20 | EvictionPolicy: cache.FIFO, 21 | MaxSize: 2, 22 | }) 23 | } 24 | 25 | // Test FIFO (First-In, First-Out) 26 | func (suite *FIFOTestSuite) TestFIFOEviction() { 27 | suite.c.Set("A", "Item A") 28 | suite.c.Set("B", "Item B") 29 | suite.c.Set("C", "Item C") 30 | 31 | assert.False(suite.T(), suite.c.Has("A")) 32 | assert.True(suite.T(), suite.c.Has("B")) 33 | assert.True(suite.T(), suite.c.Has("C")) 34 | } 35 | 36 | // Run the test suite 37 | func TestFIFOTestSuite(t *testing.T) { 38 | suite.Run(t, new(FIFOTestSuite)) 39 | } 40 | -------------------------------------------------------------------------------- /tests/lfu_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hugocarreira/easycache/cache" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // LFUTestSuite defines the test structure 12 | type LFUTestSuite struct { 13 | suite.Suite 14 | c *cache.Cache 15 | } 16 | 17 | // Setup before each test 18 | func (suite *LFUTestSuite) SetupTest() { 19 | suite.c = cache.New(&cache.Config{ 20 | EvictionPolicy: cache.LFU, 21 | MaxSize: 2, 22 | }) 23 | } 24 | 25 | // Test LFU (Least Frequently Used) 26 | func (suite *LFUTestSuite) TestLFUEviction() { 27 | suite.c.Set("A", "Item A") 28 | suite.c.Set("B", "Item B") 29 | 30 | suite.c.Get("A") 31 | suite.c.Get("A") 32 | 33 | suite.c.Set("C", "Item C") 34 | 35 | assert.False(suite.T(), suite.c.Has("B")) 36 | assert.True(suite.T(), suite.c.Has("A")) 37 | assert.True(suite.T(), suite.c.Has("C")) 38 | } 39 | 40 | // Run the test suite 41 | func TestLFUTestSuite(t *testing.T) { 42 | suite.Run(t, new(LFUTestSuite)) 43 | } 44 | -------------------------------------------------------------------------------- /tests/lru_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hugocarreira/easycache/cache" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // LRUTestSuite defines the test structure 12 | type LRUTestSuite struct { 13 | suite.Suite 14 | c *cache.Cache 15 | } 16 | 17 | // Setup before each test 18 | func (suite *LRUTestSuite) SetupTest() { 19 | suite.c = cache.New(&cache.Config{ 20 | EvictionPolicy: cache.LRU, 21 | MaxSize: 2, 22 | }) 23 | } 24 | 25 | // Test LRU (Least Recently Used) 26 | func (suite *LRUTestSuite) TestLRUEviction() { 27 | suite.c.Set("A", "Item A") 28 | suite.c.Set("B", "Item B") 29 | 30 | suite.c.Get("A") 31 | 32 | suite.c.Set("C", "Item C") 33 | 34 | assert.False(suite.T(), suite.c.Has("B")) 35 | assert.True(suite.T(), suite.c.Has("A")) 36 | assert.True(suite.T(), suite.c.Has("C")) 37 | } 38 | 39 | // Run the test suite 40 | func TestLRUTestSuite(t *testing.T) { 41 | suite.Run(t, new(LRUTestSuite)) 42 | } 43 | --------------------------------------------------------------------------------