generic

package
v0.14.1 Latest Latest
Warning

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

Go to latest
Published: Nov 18, 2025 License: MIT Imports: 3 Imported by: 0

README

Generic Cache Framework

A type-safe, high-performance caching layer for Dumber Browser that provides RAM-first access with asynchronous database persistence.

Overview

The generic cache framework provides a consistent caching pattern across all Dumber Browser caches:

  • RAM-First: All reads from memory (no DB queries)
  • Async Writes: Updates happen immediately in cache, persisted asynchronously
  • Bulk Load: Load all data from DB at startup
  • Graceful Shutdown: Flush all pending writes on shutdown

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Application Code                        │
│                    (BrowserService, etc.)                    │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                  Cache[K, V] Interface                       │
│                                                               │
│  Load(ctx)        - Bulk load at startup                     │
│  Get(key)         - RAM lookup only (never DB)               │
│  Set(key, value)  - Immediate cache + async DB               │
│  Delete(key)      - Immediate cache + async DB               │
│  List()           - All cached values                        │
│  Flush()          - Wait for pending writes                  │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│              GenericCache[K, V] Implementation               │
│                                                               │
│  ┌──────────────┐     ┌──────────────────────────────────┐ │
│  │  sync.Map    │     │   DatabaseOperations[K, V]       │ │
│  │  (RAM cache) │     │                                  │ │
│  │              │     │  - LoadAll(ctx) → map[K]V       │ │
│  │              │     │  - Persist(ctx, K, V) → error   │ │
│  │              │     │  - Delete(ctx, K) → error       │ │
│  └──────────────┘     └──────────────────────────────────┘ │
│                                                               │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  pendingWrites sync.WaitGroup                          │ │
│  │  (tracks async operations for graceful shutdown)       │ │
│  └────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                  Database (SQLite)                           │
│                                                               │
│  zoom_levels, shortcuts, certificate_validations, etc.       │
└─────────────────────────────────────────────────────────────┘

Usage Example

1. Implement DatabaseOperations

First, create a struct that implements the DatabaseOperations[K, V] interface:

package cache

import (
    "context"
    "github.com/bnema/dumber/internal/cache/generic"
    "github.com/bnema/dumber/internal/db"
)

// ZoomDBOperations implements DatabaseOperations for zoom levels
type ZoomDBOperations struct {
    queries db.DatabaseQuerier
}

func NewZoomDBOperations(queries db.DatabaseQuerier) *ZoomDBOperations {
    return &ZoomDBOperations{queries: queries}
}

func (z *ZoomDBOperations) LoadAll(ctx context.Context) (map[string]float64, error) {
    levels, err := z.queries.ListZoomLevels(ctx)
    if err != nil {
        return nil, err
    }

    result := make(map[string]float64, len(levels))
    for _, level := range levels {
        result[level.Domain] = level.ZoomFactor
    }
    return result, nil
}

func (z *ZoomDBOperations) Persist(ctx context.Context, key string, value float64) error {
    return z.queries.SetZoomLevel(ctx, key, value)
}

func (z *ZoomDBOperations) Delete(ctx context.Context, key string) error {
    return z.queries.DeleteZoomLevel(ctx, key)
}
2. Create a Cache Instance
package cache

import "github.com/bnema/dumber/internal/cache/generic"

type ZoomCache struct {
    *generic.GenericCache[string, float64]
}

func NewZoomCache(queries db.DatabaseQuerier) *ZoomCache {
    dbOps := NewZoomDBOperations(queries)
    return &ZoomCache{
        GenericCache: generic.NewGenericCache(dbOps),
    }
}
3. Use in Application Code
// At startup
zoomCache := cache.NewZoomCache(dbQueries)
if err := zoomCache.Load(ctx); err != nil {
    log.Fatalf("Failed to load zoom cache: %v", err)
}

// Get (always from RAM, never hits DB)
if zoomLevel, ok := zoomCache.Get("example.com"); ok {
    fmt.Printf("Zoom level: %.2f\n", zoomLevel)
}

// Set (immediate in cache, async to DB)
if err := zoomCache.Set("example.com", 1.5); err != nil {
    log.Printf("Failed to set zoom level: %v", err)
}

// On shutdown
if err := zoomCache.Flush(); err != nil {
    log.Printf("Failed to flush cache: %v", err)
}

Performance Characteristics

Operation Latency Notes
Load() ~2-50ms One-time at startup, bulk loads all data
Get() <1µs sync.Map lookup, never touches DB
Set() <1µs Returns immediately, DB write async
Delete() <1µs Returns immediately, DB delete async
List() ~10-100µs Iterates sync.Map, no DB access
Flush() Varies Blocks until all async writes complete

Thread Safety

All operations are thread-safe:

  • sync.Map provides lock-free concurrent reads and writes
  • sync.WaitGroup coordinates graceful shutdown
  • Database operations are serialized per key (last write wins)

Testing

The framework includes comprehensive unit tests with a mock implementation:

import "github.com/bnema/dumber/internal/cache/generic"

func TestMyCache(t *testing.T) {
    mock := generic.NewMockDatabaseOperations[string, int]()

    // Configure mock behavior
    mock.LoadAllFunc = func(ctx context.Context) (map[string]int, error) {
        return map[string]int{"key": 42}, nil
    }

    cache := generic.NewGenericCache(mock)

    // Test cache operations
    if err := cache.Load(context.Background()); err != nil {
        t.Fatal(err)
    }

    val, ok := cache.Get("key")
    if !ok || val != 42 {
        t.Errorf("Expected key=42, got %v, %v", val, ok)
    }

    // Verify mock was called
    if count := mock.GetLoadAllCallCount(); count != 1 {
        t.Errorf("Expected LoadAll called once, got %d", count)
    }
}

Design Decisions

Why Generic?

Type safety and code reuse. The same cache implementation works for:

  • Cache[string, float64] (zoom levels)
  • Cache[string, Shortcut] (search shortcuts)
  • Cache[string, CertValidation] (TLS certificates)
Why Async Persistence?
  • Startup Performance: <500ms startup time requirement
  • UI Responsiveness: Never block user actions on DB writes
  • Write Coalescing: Multiple rapid updates to same key only persist latest value
Why Bulk Load?
  • Small datasets (10-100 entries typical)
  • Faster than incremental loading with query overhead
  • Simpler code: no cache miss logic needed
Why sync.Map Instead of map + RWMutex?
  • Lock-free reads in common case
  • Better performance under concurrent load
  • Standard library, battle-tested

Limitations

  • Not for large datasets: Loads entire dataset into RAM
  • No TTL/expiration: All entries cached indefinitely
  • No size limits: Assumes datasets fit comfortably in memory
  • Last write wins: No conflict resolution for concurrent updates

For Dumber Browser's use case (small configuration tables), these tradeoffs are acceptable.

Future Enhancements

Potential improvements if needed:

  • Add metrics (cache hits, miss rate, persist latency)
  • Add TTL support for time-sensitive data
  • Add size-based eviction for large caches
  • Add batch persistence for write-heavy workloads
  • Add versioning/CAS for concurrent update detection

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cache

type Cache[K comparable, V any] interface {
	// Load bulk-loads all data from storage into memory at startup
	Load(ctx context.Context) error

	// Get retrieves a value from the cache (RAM only, never queries DB)
	Get(key K) (V, bool)

	// Set updates the cache immediately and persists to DB asynchronously
	Set(key K, value V) error

	// Delete removes from cache immediately and persists deletion asynchronously
	Delete(key K) error

	// List returns all cached values as a slice
	List() []V

	// Flush waits for all pending async writes to complete (call on shutdown)
	Flush() error
}

Cache provides a generic interface for caching any type of data. K is the key type (must be comparable), V is the value type. All operations are thread-safe via sync.Map.

Design principles: - RAM-first: All reads from memory (no DB queries) - Async writes: Updates happen immediately in cache, persisted asynchronously - Bulk load: Load all data from DB at startup - Flush on shutdown: Ensure all pending writes complete gracefully

type DatabaseOperations

type DatabaseOperations[K comparable, V any] interface {
	// LoadAll loads all entries from the database
	LoadAll(ctx context.Context) (map[K]V, error)

	// Persist saves a single entry to the database
	Persist(ctx context.Context, key K, value V) error

	// Delete removes a single entry from the database
	Delete(ctx context.Context, key K) error
}

DatabaseOperations defines the interface for database operations required by the cache. This interface allows for easy mocking in tests using gomock.

type GenericCache

type GenericCache[K comparable, V any] struct {
	// contains filtered or unexported fields
}

GenericCache implements Cache[K, V] using sync.Map for thread-safe storage and a DatabaseOperations interface for database operations.

func NewGenericCache

func NewGenericCache[K comparable, V any](
	dbOps DatabaseOperations[K, V],
) *GenericCache[K, V]

NewGenericCache creates a new cache with the provided database operations.

Parameters:

  • dbOps: Implementation of DatabaseOperations for loading, persisting, and deleting data

func (*GenericCache[K, V]) Delete

func (c *GenericCache[K, V]) Delete(key K) error

Delete removes from cache immediately (synchronous) and persists deletion to the database asynchronously (non-blocking).

func (*GenericCache[K, V]) Flush

func (c *GenericCache[K, V]) Flush() error

Flush blocks until all pending async writes complete. Should be called during graceful shutdown to ensure no data loss.

func (*GenericCache[K, V]) Get

func (c *GenericCache[K, V]) Get(key K) (V, bool)

Get retrieves a value from the cache. Returns (value, true) if found, or (zero value, false) if not found. Never queries the database.

func (*GenericCache[K, V]) List

func (c *GenericCache[K, V]) List() []V

List returns all values currently in the cache as a slice. The order is not guaranteed.

func (*GenericCache[K, V]) Load

func (c *GenericCache[K, V]) Load(ctx context.Context) error

Load bulk-loads all data from the database into memory. Should be called once at application startup.

func (*GenericCache[K, V]) Set

func (c *GenericCache[K, V]) Set(key K, value V) error

Set updates the cache immediately (synchronous) and persists to the database asynchronously (non-blocking). Returns immediately without waiting for DB write.

type MockDatabaseOperations

type MockDatabaseOperations[K comparable, V any] struct {

	// Behavior configuration
	LoadAllFunc func(ctx context.Context) (map[K]V, error)
	PersistFunc func(ctx context.Context, key K, value V) error
	DeleteFunc  func(ctx context.Context, key K) error

	// Call tracking
	LoadAllCalls []context.Context
	PersistCalls []struct {
		Ctx   context.Context
		Key   K
		Value V
	}
	DeleteCalls []struct {
		Ctx context.Context
		Key K
	}
	// contains filtered or unexported fields
}

MockDatabaseOperations is a mock implementation of DatabaseOperations for testing. It's generic and thread-safe, suitable for testing GenericCache.

func NewMockDatabaseOperations

func NewMockDatabaseOperations[K comparable, V any]() *MockDatabaseOperations[K, V]

NewMockDatabaseOperations creates a new mock with default no-op implementations.

func (*MockDatabaseOperations[K, V]) Delete

func (m *MockDatabaseOperations[K, V]) Delete(ctx context.Context, key K) error

Delete implements DatabaseOperations.Delete

func (*MockDatabaseOperations[K, V]) GetDeleteCallCount

func (m *MockDatabaseOperations[K, V]) GetDeleteCallCount() int

GetDeleteCallCount returns the number of times Delete was called

func (*MockDatabaseOperations[K, V]) GetLastDeleteCall

func (m *MockDatabaseOperations[K, V]) GetLastDeleteCall() *struct {
	Ctx context.Context
	Key K
}

GetLastDeleteCall returns the last call to Delete, or nil if none

func (*MockDatabaseOperations[K, V]) GetLastPersistCall

func (m *MockDatabaseOperations[K, V]) GetLastPersistCall() *struct {
	Ctx   context.Context
	Key   K
	Value V
}

GetLastPersistCall returns the last call to Persist, or nil if none

func (*MockDatabaseOperations[K, V]) GetLoadAllCallCount

func (m *MockDatabaseOperations[K, V]) GetLoadAllCallCount() int

GetLoadAllCallCount returns the number of times LoadAll was called

func (*MockDatabaseOperations[K, V]) GetPersistCallCount

func (m *MockDatabaseOperations[K, V]) GetPersistCallCount() int

GetPersistCallCount returns the number of times Persist was called

func (*MockDatabaseOperations[K, V]) LoadAll

func (m *MockDatabaseOperations[K, V]) LoadAll(ctx context.Context) (map[K]V, error)

LoadAll implements DatabaseOperations.LoadAll

func (*MockDatabaseOperations[K, V]) Persist

func (m *MockDatabaseOperations[K, V]) Persist(ctx context.Context, key K, value V) error

Persist implements DatabaseOperations.Persist

func (*MockDatabaseOperations[K, V]) Reset

func (m *MockDatabaseOperations[K, V]) Reset()

Reset clears all call tracking

Jump to

Keyboard shortcuts

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