Documentation
¶
Overview ¶
Package cache provides BoltDB-backed caching with TTL expiration for cost query results.
This package implements persistent caching using bbolt (go.etcd.io/bbolt) to improve CLI performance by avoiding redundant plugin calls for recently-fetched data.
Storage ¶
The cache is stored in a single BoltDB file (cache.db) within the project or global cache directory. BoltDB provides atomic transactions, indexed lookups, and reduced disk I/O compared to individual JSON files.
Bucket Layout ¶
Data is organized into three top-level buckets:
- projected: Per-resource projected cost results
- actual: Whole-query actual cost results
- recommendations: Recommendation query results
Key Format ¶
Keys use human-readable slash-separated paths for easy debugging and prefix scanning:
- projected/{provider}/{type}/{region}/{sku}
- actual/{provider}/{types}/{from}/{to}/{filter-hash}
- recommendations/multi/{sorted-types}
Concurrency ¶
BoltDB supports concurrent reads via read-only transactions. Writes are serialized through DB.Batch() for automatic coalescing of concurrent write operations. The database file is protected by an OS-level file lock (flock/LockFileEx).
TTL Expiration ¶
Entries are checked for expiration on read (lazy expiration). A startup cleanup pass removes expired entries in bulk. TTL is configurable via CLI flag (--cache-ttl), environment variable (FINFOCUS_CACHE_TTL), or config file.
Index ¶
- Constants
- Variables
- func BucketFromKey(key string) string
- func BuildActualKey(provider string, resourceTypes []string, from, to time.Time, ...) string
- func BuildProjectedKey(provider, resourceType, region, sku string) string
- func BuildRecommendationsKey(resourceTypes []string) string
- func CalculatePluginTTL(expiresAt *time.Time, defaultTTL int) (int, bool, bool)
- func FormatDuration(d time.Duration) string
- func GetCacheDirFromEnv() string
- func GetCacheEnabledFromEnv() bool
- func GetCacheMaxSizeFromEnv() int
- func GetTTLFromEnv() int
- func ParseTTL(s string) (int, error)
- func StripBucket(key string) string
- type BoltStore
- func (s *BoltStore) CleanupExpired() (int, error)
- func (s *BoltStore) Clear() error
- func (s *BoltStore) Close() error
- func (s *BoltStore) Count() (int, error)
- func (s *BoltStore) Delete(key string) error
- func (s *BoltStore) Get(key string) (*CacheEntry, error)
- func (s *BoltStore) GetDirectory() string
- func (s *BoltStore) GetTTL() int
- func (s *BoltStore) InvalidateByPrefix(prefix string) (int, error)
- func (s *BoltStore) IsEnabled() bool
- func (s *BoltStore) Set(key string, data json.RawMessage) error
- func (s *BoltStore) SetWithTTL(key string, data json.RawMessage, ttlSeconds int) error
- func (s *BoltStore) Size() (int64, error)
- type Cache
- type CacheEntry
- func (e *CacheEntry) Age() time.Duration
- func (e *CacheEntry) IsExpired() bool
- func (e *CacheEntry) IsValid() bool
- func (e *CacheEntry) MarshalJSON() ([]byte, error)
- func (e *CacheEntry) TimeUntilExpiration() time.Duration
- func (e *CacheEntry) Touch()
- func (e *CacheEntry) UnmarshalJSON(data []byte) error
- type TTLConfig
Constants ¶
const ( BucketProjected = "projected" BucketActual = "actual" BucketRecommendations = "recommendations" )
Bucket names for the BoltDB cache.
const ( DefaultTTLSeconds = config.CacheDefaultTTLSeconds DefaultCacheMaxSizeMB = config.CacheDefaultMaxSizeMB EnvTTLSeconds = config.CacheEnvTTLSeconds EnvTTLSecondsLegacy = config.CacheEnvTTLSecondsLegacy EnvCacheEnabled = config.CacheEnvEnabled EnvCacheDir = config.CacheEnvDir EnvCacheMaxSize = config.CacheEnvMaxSize )
Re-exported constants from config for backward compatibility. The canonical definitions live in internal/config/cache_defaults.go to avoid a dependency inversion (config → engine/cache).
const ( // MinTTLSeconds is the minimum allowed TTL (1 minute). MinTTLSeconds = 60 // MaxTTLSeconds is the maximum allowed TTL (7 days). MaxTTLSeconds = 604800 )
Cache-internal constants.
Variables ¶
var ( ErrCacheNotFound = errors.New("cache entry not found") ErrCacheExpired = errors.New("cache entry expired") ErrInvalidCacheKey = errors.New("cache key cannot be empty") ErrInvalidCacheTTL = errors.New("cache TTL out of range") ErrCacheDisabled = errors.New("cache is disabled") ErrCacheLocked = errors.New("cache database locked by another process") )
Common cache errors.
var (
ErrInvalidTTL = fmt.Errorf("TTL must be between %d and %d seconds", MinTTLSeconds, MaxTTLSeconds)
)
TTL validation errors.
Functions ¶
func BucketFromKey ¶ added in v0.3.0
BucketFromKey returns the leading bucket name from a cache key by taking the substring before the first '/'. If the key contains no '/', the entire key is returned.
func BuildActualKey ¶ added in v0.3.0
func BuildActualKey(provider string, resourceTypes []string, from, to time.Time, filters map[string]string) string
BuildActualKey constructs a key for whole-query actual cost caching. Format: actual/{provider}/{types}/{from}/{to}/{filter-hash} Empty segments use "_" as a placeholder to ensure fixed-position keys and avoid ambiguity (e.g., provider="aws" vs resourceTypes=["aws"]). The resulting key is safe for use as a top-level bucketed cache key.
func BuildProjectedKey ¶ added in v0.3.0
BuildProjectedKey constructs a human-readable cache key for per-resource projected costs. The key has the form "projected/{provider}/{type}/{region}/{sku}". Any empty segment is replaced with "_" to preserve fixed segment positions.
func BuildRecommendationsKey ¶ added in v0.3.0
BuildRecommendationsKey constructs a cache key for recommendation results. The key has the format "recommendations/multi/{sorted-types-joined-by-+}". When resourceTypes is empty, the types segment uses "_" as a placeholder (consistent with sibling builders) to avoid a trailing slash.
func CalculatePluginTTL ¶ added in v0.3.4
CalculatePluginTTL determines the cache TTL from a plugin's expires_at hint.
Returns:
- ttlSeconds: The TTL to use (0 if skip is true)
- skip: true if the result should not be cached (past expiration)
- capped: true if the TTL was capped at MaxTTLSeconds
Behavior:
- nil expiresAt → returns (defaultTTL, false, false)
- past/current expiresAt → returns (0, true, false)
- future expiresAt within MaxTTLSeconds → returns (remaining seconds, false, false)
- future expiresAt exceeding MaxTTLSeconds → returns (MaxTTLSeconds, false, true)
func FormatDuration ¶
FormatDuration formats a duration in a human-readable way. Examples: "1h", "30m", "5m30s".
func GetCacheDirFromEnv ¶
func GetCacheDirFromEnv() string
GetCacheDirFromEnv reads the cache directory from environment variable. Returns an empty string if not set (caller should use default).
func GetCacheEnabledFromEnv ¶
func GetCacheEnabledFromEnv() bool
GetCacheEnabledFromEnv reads the cache enabled flag from environment variable. Returns true by default if the variable is not set.
func GetCacheMaxSizeFromEnv ¶
func GetCacheMaxSizeFromEnv() int
GetCacheMaxSizeFromEnv reads the max cache size from environment variable. Returns DefaultCacheMaxSizeMB if not set or invalid.
func GetTTLFromEnv ¶
func GetTTLFromEnv() int
GetTTLFromEnv reads the TTL from environment variable or returns the default. If the environment variable is invalid, returns the default and logs a warning.
func ParseTTL ¶
ParseTTL parses a TTL string in various formats: - Integer seconds: "3600". - Duration string: "1h", "30m", "1h30m".
func StripBucket ¶ added in v0.3.0
StripBucket returns the portion of key after the first "/" separator. If key contains no "/", the original key is returned unchanged.
Types ¶
type BoltStore ¶ added in v0.3.0
type BoltStore struct {
// contains filtered or unexported fields
}
BoltStore provides BoltDB-backed caching with TTL expiration. It stores cache entries as JSON values in named buckets. Thread-safe: uses bbolt's internal MVCC for concurrency.
func NewBoltStore ¶ added in v0.3.0
func NewBoltStore(ctx context.Context, directory string, enabled bool, ttlSeconds, maxSizeMB int) (*BoltStore, error)
NewBoltStore creates and returns a BoltStore backed by a BoltDB file located in the provided directory.
NewBoltStore will:
- return a disabled BoltStore when `enabled` is false.
- create the directory if it does not exist.
- open or create the BoltDB file at "<directory>/cache.db"; if the database is locked by another process an error is returned, and if the file is detected as corrupted it will be deleted and recreated.
- initialize the required top-level buckets.
- run a startup cleanup of expired entries and perform a size check that may trigger compaction if the DB exceeds `maxSizeMB`.
Parameters:
- ctx: context used to derive a logger.
- directory: filesystem directory to contain the BoltDB file (must be non-empty).
- enabled: if false, returns a disabled store without touching the filesystem.
- ttlSeconds: default time-to-live for new cache entries, in seconds.
- maxSizeMB: maximum database size in megabytes used to decide compaction.
Returns:
- *BoltStore on success, or an error if the directory is invalid, directory creation fails, the database cannot be opened/created, or bucket initialization fails.
func (*BoltStore) CleanupExpired ¶ added in v0.3.0
CleanupExpired removes all expired entries across all buckets. Returns the number of entries removed.
func (*BoltStore) Close ¶ added in v0.3.0
Close releases the database file handle and flushes pending writes. Safe to call multiple times; only the first call closes the database.
func (*BoltStore) Count ¶ added in v0.3.0
Count returns the total number of entries across all buckets.
func (*BoltStore) Delete ¶ added in v0.3.0
Delete removes a single cache entry by exact key. Idempotent: no error if key doesn't exist.
func (*BoltStore) Get ¶ added in v0.3.0
func (s *BoltStore) Get(key string) (*CacheEntry, error)
Get retrieves a cache entry by key. Returns ErrCacheNotFound if the key does not exist. Returns ErrCacheExpired if the entry exists but has expired (lazily deleted). Returns ErrCacheDisabled if the store is disabled.
func (*BoltStore) GetDirectory ¶ added in v0.3.0
GetDirectory returns the cache directory path.
func (*BoltStore) InvalidateByPrefix ¶ added in v0.3.0
InvalidateByPrefix removes all cache entries whose keys start with the given prefix. Returns the count of entries removed. An empty prefix clears the entire cache.
func (*BoltStore) Set ¶ added in v0.3.0
func (s *BoltStore) Set(key string, data json.RawMessage) error
Set stores a cache entry with the given key and data using the store's default TTL. The key format determines which bucket the entry is stored in. Concurrent calls are batched for efficiency via db.Batch(). Returns ErrCacheDisabled if caching is disabled. Returns ErrInvalidCacheKey if key is empty.
func (*BoltStore) SetWithTTL ¶ added in v0.3.4
SetWithTTL stores a cache entry with a caller-specified TTL instead of the store default. Used when a plugin provides an expires_at hint that should override the default TTL. Returns ErrCacheDisabled if caching is disabled. Returns ErrInvalidCacheKey if key is empty.
type Cache ¶ added in v0.3.0
type Cache interface {
Get(key string) (*CacheEntry, error)
Set(key string, data json.RawMessage) error
SetWithTTL(key string, data json.RawMessage, ttlSeconds int) error
IsEnabled() bool
Close() error
InvalidateByPrefix(prefix string) (int, error)
}
Cache defines the interface for cache operations used by the engine. All implementations must be safe for concurrent use by multiple goroutines.
type CacheEntry ¶
type CacheEntry struct {
// Key is the cache key (structured, human-readable, `/`-separated).
Key string `json:"key"`
// Data is the cached value (JSON-serializable).
Data json.RawMessage `json:"data"`
// CreatedAt is the timestamp when the entry was created.
CreatedAt time.Time `json:"created_at"`
// ExpiresAt is the timestamp when the entry expires.
ExpiresAt time.Time `json:"expires_at"`
// TTLSeconds is the time-to-live in seconds (for reference).
TTLSeconds int `json:"ttl_seconds"`
}
CacheEntry represents a single cached value with TTL metadata. It wraps arbitrary JSON-serializable data with expiration information.
func NewCacheEntry ¶
func NewCacheEntry(key string, data json.RawMessage, ttlSeconds int) *CacheEntry
NewCacheEntry creates a new cache entry with the given TTL. The entry is created with the current time and calculates expiration based on TTL.
func (*CacheEntry) Age ¶
func (e *CacheEntry) Age() time.Duration
Age returns the duration since the entry was created.
func (*CacheEntry) IsExpired ¶
func (e *CacheEntry) IsExpired() bool
IsExpired checks if the cache entry has expired based on current time. Returns true if the current time is after the expiration time.
func (*CacheEntry) IsValid ¶
func (e *CacheEntry) IsValid() bool
IsValid checks if the cache entry is valid (not expired). This is the inverse of IsExpired() and is provided for readability.
func (*CacheEntry) MarshalJSON ¶
func (e *CacheEntry) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler for CacheEntry. Times are stored as Unix timestamps (int64) for bbolt storage efficiency.
func (*CacheEntry) TimeUntilExpiration ¶
func (e *CacheEntry) TimeUntilExpiration() time.Duration
TimeUntilExpiration returns the duration until the entry expires. Returns 0 if already expired.
func (*CacheEntry) Touch ¶
func (e *CacheEntry) Touch()
Touch updates the entry's expiration time by extending it by the original TTL. This is useful for implementing "refresh on access" caching strategies.
func (*CacheEntry) UnmarshalJSON ¶
func (e *CacheEntry) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler for CacheEntry. Parses Unix timestamps from stored data.
type TTLConfig ¶
type TTLConfig struct {
// Seconds is the TTL duration in seconds.
Seconds int
// Duration is the TTL as a time.Duration.
Duration time.Duration
}
TTLConfig holds cache TTL configuration with validation.
func DefaultTTLConfig ¶
func DefaultTTLConfig() *TTLConfig
DefaultTTLConfig returns the default TTL configuration.
func NewTTLConfig ¶
NewTTLConfig creates a TTL configuration with validation. Returns an error if the TTL is outside the valid range.