balios

package module
v1.1.36 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2025 License: MPL-2.0 Imports: 12 Imported by: 0

README

Balios: High-Performance Caching Library for Go

Balios Banner

Balios is a high-performance in-memory caching library for Go, based on W-TinyLFU, engineered for maximum throughput, optimal hit ratio, professional security & advanced observability—without sacrificing developer experience.

CI/CD Pipeline CodeQL Security Go Report Card Test Coverage OpenSSF Best Practices

FeaturesQuick StartPerformanceObservability ArchitecturePhilosophyDocumentation

Features

  • Type-Safe Generics API: GenericCache[K comparable, V any] with compile-time type safety
  • Automatic Loading: GetOrLoad() API with singleflight pattern for cache stampede prevention
  • W-TinyLFU Algorithm: Combines frequency and recency for optimal eviction decisions
  • Lock-Free: Uses atomic primitives for high concurrency
  • TTL Support: Hybrid expiration strategy with inline opportunistic cleanup + manual ExpireNow() API (v1.1.32+)
  • Context Support: Timeout and cancellation for loader functions
  • Negative Caching: Cache loader errors to prevent repeated failed operations (v1.1.2+)
  • Structured Errors: Rich error context with go-errors - see examples/errors/
  • Observability: OpenTelemetry integration for metrics (p50/p95/p99 latencies, hit ratio) & logger interface. Zero overhead when disabled (compiler eliminates no-op implementations) - see examples/otel-prometheus/
  • Secure by Design: Red-team tested and fuzz tested angainst a wide range of attacks

Compatibility and Support

Balios is designed for Go 1.24+ environments and follows Long-Term Support guidelines to ensure consistent performance across production deployments.

Installation

go get github.com/agilira/balios

Quick Start

Type-Safe Generic API
package main

import (
    "fmt"
    "time"
    
    "github.com/agilira/balios"
)

type User struct {
    ID   int
    Name string
    Role string
}

func main() {
    // Create type-safe cache
    cache := balios.NewGenericCache[string, User](balios.Config{
        MaxSize: 10_000,
        TTL:     time.Hour,
    })
    
    // Set a value
    cache.Set("user:123", User{
        ID:   123,
        Name: "John Doe",
        Role: "admin",
    })
    
    // Get a value (no type assertion needed)
    if user, found := cache.Get("user:123"); found {
        fmt.Printf("User: %s (%s)\n", user.Name, user.Role)
    }
    
    // Check stats
    stats := cache.Stats()
    fmt.Printf("Hit ratio: %.2f%%\n", stats.HitRatio()*100)
}

Performance

Single-Threaded Performance:

Package Set (ns/op) Set % vs Balios Get (ns/op) Get % vs Balios Allocations
Balios 194.4 ns/op +0% 110.8 ns/op +0% 2/0 allocs/op
Balios-Generic 198.2 ns/op +2% 115.0 ns/op +4% 2/0 allocs/op
Otter 365.3 ns/op +88% 121.2 ns/op +9% 1/0 allocs/op
Ristretto 280.0 ns/op +44% 156.7 ns/op +41% 2/0 allocs/op

Parallel Performance (8 cores):

Package Set (ns/op) Set % vs Balios Get (ns/op) Get % vs Balios Allocations
Balios 59.26 ns/op +0% 25.50 ns/op +0% 2/0 allocs/op
Balios-Generic 60.84 ns/op +3% 27.33 ns/op +7% 2/0 allocs/op
Otter 240.1 ns/op +305% 25.78 ns/op +1% 1/0 allocs/op
Ristretto 115.1 ns/op +94% 30.04 ns/op +18% 1/0 allocs/op

Mixed Workloads (Realistic Scenarios):

Workload Balios Balios-Generic Otter Ristretto Best
Write-Heavy (10% R / 90% W) 85.82 ns/op 100.7 ns/op 210.3 ns/op 125.4 ns/op Balios
Balanced (50% R / 50% W) 50.01 ns/op 52.81 ns/op 132.5 ns/op 113.2 ns/op Balios
Read-Heavy (90% R / 10% W) 35.08 ns/op 36.84 ns/op 47.64 ns/op 70.81 ns/op Balios
Read-Only (100% R) 36.59 ns/op 55.91 ns/op 46.95 ns/op 31.63 ns/op Ristretto

Hit Ratio (100K requests, Zipf distribution):

Cache Hit Ratio Notes
Balios 79.86% Excellent
Balios-Generic 79.71% Excellent
Otter 79.53% Excellent
Ristretto 71.19% Good

Test Environment: AMD Ryzen 5 7520U, Go 1.25+

Run the benchmarks on your hardware benchmarks/ to evaluate performance on your specific workload and configuration. See docs/PERFORMANCE.md for detailed analysis and methodology.

Advanced Configuration

Negative Caching (v1.1.2+): Cache loader errors to prevent repeated failed operations

cache := balios.NewGenericCache[int, User](balios.Config{
    MaxSize:          10_000,
    TTL:              5 * time.Minute,
    NegativeCacheTTL: 30 * time.Second, // Cache errors for 30s
})

// First call: loader fails
_, err := cache.GetOrLoad(123, func() (User, error) {
    return User{}, fmt.Errorf("database unavailable")
})
// Error returned

// Subsequent calls within 30s: cached error returned WITHOUT calling loader
_, err = cache.GetOrLoad(123, func() (User, error) {
    panic("This won't be called - error is cached!")
})
// Same error returned (no loader execution)

Use cases: Circuit breaker pattern, API rate limiting, external service failures.
See: GetOrLoad documentation for complete guide.

Automatic Loading with GetOrLoad

Prevent cache stampede with singleflight pattern:

// Multiple concurrent requests for same key = single loader execution
user, err := cache.GetOrLoad("user:123", func() (User, error) {
    // This expensive operation runs only once
    return fetchUserFromDB(123)
})
if err != nil {
    log.Printf("Failed to load user: %v", err)
    return
}

With context support for timeout/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:

  • Cache hit: Same performance as Get() operations (27.90 ns/op parallel, 0 allocations)
  • Concurrent requests: 1000 simultaneous requests = 1 loader call (singleflight)
  • Error handling: Loader errors can be cached with NegativeCacheTTL option
  • Panic recovery: Returns BALIOS_PANIC_RECOVERED error if loader panics

See examples/getorload/ for comprehensive examples.

Observability Architecture

graph TB
    subgraph "Core Cache Module"
        CACHE[Balios Cache<br/>Get/Set/Delete Operations]
        IFACE[MetricsCollector Interface<br/>Thread-Safe API]
        NOOP[NoOpMetricsCollector<br/>Zero Overhead Default<br/>Compiler Inlined]
    end

    subgraph "Implementation Layer"
        OTEL[OpenTelemetry Collector<br/>otel package<br/>Professional Metrics]
        CUSTOM[Custom Collector<br/>User Implementation<br/>Domain-Specific]
    end

    subgraph "Observability Backends"
        PROM[Prometheus<br/>Metrics Storage<br/>Time-Series DB]
        JAEGER[Jaeger<br/>Distributed Tracing<br/>Performance Analysis]
        GRAFANA[Grafana<br/>Visualization<br/>Dashboards]
    end

    subgraph "Custom Integrations"
        DD[DataDog<br/>APM Platform]
        CUSTOM_BACK[Custom Backend<br/>Internal Tools<br/>Domain-Specific]
    end

    %% Connections
    CACHE --> IFACE
    IFACE --> NOOP
    IFACE --> OTEL
    IFACE --> CUSTOM

    OTEL --> PROM
    OTEL --> JAEGER
    PROM --> GRAFANA

    CUSTOM --> DD
    CUSTOM --> CUSTOM_BACK

    %% Styling with Argus color scheme
    classDef core fill:#ecfdf5,stroke:#059669,stroke-width:2px
    classDef implementation fill:#f0f9ff,stroke:#0369a1,stroke-width:2px
    classDef observability fill:#fef3c7,stroke:#d97706,stroke-width:2px
    classDef custom fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px

    class CACHE,IFACE,NOOP core
    class OTEL,CUSTOM implementation
    class PROM,JAEGER,GRAFANA observability
    class DD,CUSTOM_BACK custom
Logger Interface
// Logger defines a minimal logging interface with zero overhead.
// Implementations should use structured logging and be allocation-free.
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{})
}

Integration example:

import "log/slog"

// Using standard library slog
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

cache := balios.New[string, User](balios.Config{
    Size: 1000,
    Logger: logger,
})

see Metrics & Observability for full documentation.

The Philosophy Behind Balios

Balios and his brother Xanthos were the immortal horses of Achilles, born from Zephyros, the swiftest of the Anemoi. They were not merely fast—they were the children of the wind itself, incomparable to mortal steeds. Balios possessed intelligence beyond any horse, an instinct that guided Achilles through every battle with perfect judgment.

When Patroclus fell, it was Xanthos who spoke—granted voice by Hera herself—to warn Achilles of his fate. But Balios remained silent, his wisdom expressed not in words but in action, in knowing when to charge and when to wheel away, in the perfect synchrony between horse and hero that transcends command.

Documentation

Future Enhancements (PLANNED)

  • Async refresh (stale-while-revalidate pattern)
  • Persistence (save/load from disk)
  • Distributed cache coordination
  • Write-through/write-behind patterns

License

Balios is licensed under the Mozilla Public License 2.0.


Balios • an AGILira fragment

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

Examples

Constants

View Source
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
)
View Source
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

func GetErrorCode(err error) errors.ErrorCode

GetErrorCode extracts the error code from an error

func GetErrorContext

func GetErrorContext(err error) map[string]interface{}

GetErrorContext extracts context from an error

func IsCacheFull

func IsCacheFull(err error) bool

IsCacheFull checks if error is a cache full error

func IsConfigError

func IsConfigError(err error) bool

IsConfigError checks if error is a configuration error

func IsEmptyKey added in v1.1.2

func IsEmptyKey(err error) bool

IsEmptyKey checks if error is an empty key error

func IsLoaderError

func IsLoaderError(err error) bool

IsLoaderError checks if error is a loader error

func IsNotFound

func IsNotFound(err error) bool

IsNotFound checks if error is a key not found error

func IsOperationError

func IsOperationError(err error) bool

IsOperationError checks if error is an operation error

func IsPersistenceError

func IsPersistenceError(err error) bool

IsPersistenceError checks if error is a persistence error

func IsRetryable

func IsRetryable(err error) bool

IsRetryable checks if the error can be retried

func NewErrCacheFull

func NewErrCacheFull(capacity int, size int) error

NewErrCacheFull creates an error when cache is full and eviction fails

func NewErrCorruptedData

func NewErrCorruptedData(filepath string, details string) error

NewErrCorruptedData creates an error when data is corrupted

func NewErrDeleteFailed

func NewErrDeleteFailed(key string, reason string) error

NewErrDeleteFailed creates an error when Delete operation fails

func NewErrEmptyKey added in v1.1.2

func NewErrEmptyKey(operation string) error

NewErrEmptyKey creates an error when key is empty

func NewErrEvictionFailed

func NewErrEvictionFailed(reason string) error

NewErrEvictionFailed creates an error when eviction fails

func NewErrInternal

func NewErrInternal(operation string, cause error) error

NewErrInternal creates a generic internal error

func NewErrInvalidCounterBits

func NewErrInvalidCounterBits(bits int) error

NewErrInvalidCounterBits creates an error for invalid counter bits

func NewErrInvalidLoader

func NewErrInvalidLoader(key string) error

NewErrInvalidLoader creates an error when loader function is nil

func NewErrInvalidMaxSize

func NewErrInvalidMaxSize(size int) error

NewErrInvalidMaxSize creates an error for invalid max size

func NewErrInvalidTTL

func NewErrInvalidTTL(ttl interface{}) error

NewErrInvalidTTL creates an error for invalid TTL

func NewErrInvalidWindowRatio

func NewErrInvalidWindowRatio(ratio float64) error

NewErrInvalidWindowRatio creates an error for invalid window ratio

func NewErrKeyNotFound

func NewErrKeyNotFound(key string) error

NewErrKeyNotFound creates an error when key is not found

func NewErrLoadFailed

func NewErrLoadFailed(filepath string, cause error) error

NewErrLoadFailed creates an error when load operation fails

func NewErrLoaderCancelled

func NewErrLoaderCancelled(key string) error

NewErrLoaderCancelled creates an error when loader is cancelled

func NewErrLoaderFailed

func NewErrLoaderFailed(key string, cause error) error

NewErrLoaderFailed creates an error when loader function fails

func NewErrLoaderTimeout

func NewErrLoaderTimeout(key string, timeout interface{}) error

NewErrLoaderTimeout creates an error when loader times out

func NewErrPanicRecovered

func NewErrPanicRecovered(operation string, panicValue interface{}) error

NewErrPanicRecovered creates an error when a panic is recovered

func NewErrSaveFailed

func NewErrSaveFailed(filepath string, cause error) error

NewErrSaveFailed creates an error when save operation fails

func NewErrSetFailed

func NewErrSetFailed(key string, reason string) error

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

func NewCache(config Config) Cache

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
}

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a configuration with sensible defaults.

func (*Config) Validate

func (c *Config) Validate() error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL