This commit is contained in:
2025-01-31 15:39:44 +03:00
parent 27cb6cb0c6
commit 74ab68d05f
185 changed files with 2321 additions and 972 deletions

139
lib/data/cache/README.md vendored Normal file
View 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
View 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
View 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
}