Files
cv-site/doc/23-DATA-CACHE.md
T
juanatsap 2c7f8de242 refactor: centralize constants and reorganize documentation
- Create internal/constants package with all hardcoded values
  (environment, cookies, themes, headers, routes, cache)
- Create internal/httputil package for HTTP helper functions
- Update all handlers and middleware to use centralized constants
- Reorganize documentation with numbered prefixes (00-26)
- Remove duplicate docs from validation folder and docs/
- Delete handlers/constants.go (moved to internal/constants)
2025-12-06 16:27:12 +00:00

6.8 KiB

Cache Package

Overview

The cache package provides application-level caching for CV and UI data, eliminating per-request file I/O by loading all data once at application startup. This improves performance and reduces latency for all handler operations.

Key Benefits:

  • Single load at startup, fast reads during request handling
  • Thread-safe concurrent access using sync.RWMutex
  • Language-keyed access ("en", "es")
  • Fast-fail strategy: fails at startup if any language data cannot be loaded

Architecture

DataCache Structure

type DataCache struct {
    cv map[string]*cvmodel.CV    // CV data indexed by language
    ui map[string]*uimodel.UI    // UI data indexed by language
    mu sync.RWMutex              // Protects concurrent reads
}

The cache stores pointer references to CV and UI models, loaded from YAML files. Since reads are frequent and writes never occur, sync.RWMutex provides efficient concurrent access.

Usage

Initialization

The cache is created once at application startup in main.go:

// Initialize data cache (load CV and UI data once at startup)
dataCache, err := cache.New([]string{"en", "es"})
if err != nil {
    log.Fatalf("Failed to initialize data cache: %v", err)
}

This loads CV and UI data for English and Spanish. If any language fails to load, the entire startup fails—catch errors early rather than on first request.

Handler Integration

The cache is injected into handlers via constructor:

cvHandler := handlers.NewCVHandler(templateMgr, serverAddr, emailService, dataCache)

Handlers access cached data using language-specific getters:

func (h *CVHandler) renderPage(w http.ResponseWriter, r *http.Request) {
    lang := r.URL.Query().Get("lang")
    cv := h.dataCache.GetCV(lang)
    ui := h.dataCache.GetUI(lang)

    // Use cv and ui data for rendering...
}

API Reference

New(languages []string) (*DataCache, error)

Creates and initializes a new cache with data for the specified languages.

Parameters:

  • languages: List of language codes to load (e.g., []string{"en", "es"})

Returns:

  • *DataCache: Initialized cache instance
  • error: Non-nil if any language fails to load

Behavior:

  • Returns nil and error if any language's CV or UI data fails to load
  • Empty language list creates empty cache (no error)
  • Fails at startup rather than deferring errors to request time

Example:

cache, err := cache.New([]string{"en", "es"})
if err != nil {
    log.Fatalf("Failed to initialize cache: %v", err)
}

GetCV(lang string) *cvmodel.CV

Retrieves cached CV data for the specified language.

Parameters:

  • lang: Language code (e.g., "en", "es")

Returns:

  • *cvmodel.CV: Pointer to CV data, or nil if language not found
  • Note: Callers must check for nil before dereferencing

Thread Safety: Safe for concurrent reads

Example:

cv := cache.GetCV("en")
if cv == nil {
    // Handle missing language
    return fmt.Errorf("CV not available for language: en")
}
// Use cv...

GetUI(lang string) *uimodel.UI

Retrieves cached UI data for the specified language.

Parameters:

  • lang: Language code (e.g., "en", "es")

Returns:

  • *uimodel.UI: Pointer to UI data, or nil if language not found

Thread Safety: Safe for concurrent reads

Example:

ui := cache.GetUI("es")
if ui != nil {
    title := ui.Navigation.Title
}

Languages() []string

Returns all language codes currently cached.

Returns:

  • []string: Slice of available language codes (order not guaranteed)

Thread Safety: Safe for concurrent reads

Example:

langs := cache.Languages()
for _, lang := range langs {
    cv := cache.GetCV(lang)
    // Process CV for each language...
}

Mutating Cached Data

Important: Deep Copies for Mutable Fields

Since cache stores pointer references, handlers that modify CV slices must create deep copies before modification:

// In handlers that modify experience/projects:
func prepareTemplateData(cv *cvmodel.CV) *cvmodel.CV {
    // Create copies of mutable slices
    copy := &cvmodel.CV{
        Personal:      cv.Personal,
        Experience:    append([]cvmodel.Experience{}, cv.Experience...), // Deep copy
        Projects:      append([]cvmodel.Project{}, cv.Projects...),      // Deep copy
        Education:     cv.Education,
        Skills:        cv.Skills,
    }

    // Now safe to modify copy.Experience and copy.Projects
    for i := range copy.Experience {
        copy.Experience[i].YearsOfExperience = calculateYears()
    }

    return copy
}

This prevents handlers from accidentally mutating cached data during request processing.

Supported Languages

Currently configured for:

  • "en" - English
  • "es" - Spanish

To add a new language, update main.go:

dataCache, err := cache.New([]string{"en", "es", "fr"})  // Add "fr"

Ensure YAML data files exist in the data directory for the new language, or startup will fail.

Error Handling

Startup Failures

The fast-fail strategy ensures all data issues are caught before the server starts:

dataCache, err := cache.New([]string{"en", "es"})
if err != nil {
    // Example error messages:
    // "load CV for 'fr': file not found"
    // "load UI for 'es': invalid YAML"
    log.Fatalf("Failed to initialize data cache: %v", err)
}

Runtime Handling

Handlers should gracefully handle missing languages:

cv := cache.GetCV(lang)
if cv == nil {
    http.Error(w, "Language not supported", http.StatusNotFound)
    return
}

Performance Considerations

I/O Efficiency

  • Single Load: CV and UI YAML files are parsed once at startup
  • No Per-Request I/O: Handler requests never touch disk
  • Memory Trade-off: Stores decoded objects in memory

Concurrency

  • RWMutex: Optimized for high read throughput, zero writes
  • No Contention: 100+ concurrent reads verified in tests
  • Nil Returns: Fast path for missing languages (map lookup only)

Memory Usage

  • Minimal overhead: Two maps + one mutex
  • Proportional to number of languages loaded
  • Shared object references (no duplication per request)

Testing

Run the comprehensive test suite:

go test ./internal/cache -v

Test coverage includes:

  • Cache initialization with valid/invalid languages
  • CV and UI data retrieval
  • Thread safety with concurrent reads
  • Data integrity verification
  • Empty language list handling

Dependencies

  • internal/models/cv - CV data model
  • internal/models/ui - UI data model
  • Go standard library: sync
  • internal/cache/data_cache.go - Cache implementation
  • internal/cache/data_cache_test.go - Comprehensive test suite
  • main.go - Cache initialization at startup
  • internal/handlers/cv.go - Handler injection point