97 lines
2.4 KiB
Go
97 lines
2.4 KiB
Go
|
|
package engine
|
||
|
|
|
||
|
|
import (
|
||
|
|
"os"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// probeCacheTTL is how long a cached probe stays usable. The cache key
|
||
|
|
// already incorporates mtime + size, so the TTL is a defense against
|
||
|
|
// runaway memory growth from stale paths, not a freshness guarantee — a
|
||
|
|
// rename + recreate at the same inode (rare) would still be caught by the
|
||
|
|
// mtime delta.
|
||
|
|
const probeCacheTTL = 30 * time.Minute
|
||
|
|
|
||
|
|
type probeCacheEntry struct {
|
||
|
|
probe *StreamProbe
|
||
|
|
expires time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
type probeCacheKey struct {
|
||
|
|
path string
|
||
|
|
mtime int64 // ModTime().UnixNano()
|
||
|
|
size int64
|
||
|
|
}
|
||
|
|
|
||
|
|
var (
|
||
|
|
probeCacheMu sync.RWMutex
|
||
|
|
probeCache = make(map[probeCacheKey]probeCacheEntry)
|
||
|
|
)
|
||
|
|
|
||
|
|
// lookupProbeCache returns the cached StreamProbe for the given path if its
|
||
|
|
// mtime + size still match the value recorded at insert time, AND the cache
|
||
|
|
// entry hasn't expired. Any stat failure / mismatch returns (nil, false) so
|
||
|
|
// the caller falls through to a fresh ffprobe run.
|
||
|
|
func lookupProbeCache(path string) (*StreamProbe, bool) {
|
||
|
|
fi, err := os.Stat(path)
|
||
|
|
if err != nil {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
key := probeCacheKey{
|
||
|
|
path: path,
|
||
|
|
mtime: fi.ModTime().UnixNano(),
|
||
|
|
size: fi.Size(),
|
||
|
|
}
|
||
|
|
probeCacheMu.RLock()
|
||
|
|
entry, ok := probeCache[key]
|
||
|
|
probeCacheMu.RUnlock()
|
||
|
|
if !ok {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
if time.Now().After(entry.expires) {
|
||
|
|
probeCacheMu.Lock()
|
||
|
|
delete(probeCache, key)
|
||
|
|
probeCacheMu.Unlock()
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
return entry.probe, true
|
||
|
|
}
|
||
|
|
|
||
|
|
// storeProbeCache stashes a fresh probe result under the (path, mtime, size)
|
||
|
|
// key. A subsequent ffprobe-skipping HIT requires the file to still have the
|
||
|
|
// same mtime + size — anything else (re-encoded, renamed+recreated at the
|
||
|
|
// same path, truncated) misses and triggers a re-probe.
|
||
|
|
func storeProbeCache(path string, probe *StreamProbe) {
|
||
|
|
fi, err := os.Stat(path)
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
key := probeCacheKey{
|
||
|
|
path: path,
|
||
|
|
mtime: fi.ModTime().UnixNano(),
|
||
|
|
size: fi.Size(),
|
||
|
|
}
|
||
|
|
probeCacheMu.Lock()
|
||
|
|
probeCache[key] = probeCacheEntry{
|
||
|
|
probe: probe,
|
||
|
|
expires: time.Now().Add(probeCacheTTL),
|
||
|
|
}
|
||
|
|
probeCacheMu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ResetProbeCache clears the in-memory probe cache. Test-only.
|
||
|
|
func ResetProbeCache() {
|
||
|
|
probeCacheMu.Lock()
|
||
|
|
probeCache = make(map[probeCacheKey]probeCacheEntry)
|
||
|
|
probeCacheMu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ProbeCacheSize returns the number of entries currently cached. Exposed
|
||
|
|
// for diagnostics + tests.
|
||
|
|
func ProbeCacheSize() int {
|
||
|
|
probeCacheMu.RLock()
|
||
|
|
defer probeCacheMu.RUnlock()
|
||
|
|
return len(probeCache)
|
||
|
|
}
|