Files
Andrey Bocharnikov 4f622fb238 feat(cache): add multi-level cache
* l1 biggache, l2 keydb
* extracted from status-im/eth-rpc-proxy to reuse in another projects

fixes status-im/eth-rpc-proxy#106
2026-02-05 22:40:32 +04:00

148 lines
3.4 KiB
Go

package models
import (
"fmt"
"time"
"gopkg.in/yaml.v3"
)
// CacheType represents the type of caching for a method
type CacheType string
const (
CacheTypePermanent CacheType = "permanent"
CacheTypeShort CacheType = "short"
CacheTypeMinimal CacheType = "minimal"
CacheTypeNone CacheType = "none"
)
// UnmarshalYAML implements custom YAML unmarshaling for CacheType
func (c *CacheType) UnmarshalYAML(value *yaml.Node) error {
var str string
if err := value.Decode(&str); err != nil {
return err
}
switch str {
case "permanent", "short", "minimal", "none":
*c = CacheType(str)
return nil
default:
return fmt.Errorf("invalid cache type '%s': must be one of 'permanent', 'short', 'minimal', 'none'", str)
}
}
// CacheStatus represents the cache status for a request
type CacheStatus string
const (
CacheStatusHit CacheStatus = "HIT"
CacheStatusMiss CacheStatus = "MISS"
CacheStatusBypass CacheStatus = "BYPASS"
)
func (cs CacheStatus) String() string {
return string(cs)
}
// IsValid checks if the cache status is one of the valid values
func (cs CacheStatus) IsValid() bool {
switch cs {
case CacheStatusHit, CacheStatusMiss, CacheStatusBypass:
return true
default:
return false
}
}
// CacheInfo contains cache configuration information
type CacheInfo struct {
TTL time.Duration `json:"ttl"`
CacheType CacheType `json:"cache_type"`
}
// TTL represents cache time-to-live configuration
type TTL struct {
Fresh time.Duration // How long the data is considered fresh
Stale time.Duration // How long stale data can be served (stale-if-error)
}
// CacheLevel represents the cache level where data was found
type CacheLevel string
const (
CacheLevelL1 CacheLevel = "L1"
CacheLevelL2 CacheLevel = "L2"
CacheLevelMiss CacheLevel = "MISS"
)
func (cl CacheLevel) String() string {
return string(cl)
}
// CacheLevelFromIndex creates a CacheLevel from a cache index.
// Index 0 returns L1, index 1 returns L2, higher indices return L3, L4, etc.
// Negative indices are treated as L1 (fallback to first level)
func CacheLevelFromIndex(index int) CacheLevel {
if index < 0 {
return CacheLevelL1
}
switch index {
case 0:
return CacheLevelL1
case 1:
return CacheLevelL2
default:
return CacheLevel(fmt.Sprintf("L%d", index+1))
}
}
// CacheResult represents the result of a cache operation with level information
type CacheResult struct {
Entry *CacheEntry `json:"entry,omitempty"`
Found bool `json:"found"`
Level CacheLevel `json:"level"`
}
// CacheEntry represents an entry in the cache with TTL information
type CacheEntry struct {
Data []byte `json:"data"`
ExpiresAt int64 `json:"expires_at"`
StaleAt int64 `json:"stale_at"`
CreatedAt int64 `json:"created_at"`
}
// IsExpired checks if the cache entry is completely expired
func (ce *CacheEntry) IsExpired() bool {
now := time.Now().Unix()
return now > ce.ExpiresAt
}
// IsFresh checks if the cache entry is still fresh
func (ce *CacheEntry) IsFresh() bool {
now := time.Now().Unix()
return now <= ce.StaleAt
}
// RemainingTTL calculates the remaining TTL for this cache entry
func (ce *CacheEntry) RemainingTTL() TTL {
now := time.Now().Unix()
freshRemaining := ce.StaleAt - now
if freshRemaining < 0 {
freshRemaining = 0
}
staleRemaining := ce.ExpiresAt - ce.StaleAt
if staleRemaining < 0 {
staleRemaining = 0
}
return TTL{
Fresh: time.Duration(freshRemaining) * time.Second,
Stale: time.Duration(staleRemaining) * time.Second,
}
}