mirror of
https://github.com/LukeHagar/slinky.git
synced 2025-12-06 04:21:20 +00:00
This commit introduces a mutex to the URLCache struct to ensure thread-safe access to cache entries during load, get, set, and clear operations. Additionally, it refines the job handling logic in the CheckURLs function by using atomic counters for processed and pending jobs, improving concurrency management.
147 lines
3.2 KiB
Go
147 lines
3.2 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// CacheEntry represents a cached URL check result
|
|
type CacheEntry struct {
|
|
URL string `json:"url"`
|
|
OK bool `json:"ok"`
|
|
Status int `json:"status"`
|
|
ErrMsg string `json:"error,omitempty"`
|
|
Checked time.Time `json:"checked"`
|
|
}
|
|
|
|
// URLCache manages URL result caching
|
|
type URLCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]CacheEntry
|
|
ttl time.Duration
|
|
path string
|
|
}
|
|
|
|
// NewURLCache creates a new URL cache with optional file path
|
|
func NewURLCache(cachePath string, ttlHours int) *URLCache {
|
|
ttl := time.Duration(ttlHours) * time.Hour
|
|
if ttl <= 0 {
|
|
ttl = 24 * time.Hour // Default 24 hours
|
|
}
|
|
return &URLCache{
|
|
entries: make(map[string]CacheEntry),
|
|
ttl: ttl,
|
|
path: cachePath,
|
|
}
|
|
}
|
|
|
|
// Load loads cache entries from file if path is set
|
|
func (c *URLCache) Load() error {
|
|
if c.path == "" {
|
|
return nil // No cache file specified
|
|
}
|
|
|
|
data, err := os.ReadFile(c.path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil // Cache file doesn't exist yet, that's OK
|
|
}
|
|
return fmt.Errorf("failed to read cache file: %w", err)
|
|
}
|
|
|
|
var entries []CacheEntry
|
|
if err := json.Unmarshal(data, &entries); err != nil {
|
|
// Non-critical: if cache is corrupted, start fresh
|
|
return nil
|
|
}
|
|
|
|
now := time.Now()
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.entries = make(map[string]CacheEntry, len(entries))
|
|
for _, entry := range entries {
|
|
// Only load entries that haven't expired
|
|
if now.Sub(entry.Checked) < c.ttl {
|
|
c.entries[entry.URL] = entry
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves a cached result for a URL
|
|
func (c *URLCache) Get(url string) (CacheEntry, bool) {
|
|
c.mu.RLock()
|
|
entry, ok := c.entries[url]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return CacheEntry{}, false
|
|
}
|
|
|
|
// Check if entry has expired
|
|
if time.Since(entry.Checked) >= c.ttl {
|
|
c.mu.Lock()
|
|
delete(c.entries, url)
|
|
c.mu.Unlock()
|
|
return CacheEntry{}, false
|
|
}
|
|
|
|
return entry, true
|
|
}
|
|
|
|
// Set stores a result in the cache
|
|
func (c *URLCache) Set(url string, ok bool, status int, errMsg string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.entries[url] = CacheEntry{
|
|
URL: url,
|
|
OK: ok,
|
|
Status: status,
|
|
ErrMsg: errMsg,
|
|
Checked: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Save saves cache entries to file if path is set
|
|
func (c *URLCache) Save() error {
|
|
if c.path == "" {
|
|
return nil // No cache file specified
|
|
}
|
|
|
|
// Convert map to slice for JSON serialization
|
|
c.mu.RLock()
|
|
entries := make([]CacheEntry, 0, len(c.entries))
|
|
for _, entry := range c.entries {
|
|
entries = append(entries, entry)
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
data, err := json.MarshalIndent(entries, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal cache: %w", err)
|
|
}
|
|
|
|
// Write to temp file first, then rename (atomic write)
|
|
tmpPath := c.path + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write cache file: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(tmpPath, c.path); err != nil {
|
|
return fmt.Errorf("failed to rename cache file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Clear removes all entries from the cache
|
|
func (c *URLCache) Clear() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.entries = make(map[string]CacheEntry)
|
|
}
|
|
|