package paths import ( "os" "sync" "time" lru "github.com/hashicorp/golang-lru/v2" "github.com/filecoin-project/lotus/storage/sealer/fsutil" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) var StatTimeout = 5 * time.Second var MaxDiskUsageDuration = time.Second type cachedLocalStorage struct { base LocalStorage statLk sync.Mutex stats *lru.Cache[string, statEntry] pathDUs *lru.Cache[string, *diskUsageEntry] } func newCachedLocalStorage(ls LocalStorage) *cachedLocalStorage { statCache, _ := lru.New[string, statEntry](1024) duCache, _ := lru.New[string, *diskUsageEntry](1024) return &cachedLocalStorage{ base: ls, stats: statCache, pathDUs: duCache, } } type statEntry struct { stat fsutil.FsStat time time.Time } type diskUsageEntry struct { last diskUsageResult usagePromise <-chan diskUsageResult } type diskUsageResult struct { usage int64 time time.Time } func (c *cachedLocalStorage) GetStorage() (storiface.StorageConfig, error) { return c.base.GetStorage() } func (c *cachedLocalStorage) SetStorage(f func(*storiface.StorageConfig)) error { return c.base.SetStorage(f) } func (c *cachedLocalStorage) Stat(path string) (fsutil.FsStat, error) { c.statLk.Lock() defer c.statLk.Unlock() if v, ok := c.stats.Get(path); ok && time.Now().Sub(v.time) < StatTimeout { return v.stat, nil } // if we don't, get the stat st, err := c.base.Stat(path) if err == nil { c.stats.Add(path, statEntry{ stat: st, time: time.Now(), }) } return st, err } func (c *cachedLocalStorage) DiskUsage(path string) (int64, error) { c.statLk.Lock() defer c.statLk.Unlock() var entry *diskUsageEntry if v, ok := c.pathDUs.Get(path); ok { entry = v // if we have recent cached entry, use that if time.Now().Sub(entry.last.time) < StatTimeout { return entry.last.usage, nil } } else { entry = new(diskUsageEntry) c.pathDUs.Add(path, entry) } // start a new disk usage request; this can take a while so start a // goroutine, and if it doesn't return quickly, return either the // previous value, or zero - that's better than potentially blocking // here for a long time. if entry.usagePromise == nil { resCh := make(chan diskUsageResult, 1) go func() { du, err := c.base.DiskUsage(path) if err != nil { if !os.IsNotExist(err) { log.Errorw("error getting disk usage", "path", path, "error", err) } } resCh <- diskUsageResult{ usage: du, time: time.Now(), } }() entry.usagePromise = resCh } // wait for the disk usage result; if it doesn't come, fallback on // previous usage select { case du := <-entry.usagePromise: entry.usagePromise = nil entry.last = du case <-time.After(MaxDiskUsageDuration): log.Warnw("getting usage is slow, falling back to previous usage", "path", path, "fallback", entry.last.usage, "age", time.Now().Sub(entry.last.time)) } return entry.last.usage, nil } var _ LocalStorage = &cachedLocalStorage{}