Documentation
¶
Overview ¶
Package balios provides the fastest in-memory cache implementation in Go.
Balios is based on W-TinyLFU (Window Tiny Least Frequently Used) algorithm and designed to outperform existing solutions like Otter and Ristretto through zero-allocation operations and lock-free data structures.
Example usage:
cache := balios.NewCache(balios.Config{
MaxSize: 10_000,
WindowRatio: 0.01,
})
cache.Set("key", "value")
value, found := cache.Get("key")
Copyright (c) 2025 AGILira - A. Giordano Series: an AGILira fragment SPDX-License-Identifier: MPL-2.0
Package balios provides a high-performance, thread-safe, in-memory cache implementation using the W-TinyLFU (Window-TinyLFU) eviction algorithm.
Overview ¶
Balios is designed for production use with focus on:
- Performance: 108.7 ns/op Get, 135.5 ns/op Set (AMD Ryzen 5 7520U)
- Concurrency: Lock-free operations using atomic primitives
- Type Safety: Generic API with compile-time type checking
- Observability: OpenTelemetry integration (optional separate package)
Features ¶
- W-TinyLFU Algorithm: Optimal cache hit ratio (combines frequency and recency)
- Lock-Free Design: Atomic operations for high concurrency
- Type-Safe Generics: GenericCache[K comparable, V any]
- TTL Support: Automatic expiration with lazy cleanup + manual ExpireNow() API
- GetOrLoad API: Cache stampede prevention with singleflight pattern
- Negative Caching: Cache loader errors to prevent repeated failures (v1.1.2+)
- Structured Errors: Rich error context with error codes
- Metrics Collection: MetricsCollector interface for observability
Quick Start ¶
Basic usage with generic API:
import "github.com/agilira/balios"
type User struct {
ID int
Name string
}
func main() {
// Create cache with generics (type-safe)
cache := balios.NewGenericCache[string, User](balios.Config{
MaxSize: 10_000,
TTL: time.Hour,
})
// Set value (type-safe, no interface{})
cache.Set("user:123", User{ID: 123, Name: "Alice"})
// Get value (no type assertion needed)
if user, found := cache.Get("user:123"); found {
fmt.Printf("User: %s\n", user.Name)
}
// Check stats
stats := cache.Stats()
fmt.Printf("Hit ratio: %.2f%%\n", stats.HitRatio()*100)
}
Cache Stampede Prevention ¶
The GetOrLoad API prevents cache stampede using singleflight pattern. Multiple concurrent requests for the same key execute the loader function only once:
user, err := cache.GetOrLoad("user:123", func() (User, error) {
// This expensive operation runs only once
// even if 1000 goroutines call GetOrLoad concurrently
return fetchUserFromDB(123)
})
if err != nil {
log.Printf("Failed to load user: %v", err)
}
With context support for timeout and cancellation:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := cache.GetOrLoadWithContext(ctx, "user:123",
func(ctx context.Context) (User, error) {
return fetchUserFromDBWithContext(ctx, 123)
})
Key characteristics of GetOrLoad:
- Cache hit: Same performance as Get() (27.90 ns/op parallel)
- Concurrent requests: N requests = 1 loader call (singleflight)
- Error handling: Errors can be cached with NegativeCacheTTL option (v1.1.2+)
- Panic recovery: Returns BALIOS_PANIC_RECOVERED error if loader panics
W-TinyLFU Algorithm ¶
W-TinyLFU (Window-TinyLFU) provides near-optimal cache hit ratios by combining:
- Window Cache: Recent items (20% of capacity) using LRU
- Main Cache: Frequent items (80% of capacity) using LFU with Count-Min Sketch
- Admission Policy: TinyLFU filter prevents one-hit-wonders from evicting valuable entries
The algorithm achieves 90-95% of OPT (optimal) hit ratio in real-world workloads while maintaining O(1) time complexity for all operations.
Memory overhead: ~4 bytes per cache entry (for frequency tracking).
Concurrency Model ¶
Balios uses a lock-free design with atomic operations:
- Reads: Atomic loads, no locks (except during eviction)
- Writes: CAS (Compare-And-Swap) operations
- Eviction: Fine-grained locking (only contested entries)
- Thread-Safe: All operations safe for concurrent use
Benchmark with 8 goroutines:
- Get: 27.90 ns/op (8 parallel)
- Set: 39.47 ns/op (8 parallel)
- No deadlocks or race conditions
TTL (Time To Live) ¶
Automatic expiration with configurable TTL:
cache := balios.NewGenericCache[string, User](balios.Config{
MaxSize: 10_000,
TTL: 5 * time.Minute, // Entries expire after 5 minutes
})
TTL features:
- Lazy Expiration: Checked on access, not proactive scanning
- Per-Entry Timestamps: Nanosecond precision
- Zero Overhead: No background goroutines
- Configurable: Set via Config.TTL
Observability ¶
Built-in stats tracking:
stats := cache.Stats()
fmt.Printf("Hits: %d, Misses: %d, Hit Ratio: %.2f%%\n",
stats.Hits, stats.Misses, stats.HitRatio()*100)
fmt.Printf("Size: %d, Evictions: %d\n",
stats.Size, stats.Evictions)
Enterprise observability with OpenTelemetry (optional):
import baliosostel "github.com/agilira/balios/otel"
// Setup OTEL with Prometheus exporter
exporter, _ := prometheus.New()
provider := metric.NewMeterProvider(metric.WithReader(exporter))
// Create metrics collector
metricsCollector, _ := baliosostel.NewOTelMetricsCollector(provider)
// Configure cache with metrics
cache := balios.NewGenericCache[string, User](balios.Config{
MaxSize: 10_000,
MetricsCollector: metricsCollector, // Optional, zero overhead if nil
})
Metrics exposed (via OpenTelemetry):
- balios_get_latency_ns: Histogram with automatic percentiles (p50, p95, p99, p99.9)
- balios_set_latency_ns: Set operation latencies
- balios_delete_latency_ns: Delete operation latencies
- balios_get_hits_total: Counter of cache hits
- balios_get_misses_total: Counter of cache misses
- balios_evictions_total: Counter of evictions
The core balios package has zero OTEL dependencies. The balios/otel package is a separate module (~5% overhead when used).
Configuration ¶
Complete configuration options:
config := balios.Config{
// Required: Maximum number of entries
MaxSize: 10_000,
// Optional: Time-to-live for entries (default: no expiration)
TTL: time.Hour,
// Optional: Negative cache TTL for loader errors (default: 0, disabled)
// When enabled, failed loads are cached to prevent repeated expensive failures
NegativeCacheTTL: 5 * time.Second,
// Optional: Logger for errors and events (default: nil)
Logger: myLogger,
// Optional: Metrics collector (default: NoOp, zero overhead)
MetricsCollector: metricsCollector,
// Optional: Custom time provider for testing (default: real time)
TimeProvider: myTimeProvider,
}
cache := balios.NewGenericCache[string, User](config)
Error Handling ¶
Balios uses structured errors with error codes:
user, err := cache.GetOrLoad("user:123", loader)
if err != nil {
if errors.Is(err, balios.ErrLoaderPanic) {
// Loader panicked, check error for details
log.Printf("Loader panic: %v", err)
} else if errors.Is(err, balios.ErrContextCanceled) {
// Context was canceled
log.Printf("Operation canceled: %v", err)
} else {
// Other loader error
log.Printf("Loader failed: %v", err)
}
return
}
Available error codes:
- BALIOS_EMPTY_KEY: Empty key provided (keys cannot be empty)
- BALIOS_INVALID_LOADER: Loader function is nil
- BALIOS_PANIC_RECOVERED: Loader function panicked (panic value included)
- BALIOS_LOADER_FAILED: Loader function returned error
- BALIOS_INVALID_CONFIG: Invalid configuration
All errors implement error interface and can be unwrapped.
Performance ¶
Benchmark results (AMD Ryzen 5 7520U, Go 1.25+):
BenchmarkBalios_Set_SingleThread-8 17974251 194.4 ns/op 26 B/op 2 allocs/op BenchmarkBalios_Get_SingleThread-8 32055717 110.8 ns/op 0 B/op 0 allocs/op BenchmarkBalios_Set_Parallel-8 60964353 59.26 ns/op 26 B/op 2 allocs/op BenchmarkBalios_Get_Parallel-8 139151439 25.50 ns/op 0 B/op 0 allocs/op
Key characteristics:
- Zero allocations on Get operations
- Lock-free reads (except during eviction)
- Excellent parallel scalability
- Sub-microsecond latencies
Hit ratio comparison (100K requests, Zipf distribution):
- Balios: 79.86%
- Balios-Generic: 79.71%
- Otter: 79.53%
- Ristretto: 71.19%
Memory Layout ¶
Internal structure (for 10,000 entry cache):
Cache Entry: 48 bytes (key hash + value pointer + metadata) Hash Table: 160 KB (20,000 buckets * 8 bytes) TinyLFU: 20 KB (Count-Min Sketch: 4 rows * 5,000 counters * 1 byte) Window LRU: 96 KB (2,000 entries * 48 bytes) Main Cache: 384 KB (8,000 entries * 48 bytes) Total: ~660 KB overhead + entry values
Memory per entry: ~66 bytes overhead (excluding value size)
Thread Safety ¶
All cache operations are thread-safe:
cache := balios.NewGenericCache[string, int](balios.Config{MaxSize: 1000})
// Safe to use from multiple goroutines
go func() { cache.Set("key1", 1) }()
go func() { cache.Get("key1") }()
go func() { cache.Delete("key1") }()
go func() { stats := cache.Stats() }()
Internal synchronization:
- Atomic operations for reads
- CAS for writes
- Fine-grained locks during eviction
- No global locks
Tested with -race detector: zero race conditions detected.
Legacy Interface API ¶
Non-generic API for compatibility (uses interface{}):
cache := balios.NewCache(balios.Config{MaxSize: 10_000})
cache.Set("key", User{ID: 123, Name: "Alice"})
if value, found := cache.Get("key"); found {
user := value.(User) // Type assertion required
fmt.Printf("User: %s\n", user.Name)
}
Prefer the generic API (NewGenericCache) for type safety.
Best Practices ¶
1. Size the cache appropriately:
- Too small: High eviction rate, poor hit ratio
- Too large: Wasted memory, slower lookups
- Rule of thumb: ~2x your working set
2. Monitor hit ratio:
- Target: >70% for most workloads
- Low hit ratio indicates cache too small or poor key distribution
3. Use GetOrLoad for expensive operations:
- Prevents cache stampede
- Automatic deduplication of concurrent requests
4. Set appropriate TTL:
- Too short: More cache misses
- Too long: Stale data
- Consider data freshness requirements
5. Handle loader errors:
- Errors can be cached with NegativeCacheTTL to prevent repeated failures
- Implement retry logic in loader if needed
- Use context timeouts to prevent hanging
6. Use context with timeout:
- Prevents hanging on slow loaders
- Enables graceful cancellation
7. Enable metrics in production:
- Use balios/otel package for observability
- Monitor p95/p99 latencies
- Alert on low hit ratio or high eviction rate
Examples ¶
See the examples directory for complete working examples:
- examples/getorload/: GetOrLoad API usage
- examples/otel-prometheus/: OpenTelemetry + Prometheus + Grafana integration
- examples/errors/: Error handling patterns
Documentation ¶
Detailed documentation:
- docs/ARCHITECTURE.md: W-TinyLFU internals, lock-free design
- docs/GETORLOAD.md: Cache stampede prevention, singleflight pattern
- docs/METRICS.md: Observability, PromQL queries, Grafana dashboards
- docs/ERRORS.md: Error codes, structured errors
Packages ¶
- github.com/agilira/balios: Core cache implementation
- github.com/agilira/balios/otel: OpenTelemetry integration (separate module)
License ¶
See LICENSE file in the repository.
Contributions welcome at https://github.com/agilira/balios
errors.go: comprehensive error handling for balios cache operations
This file provides structured error types using agilira/go-errors, enabling rich error context, categorization, and standardized error codes for all cache operations.
Copyright (c) 2025 AGILira - A. Giordano Series: an AGILira fragment SPDX-License-Identifier: MPL-2.0
loading.go: GetOrLoad implementation with singleflight pattern
This file implements the GetOrLoad and GetOrLoadWithContext methods, providing cache-aside pattern with automatic deduplication of concurrent loads using a singleflight mechanism.
Copyright (c) 2025 AGILira - A. Giordano Series: an AGILira fragment SPDX-License-Identifier: MPL-2.0
loading_generic.go: type-safe GetOrLoad implementation with generics
This file provides generic versions of GetOrLoad and GetOrLoadWithContext, enabling type-safe cache-aside pattern without type assertions.
Copyright (c) 2025 AGILira - A. Giordano Series: an AGILira fragment SPDX-License-Identifier: MPL-2.0
Index ¶
- Constants
- func GetErrorCode(err error) errors.ErrorCode
- func GetErrorContext(err error) map[string]interface{}
- func IsCacheFull(err error) bool
- func IsConfigError(err error) bool
- func IsEmptyKey(err error) bool
- func IsLoaderError(err error) bool
- func IsNotFound(err error) bool
- func IsOperationError(err error) bool
- func IsPersistenceError(err error) bool
- func IsRetryable(err error) bool
- func NewErrCacheFull(capacity int, size int) error
- func NewErrCorruptedData(filepath string, details string) error
- func NewErrDeleteFailed(key string, reason string) error
- func NewErrEmptyKey(operation string) error
- func NewErrEvictionFailed(reason string) error
- func NewErrInternal(operation string, cause error) error
- func NewErrInvalidCounterBits(bits int) error
- func NewErrInvalidLoader(key string) error
- func NewErrInvalidMaxSize(size int) error
- func NewErrInvalidTTL(ttl interface{}) error
- func NewErrInvalidWindowRatio(ratio float64) error
- func NewErrKeyNotFound(key string) error
- func NewErrLoadFailed(filepath string, cause error) error
- func NewErrLoaderCancelled(key string) error
- func NewErrLoaderFailed(key string, cause error) error
- func NewErrLoaderTimeout(key string, timeout interface{}) error
- func NewErrPanicRecovered(operation string, panicValue interface{}) error
- func NewErrSaveFailed(filepath string, cause error) error
- func NewErrSetFailed(key string, reason string) error
- type Cache
- type CacheStats
- type Config
- type GenericCache
- func (c *GenericCache[K, V]) Capacity() int
- func (c *GenericCache[K, V]) Clear()
- func (c *GenericCache[K, V]) Close() error
- func (c *GenericCache[K, V]) Delete(key K)
- func (c *GenericCache[K, V]) Get(key K) (value V, found bool)
- func (c *GenericCache[K, V]) GetOrLoad(key K, loader func() (V, error)) (V, error)
- func (c *GenericCache[K, V]) GetOrLoadWithContext(ctx context.Context, key K, loader func(context.Context) (V, error)) (V, error)
- func (c *GenericCache[K, V]) Has(key K) bool
- func (c *GenericCache[K, V]) Len() int
- func (c *GenericCache[K, V]) Set(key K, value V)
- func (c *GenericCache[K, V]) Stats() CacheStats
- type Logger
- type MetricsCollector
- type NoOpLogger
- type NoOpMetricsCollector
- type TimeProvider
Examples ¶
Constants ¶
const ( // Version of Balios cache library Version = "v1.0.1" // DefaultMaxSize is the default maximum number of entries DefaultMaxSize = 10_000 // DefaultWindowRatio is the default ratio of window cache to total cache size DefaultWindowRatio = 0.01 // 1% // DefaultCounterBits is the default number of bits per counter in frequency sketch DefaultCounterBits = 4 )
const ( // Configuration errors (1xxx) ErrCodeInvalidConfig errors.ErrorCode = "BALIOS_INVALID_CONFIG" ErrCodeInvalidMaxSize errors.ErrorCode = "BALIOS_INVALID_MAX_SIZE" ErrCodeInvalidWindowRatio errors.ErrorCode = "BALIOS_INVALID_WINDOW_RATIO" ErrCodeInvalidCounterBits errors.ErrorCode = "BALIOS_INVALID_COUNTER_BITS" ErrCodeInvalidTTL errors.ErrorCode = "BALIOS_INVALID_TTL" // Operation errors (2xxx) ErrCodeCacheFull errors.ErrorCode = "BALIOS_CACHE_FULL" ErrCodeKeyNotFound errors.ErrorCode = "BALIOS_KEY_NOT_FOUND" ErrCodeEmptyKey errors.ErrorCode = "BALIOS_EMPTY_KEY" ErrCodeEvictionFailed errors.ErrorCode = "BALIOS_EVICTION_FAILED" ErrCodeSetFailed errors.ErrorCode = "BALIOS_SET_FAILED" ErrCodeDeleteFailed errors.ErrorCode = "BALIOS_DELETE_FAILED" // Loader errors (3xxx) ErrCodeLoaderFailed errors.ErrorCode = "BALIOS_LOADER_FAILED" ErrCodeLoaderTimeout errors.ErrorCode = "BALIOS_LOADER_TIMEOUT" ErrCodeLoaderCancelled errors.ErrorCode = "BALIOS_LOADER_CANCELLED" ErrCodeInvalidLoader errors.ErrorCode = "BALIOS_INVALID_LOADER" // Persistence errors (4xxx) ErrCodeSaveFailed errors.ErrorCode = "BALIOS_SAVE_FAILED" ErrCodeLoadFailed errors.ErrorCode = "BALIOS_LOAD_FAILED" ErrCodeCorruptedData errors.ErrorCode = "BALIOS_CORRUPTED_DATA" // Internal errors (5xxx) ErrCodeInternalError errors.ErrorCode = "BALIOS_INTERNAL_ERROR" ErrCodePanicRecovered errors.ErrorCode = "BALIOS_PANIC_RECOVERED" )
Error codes for Balios cache operations
Variables ¶
This section is empty.
Functions ¶
func GetErrorCode ¶
GetErrorCode extracts the error code from an error
func GetErrorContext ¶
GetErrorContext extracts context from an error
func IsCacheFull ¶
IsCacheFull checks if error is a cache full error
func IsConfigError ¶
IsConfigError checks if error is a configuration error
func IsEmptyKey ¶ added in v1.1.2
IsEmptyKey checks if error is an empty key error
func IsLoaderError ¶
IsLoaderError checks if error is a loader error
func IsNotFound ¶
IsNotFound checks if error is a key not found error
func IsOperationError ¶
IsOperationError checks if error is an operation error
func IsPersistenceError ¶
IsPersistenceError checks if error is a persistence error
func NewErrCacheFull ¶
NewErrCacheFull creates an error when cache is full and eviction fails
func NewErrCorruptedData ¶
NewErrCorruptedData creates an error when data is corrupted
func NewErrDeleteFailed ¶
NewErrDeleteFailed creates an error when Delete operation fails
func NewErrEmptyKey ¶ added in v1.1.2
NewErrEmptyKey creates an error when key is empty
func NewErrEvictionFailed ¶
NewErrEvictionFailed creates an error when eviction fails
func NewErrInternal ¶
NewErrInternal creates a generic internal error
func NewErrInvalidCounterBits ¶
NewErrInvalidCounterBits creates an error for invalid counter bits
func NewErrInvalidLoader ¶
NewErrInvalidLoader creates an error when loader function is nil
func NewErrInvalidMaxSize ¶
NewErrInvalidMaxSize creates an error for invalid max size
func NewErrInvalidTTL ¶
func NewErrInvalidTTL(ttl interface{}) error
NewErrInvalidTTL creates an error for invalid TTL
func NewErrInvalidWindowRatio ¶
NewErrInvalidWindowRatio creates an error for invalid window ratio
func NewErrKeyNotFound ¶
NewErrKeyNotFound creates an error when key is not found
func NewErrLoadFailed ¶
NewErrLoadFailed creates an error when load operation fails
func NewErrLoaderCancelled ¶
NewErrLoaderCancelled creates an error when loader is cancelled
func NewErrLoaderFailed ¶
NewErrLoaderFailed creates an error when loader function fails
func NewErrLoaderTimeout ¶
NewErrLoaderTimeout creates an error when loader times out
func NewErrPanicRecovered ¶
NewErrPanicRecovered creates an error when a panic is recovered
func NewErrSaveFailed ¶
NewErrSaveFailed creates an error when save operation fails
func NewErrSetFailed ¶
NewErrSetFailed creates an error when Set operation fails
Types ¶
type Cache ¶
type Cache interface {
// Get retrieves a value from the cache.
// Returns the value and true if found, nil and false otherwise.
// This method must be zero-allocation on the hot path.
Get(key string) (value interface{}, found bool)
// Set stores a key-value pair in the cache.
// Returns true if the item was successfully stored.
//
// Note: Returns false only in extreme cases when the cache is full and
// eviction fails repeatedly, which is virtually impossible in normal operation
// (< 0.001% probability with proper cache sizing). In practice, Set() always succeeds.
//
// This method must be zero-allocation on the hot path.
Set(key string, value interface{}) bool
// Delete removes an item from the cache.
// Returns true if the item was present and removed.
Delete(key string) bool
// Has checks if a key exists in the cache without retrieving the value.
// This method should be faster than Get when only existence matters.
Has(key string) bool
// Len returns the current number of items in the cache.
Len() int
// Capacity returns the maximum number of items the cache can hold.
Capacity() int
// Clear removes all items from the cache.
// Note: This operation is not atomic. During Clear(), other goroutines
// may still read/write, potentially observing a partially cleared cache.
// This is acceptable for most use cases (cache flush, shutdown, testing).
Clear()
// Stats returns cache statistics.
Stats() CacheStats
// GetOrLoad returns the value from cache, or loads it using the provided loader.
// If multiple goroutines call GetOrLoad for the same missing key concurrently,
// only one loader will be executed (singleflight pattern).
// The loaded value is cached with the cache's default TTL.
// If the loader returns an error, the error is NOT cached.
GetOrLoad(key string, loader func() (interface{}, error)) (interface{}, error)
// GetOrLoadWithContext is like GetOrLoad but respects context cancellation and timeout.
// The context is passed to the loader function for cancellation control.
GetOrLoadWithContext(ctx context.Context, key string, loader func(context.Context) (interface{}, error)) (interface{}, error)
// ExpireNow manually expires all entries that have exceeded their TTL.
// This method scans the entire cache and removes expired entries immediately.
// Returns the number of entries that were expired and removed.
//
// Use cases:
// - Periodic cleanup via external scheduler (cron, ticker)
// - Pre-shutdown cleanup to free resources
// - Testing and debugging expiration behavior
//
// Performance:
// - O(n) where n is the number of entries in the cache
// - Lock-free with CAS operations for thread safety
// - Safe to call concurrently with other cache operations
// - Returns 0 immediately if TTL is not configured
//
// Returns:
// - Number of expired entries removed from the cache
ExpireNow() int
// Close gracefully shuts down the cache and releases resources.
Close() error
}
Cache represents a high-performance in-memory cache interface. All methods must be safe for concurrent use.
Example (Negative_caching) ¶
ExampleCache_negative_caching demonstrates error caching.
package main
import (
"fmt"
"time"
"github.com/agilira/balios"
)
func main() {
cache := balios.NewCache(balios.Config{
MaxSize: 100,
TTL: time.Hour,
NegativeCacheTTL: 5 * time.Second, // Cache errors for 5 seconds
})
defer func() { _ = cache.Close() }()
callCount := 0
failingLoader := func() (interface{}, error) {
callCount++
return nil, fmt.Errorf("database unavailable")
}
// First call: loader fails, error is cached
_, err := cache.GetOrLoad("key", failingLoader)
fmt.Printf("First call - Count: %d, Error: %v\n", callCount, err != nil)
// Second call within 5 seconds: returns cached error without calling loader
_, err = cache.GetOrLoad("key", failingLoader)
fmt.Printf("Second call - Count: %d, Error: %v\n", callCount, err != nil)
}
Output: First call - Count: 1, Error: true Second call - Count: 1, Error: true
func NewCache ¶
NewCache creates a new W-TinyLFU cache with lock-free operations.
Example ¶
ExampleNewCache demonstrates basic cache creation and usage.
package main
import (
"fmt"
"time"
"github.com/agilira/balios"
)
func main() {
// Create a cache with default configuration
cache := balios.NewCache(balios.Config{
MaxSize: 1000,
TTL: time.Hour,
})
defer func() { _ = cache.Close() }()
// Store a value
cache.Set("user:123", map[string]string{
"name": "John Doe",
"email": "john@example.com",
})
// Retrieve the value
if _, found := cache.Get("user:123"); found {
fmt.Println("Found user in cache")
}
}
Output: Found user in cache
type CacheStats ¶
type CacheStats struct {
// Hits is the number of cache hits
Hits uint64
// Misses is the number of cache misses
Misses uint64
// Sets is the number of successful set operations
Sets uint64
// Deletes is the number of successful delete operations
Deletes uint64
// Evictions is the number of items evicted from the cache
Evictions uint64
// Expirations is the number of items expired due to TTL
Expirations uint64
// Size is the current number of items in the cache
Size int
// Capacity is the maximum number of items the cache can hold
Capacity int
}
CacheStats provides statistics about cache performance.
func (CacheStats) HitRatio ¶
func (s CacheStats) HitRatio() float64
HitRatio returns the cache hit ratio as a percentage (0-100). Returns 0.0 if no Get operations have been performed yet. Formula: (Hits / (Hits + Misses)) * 100
type Config ¶
type Config struct {
// MaxSize is the maximum number of entries the cache can hold.
// Must be > 0. Default: DefaultMaxSize.
MaxSize int
// WindowRatio is the ratio of window cache to total cache size.
// Must be between 0.0 and 1.0. Default: DefaultWindowRatio.
WindowRatio float64
// CounterBits is the number of bits per counter in the frequency sketch.
// Must be between 1 and 8. Default: DefaultCounterBits.
CounterBits int
// TTL is the time-to-live for cache entries.
// If 0, entries never expire. Default: 0 (no expiration).
TTL time.Duration
// NegativeCacheTTL is the time-to-live for caching loader errors.
// When GetOrLoad fails, the error can be cached to prevent repeated
// expensive operations that consistently fail.
// If 0, errors are not cached (default behavior).
// Recommended: 1-10 seconds for most use cases.
// Example: Database unreachable errors don't need to be retried every millisecond.
NegativeCacheTTL time.Duration
// CleanupInterval is how often to run cleanup of expired entries.
// Only used if TTL > 0. Default: TTL / 10.
CleanupInterval time.Duration
// Logger is used for debugging and monitoring.
// If nil, NoOpLogger is used. Default: NoOpLogger.
Logger Logger
// TimeProvider provides current time for TTL calculations.
// If nil, a default implementation is used. Default: system time.
TimeProvider TimeProvider
// MetricsCollector is used for collecting operation metrics (latencies, hit/miss rates).
// If nil, NoOpMetricsCollector is used (zero overhead). Default: NoOpMetricsCollector.
// Use this to integrate with Prometheus, DataDog, StatsD, or other monitoring systems.
MetricsCollector MetricsCollector
// OnEvict is called when an entry is evicted from the cache.
// This callback must be fast and non-blocking.
OnEvict func(key string, value interface{})
// OnExpire is called when an entry expires (TTL-based removal).
// This callback must be fast and non-blocking.
OnExpire func(key string, value interface{})
}
Config holds configuration parameters for the cache.
Example ¶
ExampleConfig demonstrates advanced cache configuration.
package main
import (
"fmt"
"time"
"github.com/agilira/balios"
)
func main() {
cache := balios.NewCache(balios.Config{
MaxSize: 10_000, // Maximum 10k entries
TTL: 30 * time.Minute, // Entries expire after 30 minutes
NegativeCacheTTL: 5 * time.Second, // Cache errors for 5 seconds
WindowRatio: 0.01, // 1% window cache (W-TinyLFU)
OnEvict: func(key string, value interface{}) {
// Called when an entry is evicted
fmt.Printf("Evicted: %s\n", key)
},
OnExpire: func(key string, value interface{}) {
// Called when an entry expires
fmt.Printf("Expired: %s\n", key)
},
})
defer func() { _ = cache.Close() }()
cache.Set("key", "value")
// Cache is now configured and ready to use
}
Output:
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a configuration with sensible defaults.
func (*Config) Validate ¶
Validate checks configuration parameters and applies sensible defaults. Returns nil (no actual validation errors, only normalization).
This method is automatically called by NewCache and NewGenericCache, so you typically don't need to call it manually. However, it's provided as a public API if you want to inspect the normalized configuration before creating a cache.
Default values applied:
- MaxSize: DefaultMaxSize (10,000) if <= 0
- WindowRatio: DefaultWindowRatio (0.01) if <= 0 or >= 1
- CounterBits: DefaultCounterBits (4) if < 1 or > 8
- CleanupInterval: TTL/10 if TTL > 0 and CleanupInterval <= 0
- Logger: NoOpLogger{} if nil
- TimeProvider: systemTimeProvider{} if nil
- MetricsCollector: NoOpMetricsCollector{} if nil
type GenericCache ¶
type GenericCache[K comparable, V any] struct { // contains filtered or unexported fields }
GenericCache provides a type-safe cache interface using Go generics. K must be comparable (can be used as map key). V can be any type.
Example:
cache := balios.NewGenericCache[string, User](balios.Config{
MaxSize: 10_000,
TTL: time.Hour,
})
cache.Set("user:123", user)
if value, found := cache.Get("user:123"); found {
fmt.Printf("User: %+v\n", value)
}
Example (Integer_keys) ¶
ExampleGenericCache_integer_keys demonstrates using integer keys.
package main
import (
"fmt"
"github.com/agilira/balios"
)
func main() {
// Create cache with integer keys
cache := balios.NewGenericCache[int, string](balios.Config{
MaxSize: 100,
})
defer func() { _ = cache.Close() }()
// Store HTTP status messages
cache.Set(200, "OK")
cache.Set(404, "Not Found")
cache.Set(500, "Internal Server Error")
// Retrieve by integer key
if msg, found := cache.Get(404); found {
fmt.Printf("HTTP 404: %s\n", msg)
}
}
Output: HTTP 404: Not Found
func NewGenericCache ¶
func NewGenericCache[K comparable, V any](cfg Config) *GenericCache[K, V]
NewGenericCache creates a new type-safe generic cache.
Parameters:
- cfg: Cache configuration (MaxSize, TTL, WindowRatio, etc.)
Returns a new GenericCache instance.
Example ¶
ExampleNewGenericCache demonstrates type-safe generic cache usage.
package main
import (
"fmt"
"time"
"github.com/agilira/balios"
)
func main() {
// Create a type-safe cache for User structs
type User struct {
ID int
Name string
Email string
}
cache := balios.NewGenericCache[string, User](balios.Config{
MaxSize: 1000,
TTL: time.Hour,
})
defer func() { _ = cache.Close() }()
// Store a user (type-safe!)
cache.Set("user:123", User{
ID: 123,
Name: "John Doe",
Email: "john@example.com",
})
// Retrieve the user (returns User, not interface{})
if user, found := cache.Get("user:123"); found {
fmt.Printf("User: %s (%s)\n", user.Name, user.Email)
}
}
Output: User: John Doe (john@example.com)
func (*GenericCache[K, V]) Capacity ¶ added in v1.1.2
func (c *GenericCache[K, V]) Capacity() int
Capacity returns the maximum number of items the cache can hold.
func (*GenericCache[K, V]) Clear ¶
func (c *GenericCache[K, V]) Clear()
Clear removes all entries from the cache and resets statistics.
func (*GenericCache[K, V]) Close ¶
func (c *GenericCache[K, V]) Close() error
Close cleans up cache resources and stops background goroutines. After calling Close, the cache should not be used. Returns any error from closing the underlying cache.
func (*GenericCache[K, V]) Delete ¶
func (c *GenericCache[K, V]) Delete(key K)
Delete removes a key from the cache.
Parameters:
- key: The key to remove
func (*GenericCache[K, V]) Get ¶
func (c *GenericCache[K, V]) Get(key K) (value V, found bool)
Get retrieves a value from the cache.
Parameters:
- key: The key to retrieve
Returns:
- value: The stored value (zero value if not found)
- found: true if key exists and is not expired
Example ¶
ExampleGenericCache_Get demonstrates retrieving values from a generic cache.
package main
import (
"fmt"
"github.com/agilira/balios"
)
func main() {
cache := balios.NewGenericCache[int, string](balios.Config{
MaxSize: 100,
})
defer func() { _ = cache.Close() }()
// Store a value with integer key
cache.Set(404, "Not Found")
cache.Set(200, "OK")
// Retrieve values (type-safe)
if message, found := cache.Get(404); found {
fmt.Printf("Status 404: %s\n", message)
}
}
Output: Status 404: Not Found
func (*GenericCache[K, V]) GetOrLoad ¶
func (c *GenericCache[K, V]) GetOrLoad(key K, loader func() (V, error)) (V, error)
GetOrLoad is the generic version of Cache.GetOrLoad. Returns the value from cache, or loads it using the provided loader function.
Type Parameters:
- K: Key type (must be comparable)
- V: Value type (any type)
Parameters:
- key: The cache key to lookup or load
- loader: Function to load the value if not in cache
Returns:
- value: The cached or loaded value (zero value on error)
- error: Loader error or validation error
Example:
cache := NewGenericCache[int, string](Config{MaxSize: 100})
value, err := cache.GetOrLoad(42, func() (string, error) {
return fetchFromDB(42)
})
func (*GenericCache[K, V]) GetOrLoadWithContext ¶
func (c *GenericCache[K, V]) GetOrLoadWithContext(ctx context.Context, key K, loader func(context.Context) (V, error)) (V, error)
GetOrLoadWithContext is the generic version of Cache.GetOrLoadWithContext. Like GetOrLoad but respects context cancellation and timeout.
Type Parameters:
- K: Key type (must be comparable)
- V: Value type (any type)
Parameters:
- ctx: Context for cancellation and timeout control
- key: The cache key to lookup or load
- loader: Function to load the value if not in cache. Receives the context.
Returns:
- value: The cached or loaded value (zero value on error)
- error: Context error, loader error, or validation error
Example:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
value, err := cache.GetOrLoadWithContext(ctx, 42, func(ctx context.Context) (string, error) {
return fetchFromDBWithContext(ctx, 42)
})
func (*GenericCache[K, V]) Has ¶
func (c *GenericCache[K, V]) Has(key K) bool
Has checks if a key exists in the cache without retrieving it. This is more efficient than Get when you only need to check existence.
Parameters:
- key: The key to check
Returns true if key exists and is not expired.
func (*GenericCache[K, V]) Len ¶ added in v1.1.2
func (c *GenericCache[K, V]) Len() int
Len returns the current number of items in the cache. This is equivalent to Stats().Size but more efficient.
func (*GenericCache[K, V]) Set ¶
func (c *GenericCache[K, V]) Set(key K, value V)
Set stores a key-value pair in the cache. The value will be stored until evicted or expired (if TTL is set).
Parameters:
- key: The key to store (must be comparable)
- value: The value to store (can be any type)
Example ¶
ExampleGenericCache_Set demonstrates storing values in a generic cache.
package main
import (
"fmt"
"github.com/agilira/balios"
)
func main() {
cache := balios.NewGenericCache[string, int](balios.Config{
MaxSize: 100,
})
defer func() { _ = cache.Close() }()
// Store multiple values
cache.Set("answer", 42)
cache.Set("count", 1337)
cache.Set("total", 9001)
// Check if values exist
if cache.Has("answer") {
fmt.Println("Answer exists in cache")
}
}
Output: Answer exists in cache
func (*GenericCache[K, V]) Stats ¶
func (c *GenericCache[K, V]) Stats() CacheStats
Stats returns current cache statistics.
Returns CacheStats containing:
- Hits: Number of successful Get operations
- Misses: Number of failed Get operations
- ItemCount: Current number of items in cache
- Evictions: Number of items evicted
type Logger ¶
type Logger interface {
// Debug logs a debug message with optional key-value pairs.
Debug(msg string, keyvals ...interface{})
// Info logs an info message with optional key-value pairs.
Info(msg string, keyvals ...interface{})
// Warn logs a warning message with optional key-value pairs.
Warn(msg string, keyvals ...interface{})
// Error logs an error message with optional key-value pairs.
Error(msg string, keyvals ...interface{})
}
Logger defines a minimal logging interface with zero overhead. Implementations should use structured logging and be allocation-free.
type MetricsCollector ¶
type MetricsCollector interface {
// RecordGet records a Get operation with its latency and hit/miss result.
// latencyNs is the duration of the Get operation in nanoseconds.
// hit indicates whether the key was found (true) or not (false).
RecordGet(latencyNs int64, hit bool)
// RecordSet records a Set operation with its latency.
// latencyNs is the duration of the Set operation in nanoseconds.
RecordSet(latencyNs int64)
// RecordDelete records a Delete operation with its latency.
// latencyNs is the duration of the Delete operation in nanoseconds.
RecordDelete(latencyNs int64)
// RecordEviction records a cache eviction event.
// Called when an entry is evicted due to cache being full.
RecordEviction()
// RecordExpiration records a cache expiration event.
// Called when an entry is expired due to TTL.
RecordExpiration()
}
MetricsCollector defines an interface for collecting cache operation metrics. Implementations can send metrics to Prometheus, DataDog, StatsD, or other monitoring systems. This interface is designed for zero overhead when nil - no metrics are collected.
Performance requirements:
- All methods must be lock-free or use minimal locking
- All methods must be allocation-free
- All methods must complete in < 100ns for production use
Thread-safety:
- All methods must be safe for concurrent use
- Multiple goroutines will call these methods simultaneously
type NoOpLogger ¶
type NoOpLogger struct{}
NoOpLogger is a logger that does nothing. Used as default to avoid nil checks.
func (NoOpLogger) Debug ¶
func (NoOpLogger) Debug(msg string, keyvals ...interface{})
Debug does nothing (no-op implementation).
func (NoOpLogger) Error ¶
func (NoOpLogger) Error(msg string, keyvals ...interface{})
Error does nothing (no-op implementation).
func (NoOpLogger) Info ¶
func (NoOpLogger) Info(msg string, keyvals ...interface{})
Info does nothing (no-op implementation).
func (NoOpLogger) Warn ¶
func (NoOpLogger) Warn(msg string, keyvals ...interface{})
Warn does nothing (no-op implementation).
type NoOpMetricsCollector ¶
type NoOpMetricsCollector struct{}
NoOpMetricsCollector is a metrics collector that does nothing. Used as default to avoid nil checks and ensure zero overhead. All methods are inlined by the compiler for maximum performance.
func (NoOpMetricsCollector) RecordDelete ¶
func (NoOpMetricsCollector) RecordDelete(latencyNs int64)
RecordDelete does nothing. Inlined by compiler.
func (NoOpMetricsCollector) RecordEviction ¶
func (NoOpMetricsCollector) RecordEviction()
RecordEviction does nothing. Inlined by compiler.
func (NoOpMetricsCollector) RecordExpiration ¶ added in v1.1.32
func (NoOpMetricsCollector) RecordExpiration()
RecordExpiration does nothing. Inlined by compiler.
func (NoOpMetricsCollector) RecordGet ¶
func (NoOpMetricsCollector) RecordGet(latencyNs int64, hit bool)
RecordGet does nothing. Inlined by compiler.
func (NoOpMetricsCollector) RecordSet ¶
func (NoOpMetricsCollector) RecordSet(latencyNs int64)
RecordSet does nothing. Inlined by compiler.
type TimeProvider ¶
type TimeProvider interface {
// Now returns the current time in nanoseconds since epoch.
// This method must be very fast and allocation-free.
Now() int64
}
TimeProvider provides current time with caching for performance. This interface allows injecting optimized time implementations.
