...
This commit is contained in:
139
lib/data/cache/README.md
vendored
Normal file
139
lib/data/cache/README.md
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
# HeroLib Cache System
|
||||
|
||||
A high-performance, generic in-memory caching system for V with support for TTL, size limits, and LRU eviction.
|
||||
|
||||
## Features
|
||||
|
||||
- Generic type support (can cache any type)
|
||||
- Configurable maximum entries and memory size limits
|
||||
- Time-To-Live (TTL) support
|
||||
- Least Recently Used (LRU) eviction policy
|
||||
- Memory-aware caching with size-based eviction
|
||||
- Thread-safe operations
|
||||
- Optional persistence support (configurable)
|
||||
|
||||
## Configuration
|
||||
|
||||
The cache system is highly configurable through the `CacheConfig` struct:
|
||||
|
||||
```v
|
||||
pub struct CacheConfig {
|
||||
pub mut:
|
||||
max_entries u32 = 1000 // Maximum number of entries
|
||||
max_size_mb f64 = 100.0 // Maximum cache size in MB
|
||||
ttl_seconds i64 = 3600 // Time-to-live in seconds (0 = no TTL)
|
||||
eviction_ratio f64 = 0.05 // Percentage of entries to evict when full (5%)
|
||||
persist bool // Whether to persist cache to disk
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Here's a simple example of using the cache:
|
||||
|
||||
```v
|
||||
import freeflowuniverse.herolib.data.cache
|
||||
|
||||
// Define your struct type
|
||||
@[heap]
|
||||
struct User {
|
||||
id u32
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Create a cache with default configuration
|
||||
mut user_cache := cache.new_cache[User]()
|
||||
|
||||
// Create a user
|
||||
user := &User{
|
||||
id: 1
|
||||
name: 'Alice'
|
||||
age: 30
|
||||
}
|
||||
|
||||
// Add to cache
|
||||
user_cache.set(user.id, user)
|
||||
|
||||
// Retrieve from cache
|
||||
if cached_user := user_cache.get(1) {
|
||||
println('Found user: ${cached_user.name}')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
```v
|
||||
mut user_cache := cache.new_cache[User](
|
||||
max_entries: 1000 // Maximum number of entries
|
||||
max_size_mb: 10.0 // Maximum cache size in MB
|
||||
ttl_seconds: 300 // Items expire after 5 minutes
|
||||
eviction_ratio: 0.2 // Evict 20% of entries when full
|
||||
)
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
The cache automatically manages memory using two mechanisms:
|
||||
|
||||
1. **Entry Count Limit**: When `max_entries` is reached, least recently used items are evicted.
|
||||
2. **Memory Size Limit**: When `max_size_mb` is reached, items are evicted based on the `eviction_ratio`.
|
||||
|
||||
```v
|
||||
// Create a cache with strict memory limits
|
||||
config := cache.CacheConfig{
|
||||
max_entries: 100 // Only keep 100 entries maximum
|
||||
max_size_mb: 1.0 // Limit cache to 1MB
|
||||
eviction_ratio: 0.1 // Remove 10% of entries when full
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Operations
|
||||
|
||||
```v
|
||||
mut cache := cache.new_cache[User](cache.CacheConfig{})
|
||||
|
||||
// Add/update items
|
||||
cache.set(1, user1)
|
||||
cache.set(2, user2)
|
||||
|
||||
// Get items
|
||||
if user := cache.get(1) {
|
||||
// Use cached user
|
||||
}
|
||||
|
||||
// Check cache size
|
||||
println('Cache entries: ${cache.len()}')
|
||||
|
||||
// Clear the cache
|
||||
cache.clear()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Choose Appropriate TTL**: Set TTL based on how frequently your data changes and how critical freshness is.
|
||||
|
||||
2. **Memory Management**:
|
||||
- Set reasonable `max_entries` and `max_size_mb` limits based on your application's memory constraints
|
||||
- Monitor cache size using `len()`
|
||||
- Use appropriate `eviction_ratio` (typically 0.05-0.2) to balance performance and memory usage
|
||||
|
||||
3. **Type Safety**:
|
||||
- Always use `@[heap]` attribute for structs stored in cache
|
||||
- Ensure cached types are properly memory managed
|
||||
|
||||
4. **Error Handling**:
|
||||
- Always use option types when retrieving items (`if value := cache.get(key) {`)
|
||||
- Handle cache misses gracefully
|
||||
|
||||
5. **Performance**:
|
||||
- Consider the trade-off between cache size and hit rate
|
||||
- Monitor and adjust TTL and eviction settings based on usage patterns
|
||||
|
||||
## Thread Safety
|
||||
|
||||
The cache implementation is thread-safe for concurrent access. However, when using the cache in a multi-threaded environment, ensure proper synchronization when accessing cached objects.
|
||||
167
lib/data/cache/cache.v
vendored
Normal file
167
lib/data/cache/cache.v
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
module cache
|
||||
|
||||
import time
|
||||
import math
|
||||
|
||||
// CacheConfig holds cache configuration parameters
|
||||
pub struct CacheConfig {
|
||||
pub mut:
|
||||
max_entries u32 = 1000 // Maximum number of entries
|
||||
max_size_mb f64 = 100.0 // Maximum cache size in MB
|
||||
ttl_seconds i64 = 3600 // Time-to-live in seconds (0 = no TTL)
|
||||
eviction_ratio f64 = 0.05 // Percentage of entries to evict when full (5%)
|
||||
}
|
||||
|
||||
// CacheEntry represents a cached object with its metadata
|
||||
@[heap]
|
||||
struct CacheEntry[T] {
|
||||
mut:
|
||||
obj T // Reference to the cached object
|
||||
last_access i64 // Unix timestamp of last access
|
||||
created_at i64 // Unix timestamp of creation
|
||||
size u32 // Approximate size in bytes
|
||||
}
|
||||
|
||||
// Cache manages the in-memory caching of objects
|
||||
pub struct Cache[T] {
|
||||
mut:
|
||||
entries map[u32]&CacheEntry[T] // Map of object ID to cache entry
|
||||
config CacheConfig // Cache configuration
|
||||
access_log []u32 // Ordered list of object IDs by access time
|
||||
total_size u64 // Total size of cached entries in bytes
|
||||
}
|
||||
|
||||
// new_cache creates a new cache instance with the given configuration
|
||||
pub fn new_cache[T](config CacheConfig) &Cache[T] {
|
||||
return &Cache[T]{
|
||||
entries: map[u32]&CacheEntry[T]{}
|
||||
config: config
|
||||
access_log: []u32{cap: int(config.max_entries)}
|
||||
total_size: 0
|
||||
}
|
||||
}
|
||||
|
||||
// get retrieves an object from the cache if it exists
|
||||
pub fn (mut c Cache[T]) get(id u32) ?&T {
|
||||
if entry := c.entries[id] {
|
||||
now := time.now().unix()
|
||||
|
||||
// Check TTL
|
||||
if c.config.ttl_seconds > 0 {
|
||||
if (now - entry.created_at) > c.config.ttl_seconds {
|
||||
c.remove(id)
|
||||
return none
|
||||
}
|
||||
}
|
||||
|
||||
// Update access time
|
||||
unsafe {
|
||||
entry.last_access = now
|
||||
}
|
||||
// Move ID to end of access log
|
||||
idx := c.access_log.index(id)
|
||||
if idx >= 0 {
|
||||
c.access_log.delete(idx)
|
||||
}
|
||||
c.access_log << id
|
||||
|
||||
return &entry.obj
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// set adds or updates an object in the cache
|
||||
pub fn (mut c Cache[T]) set(id u32, obj &T) {
|
||||
now := time.now().unix()
|
||||
|
||||
// Calculate entry size (approximate)
|
||||
entry_size := sizeof(T) + sizeof(CacheEntry[T])
|
||||
|
||||
// Check memory and entry count limits
|
||||
new_total := c.total_size + u64(entry_size)
|
||||
max_bytes := u64(c.config.max_size_mb * 1024 * 1024)
|
||||
|
||||
// Always evict if we're at or above max_entries
|
||||
if c.entries.len >= int(c.config.max_entries) {
|
||||
c.evict()
|
||||
} else if new_total > max_bytes {
|
||||
// Otherwise evict only if we're over memory limit
|
||||
c.evict()
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
entry := &CacheEntry[T]{
|
||||
obj: *obj
|
||||
last_access: now
|
||||
created_at: now
|
||||
size: u32(entry_size)
|
||||
}
|
||||
|
||||
// Update total size
|
||||
if old := c.entries[id] {
|
||||
c.total_size -= u64(old.size)
|
||||
}
|
||||
c.total_size += u64(entry_size)
|
||||
|
||||
// Add to entries map
|
||||
c.entries[id] = entry
|
||||
|
||||
// Update access log
|
||||
idx := c.access_log.index(id)
|
||||
if idx >= 0 {
|
||||
c.access_log.delete(idx)
|
||||
}
|
||||
c.access_log << id
|
||||
|
||||
// Ensure access_log stays in sync with entries
|
||||
if c.access_log.len > c.entries.len {
|
||||
c.access_log = c.access_log[c.access_log.len - c.entries.len..]
|
||||
}
|
||||
}
|
||||
|
||||
// evict removes entries based on configured eviction ratio
|
||||
fn (mut c Cache[T]) evict() {
|
||||
// If we're at max entries, remove enough to get to 80% capacity
|
||||
target_size := int(c.config.max_entries) * 8 / 10 // 80%
|
||||
num_to_evict := if c.entries.len >= int(c.config.max_entries) {
|
||||
c.entries.len - target_size
|
||||
} else {
|
||||
math.max(1, int(c.entries.len * c.config.eviction_ratio))
|
||||
}
|
||||
|
||||
if num_to_evict > 0 {
|
||||
// Remove oldest entries
|
||||
mut evicted_size := u64(0)
|
||||
for i := 0; i < num_to_evict && i < c.access_log.len; i++ {
|
||||
id := c.access_log[i]
|
||||
if entry := c.entries[id] {
|
||||
evicted_size += u64(entry.size)
|
||||
c.entries.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update total size and access log
|
||||
c.total_size -= evicted_size
|
||||
c.access_log = c.access_log[num_to_evict..]
|
||||
}
|
||||
}
|
||||
|
||||
// remove deletes a single entry from the cache
|
||||
pub fn (mut c Cache[T]) remove(id u32) {
|
||||
if entry := c.entries[id] {
|
||||
c.total_size -= u64(entry.size)
|
||||
}
|
||||
c.entries.delete(id)
|
||||
}
|
||||
|
||||
// clear empties the cache
|
||||
pub fn (mut c Cache[T]) clear() {
|
||||
c.entries.clear()
|
||||
c.access_log.clear()
|
||||
c.total_size = 0
|
||||
}
|
||||
|
||||
// len returns the number of entries in the cache
|
||||
pub fn (c &Cache[T]) len() int {
|
||||
return c.entries.len
|
||||
}
|
||||
152
lib/data/cache/cache_test.v
vendored
Normal file
152
lib/data/cache/cache_test.v
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
module cache
|
||||
|
||||
import time
|
||||
|
||||
@[heap]
|
||||
struct TestData {
|
||||
value string
|
||||
}
|
||||
|
||||
fn test_cache_creation() {
|
||||
config := CacheConfig{
|
||||
max_entries: 100
|
||||
max_size_mb: 1.0
|
||||
ttl_seconds: 60
|
||||
eviction_ratio: 0.1
|
||||
}
|
||||
mut cache := new_cache[TestData](config)
|
||||
assert cache.len() == 0
|
||||
assert cache.config.max_entries == 100
|
||||
assert cache.config.max_size_mb == 1.0
|
||||
assert cache.config.ttl_seconds == 60
|
||||
assert cache.config.eviction_ratio == 0.1
|
||||
}
|
||||
|
||||
fn test_cache_set_get() {
|
||||
mut cache := new_cache[TestData](CacheConfig{})
|
||||
data := &TestData{
|
||||
value: 'test'
|
||||
}
|
||||
|
||||
cache.set(1, data)
|
||||
assert cache.len() == 1
|
||||
|
||||
if cached := cache.get(1) {
|
||||
assert cached.value == 'test'
|
||||
} else {
|
||||
assert false, 'Failed to get cached item'
|
||||
}
|
||||
|
||||
if _ := cache.get(2) {
|
||||
assert false, 'Should not find non-existent item'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_cache_ttl() {
|
||||
$if debug {
|
||||
eprintln('> test_cache_ttl')
|
||||
}
|
||||
mut cache := new_cache[TestData](CacheConfig{
|
||||
ttl_seconds: 1
|
||||
})
|
||||
data := &TestData{
|
||||
value: 'test'
|
||||
}
|
||||
|
||||
cache.set(1, data)
|
||||
assert cache.len() == 1
|
||||
|
||||
if cached := cache.get(1) {
|
||||
assert cached.value == 'test'
|
||||
}
|
||||
|
||||
time.sleep(2 * time.second)
|
||||
$if debug {
|
||||
eprintln('> waited 2 seconds')
|
||||
}
|
||||
|
||||
if _ := cache.get(1) {
|
||||
assert false, 'Item should have expired'
|
||||
}
|
||||
assert cache.len() == 0
|
||||
}
|
||||
|
||||
fn test_cache_eviction() {
|
||||
mut cache := new_cache[TestData](CacheConfig{
|
||||
max_entries: 2
|
||||
eviction_ratio: 0.5
|
||||
})
|
||||
|
||||
data1 := &TestData{
|
||||
value: 'one'
|
||||
}
|
||||
data2 := &TestData{
|
||||
value: 'two'
|
||||
}
|
||||
data3 := &TestData{
|
||||
value: 'three'
|
||||
}
|
||||
|
||||
cache.set(1, data1)
|
||||
cache.set(2, data2)
|
||||
assert cache.len() == 2
|
||||
|
||||
// Access data1 to make it more recently used
|
||||
cache.get(1)
|
||||
|
||||
// Adding data3 should trigger eviction of data2 (least recently used)
|
||||
cache.set(3, data3)
|
||||
assert cache.len() == 2
|
||||
|
||||
if _ := cache.get(2) {
|
||||
assert false, 'Item 2 should have been evicted'
|
||||
}
|
||||
|
||||
if cached := cache.get(1) {
|
||||
assert cached.value == 'one'
|
||||
} else {
|
||||
assert false, 'Item 1 should still be cached'
|
||||
}
|
||||
|
||||
if cached := cache.get(3) {
|
||||
assert cached.value == 'three'
|
||||
} else {
|
||||
assert false, 'Item 3 should be cached'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_cache_clear() {
|
||||
mut cache := new_cache[TestData](CacheConfig{})
|
||||
data := &TestData{
|
||||
value: 'test'
|
||||
}
|
||||
|
||||
cache.set(1, data)
|
||||
assert cache.len() == 1
|
||||
|
||||
cache.clear()
|
||||
assert cache.len() == 0
|
||||
|
||||
if _ := cache.get(1) {
|
||||
assert false, 'Cache should be empty after clear'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_cache_size_limit() {
|
||||
// Set a very small size limit to force eviction
|
||||
mut cache := new_cache[TestData](CacheConfig{
|
||||
max_size_mb: 0.0001 // ~100 bytes
|
||||
eviction_ratio: 0.5
|
||||
})
|
||||
|
||||
// Add multiple entries to exceed size limit
|
||||
for i := u32(0); i < 10; i++ {
|
||||
data := &TestData{
|
||||
value: 'test${i}'
|
||||
}
|
||||
cache.set(i, data)
|
||||
}
|
||||
|
||||
// Cache should have evicted some entries to stay under size limit
|
||||
assert cache.len() < 10
|
||||
}
|
||||
Reference in New Issue
Block a user