Skip to content

Commit

Permalink
clearfeat: Add configurable cache refresh system
Browse files Browse the repository at this point in the history
Adds a cache refresh mechanism that runs before every command:
- Add refresh_interval config option (in minutes) to control cache freshness
- Add metadata tracking for cache refresh timestamps
- Split cache storage into data (.cache) and metadata (.cache.meta) files

Cache flow:
1. Every command loads existing cache data and metadata
2. If refresh_interval has elapsed:
   - For stats/reload: Synchronous refresh
   - For other commands: Async refresh (but command still uses existing cache data)
3. Command executes with loaded cache data

Technical implementation:
- Mutex-protected cache operations
- Separate metadata file for tracking refresh timestamps
- Support for excluded paths and patterns during refresh
- Clean cache implementation for version upgrades

Example config:
refresh_interval: 60  # Minutes between cache refresh attempts

Breaking changes: None
Migration: No action needed, existing cache files remain compatible

Note: The async refresh for non-stats commands doesnt provide immediate benefits
as commands always use the existing cache data. Consider refactoring to use async
refresh more effectively or removing the distinction between sync/async refresh.
  • Loading branch information
AccursedGalaxy committed Nov 4, 2024
1 parent 7e26f34 commit fe0390c
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 1 deletion.
87 changes: 86 additions & 1 deletion cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/AccursedGalaxy/streakode/scan"
Expand All @@ -16,15 +17,38 @@ import (
// -> Currently when a major version upgrade happens we need to manually delete cache file and do a refresh.

// in-memory cache to hold repo metadata during runtime
var Cache map[string]scan.RepoMetadata
var (
Cache map[string]scan.RepoMetadata
metadata CacheMetadata
mutex sync.RWMutex
)

type CacheMetadata struct {
LastRefresh time.Time
Version string // For Future Version Tracking
}

// InitCache - Initializes the in memory cache
func InitCache() {
Cache = make(map[string]scan.RepoMetadata)
}

// check if refresh is needed
func ShouldAutoRefresh(refreshInterval time.Duration) bool {
mutex.RLock()
defer mutex.RUnlock()

if metadata.LastRefresh.IsZero() {
return true
}
return time.Since(metadata.LastRefresh) > refreshInterval
}

// LoadCache - loads repository metadata from a JSON cache file into memory
func LoadCache(filePath string) error {
mutex.Lock()
defer mutex.Unlock()

file, err := os.Open(filePath)
if os.IsNotExist(err) {
InitCache()
Expand All @@ -41,11 +65,32 @@ func LoadCache(filePath string) error {
return nil
}

// Load metadata from a separate file
metadataPath := filePath + ".meta"
metaFile, err := os.Open(metadataPath)
if os.IsNotExist(err) {
metadata = CacheMetadata{LastRefresh: time.Time{}}
return nil
}
if err != nil {
return fmt.Errorf("error opening metadata file: %v", err)
}
defer metaFile.Close()

decoder = json.NewDecoder(metaFile)
if err := decoder.Decode(&metadata); err != nil {
metadata = CacheMetadata{LastRefresh: time.Time{}}
return nil
}

return nil
}

// SaveCache - saves the in-memory cache to a JSON file
func SaveCache(filePath string) error {
mutex.Lock()
defer mutex.Unlock()

// Create directory if it doesn't exist
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
Expand All @@ -63,6 +108,20 @@ func SaveCache(filePath string) error {
return fmt.Errorf("error encoding cache: %v", err)
}

// Save metadata to a separate file
metadataPath := filePath + ".meta"
metaFile, err := os.Create(metadataPath)
if err != nil {
return fmt.Errorf("error creating metadata file: %v", err)
}
defer metaFile.Close()

metadata.LastRefresh = time.Now()
encoder = json.NewEncoder(metaFile)
if err := encoder.Encode(metadata); err != nil {
return fmt.Errorf("error encoding metadata: %v", err)
}

return nil
}

Expand Down Expand Up @@ -138,3 +197,29 @@ func RefreshCache(dirs []string, author string, cacheFilePath string, excludedPa

return SaveCache(cacheFilePath)
}

// AsyncRefreshCache performs a non-blocking cache refresh
func AsyncRefreshCache(dirs []string, author string, cacheFilePath string, excludedPatterns []string, excludedPaths []string) {
go func() {
if err := RefreshCache(dirs, author, cacheFilePath, excludedPatterns, excludedPaths); err != nil {
log.Printf("Background cache refresh failed: %v", err)
}
}()
}

// QuickNeedsRefresh performs a fast check if refresh is needed without scanning repositories
func QuickNeedsRefresh(refreshInterval time.Duration) bool {
mutex.RLock()
defer mutex.RUnlock()

if metadata.LastRefresh.IsZero() {
return true
}

// Check if cache file exists and its modification time
if time.Since(metadata.LastRefresh) > refreshInterval {
return true
}

return false
}
53 changes: 53 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
"time"

"github.com/AccursedGalaxy/streakode/cache"
"github.com/AccursedGalaxy/streakode/cmd"
Expand Down Expand Up @@ -35,6 +36,54 @@ func getCacheFilePath(profile string) string {
return filepath.Join(home, fmt.Sprintf(".streakode_%s.cache", profile))
}

func ensureCacheRefresh() error {
// Skip if no refresh interval is configured
if config.AppConfig.RefreshInterval <= 0 {
return nil
}

interval := time.Duration(config.AppConfig.RefreshInterval) * time.Minute

// Quick check if refresh is needed
if cache.QuickNeedsRefresh(interval) {
cacheFilePath := getCacheFilePath(config.AppState.ActiveProfile)

// For commands that need fresh data, use sync refresh
if requiresFreshData() {
return cache.RefreshCache(
config.AppConfig.ScanDirectories,
config.AppConfig.Author,
cacheFilePath,
config.AppConfig.ScanSettings.ExcludedPatterns,
config.AppConfig.ScanSettings.ExcludedPaths,
)
}

// For other commands, use async refresh
cache.AsyncRefreshCache(
config.AppConfig.ScanDirectories,
config.AppConfig.Author,
cacheFilePath,
config.AppConfig.ScanSettings.ExcludedPatterns,
config.AppConfig.ScanSettings.ExcludedPaths,
)
}
return nil
}

func requiresFreshData() bool {
// Get the command being executed
cmd := os.Args[1]

// List of commands that need fresh data
freshDataCommands := map[string]bool{
"stats": true,
"reload": true,
}

return freshDataCommands[cmd]
}

func main() {
var profile string

Expand All @@ -55,6 +104,10 @@ func main() {
if err := cache.LoadCache(cacheFilePath); err != nil {
fmt.Printf("Error loading cache: %v\n", err)
}

if err := ensureCacheRefresh(); err != nil {
fmt.Printf("Error refreshing cache: %v\n", err)
}
},
}

Expand Down

0 comments on commit fe0390c

Please sign in to comment.