diff --git a/cache/disk/census.go b/cache/disk/census.go index 8ae274a..746605f 100644 --- a/cache/disk/census.go +++ b/cache/disk/census.go @@ -425,7 +425,6 @@ func (r *censusRecorder) OnEvict(key string, value lruItem) { if now > value.addedAt { createdAge = now - value.addedAt } - neverRead := value.lastAccess == value.addedAt lastReadIdx := censusBucketIdx(lastReadAge) createdIdx := censusBucketIdx(createdAge) @@ -436,13 +435,16 @@ func (r *censusRecorder) OnEvict(key string, value lruItem) { acc.residentEntries-- acc.evictedBytes += value.sizeOnDisk acc.evictedEntries++ - if neverRead { + switch value.accessState { + case itemStateNew: acc.evictedNeverReadByes += value.sizeOnDisk - } else { + case itemStateTouched: if lastReadAge < censusLiveThreshold { acc.evictedLiveBytes += value.sizeOnDisk } acc.evictedByLastReadAge[lastReadIdx] += value.sizeOnDisk + // itemStateBootScanned: lastAccess is file atime (frozen at creation on + // noatime mounts), not a read, so the entry has no last-read history. } acc.evictedByCreatedAge[createdIdx] += value.sizeOnDisk acc.removeResident(class, value.sizeOnDisk, value.accessState) @@ -452,16 +454,15 @@ func (r *censusRecorder) OnEvict(key string, value lruItem) { r.mu.Unlock() sz := float64(value.sizeOnDisk) - // Never-read entries have no last read: they are counted in the - // never-read counter and the created-age buckets only, so the - // last_read series stays a pure harm signal (bytes someone actually - // used, evicted anyway). This also gives thrash alerts built-in - // restart tolerance: boot-scanned entries look never-read until first - // touched, so a roll cannot fabricate still-live evictions. - if neverRead { + // Only touched entries have a last read, so the last_read series stays + // a pure harm signal (bytes someone actually used, evicted anyway) and + // a roll cannot fabricate still-live evictions out of boot-scanned + // entries; those count in the created-age buckets only. + switch value.accessState { + case itemStateNew: r.evictedNeverReadBytes.WithLabelValues(tool).Add(sz) r.evictedNeverReadEntries.WithLabelValues(tool).Inc() - } else { + case itemStateTouched: r.evictedBytesByAge.WithLabelValues("last_read", censusBucketLabels[lastReadIdx], tool).Add(sz) r.evictedEntriesByAge.WithLabelValues("last_read", censusBucketLabels[lastReadIdx], tool).Inc() } diff --git a/cache/disk/census_test.go b/cache/disk/census_test.go index 98a2e34..5743507 100644 --- a/cache/disk/census_test.go +++ b/cache/disk/census_test.go @@ -409,6 +409,28 @@ func TestCensusBootScannedIsUnknownNotNeverRead(t *testing.T) { if rows[0].EvictedNeverReadBytesByClass[class] != 0 { t.Errorf("EvictedNeverReadBytesByClass = %d, want 0", rows[0].EvictedNeverReadBytesByClass[class]) } + // The scalar and its counters follow the same access state: a boot-scanned + // entry has addedAt == lastAccess, which must not read as never-read. + if rows[0].EvictedNeverReadBytes != 0 || rows[0].EvictedBytes != 50 { + t.Errorf("EvictedNeverReadBytes/EvictedBytes = %d/%d, want 0/50", rows[0].EvictedNeverReadBytes, rows[0].EvictedBytes) + } + if got := testutil.ToFloat64(rec.evictedNeverReadEntries.WithLabelValues("unknown")); got != 0 { + t.Errorf("evictedNeverReadEntries = %v, want 0", got) + } + // Nor as read: its lastAccess is file atime, not a hit. + if rows[0].EvictedLiveBytes != 0 { + t.Errorf("EvictedLiveBytes = %d, want 0", rows[0].EvictedLiveBytes) + } + for i, v := range rows[0].EvictedByLastReadAge { + if v != 0 { + t.Errorf("EvictedByLastReadAge[%d] = %d, want 0", i, v) + } + } + for _, label := range censusBucketLabels { + if got := testutil.ToFloat64(rec.evictedEntriesByAge.WithLabelValues("last_read", label, "unknown")); got != 0 { + t.Errorf("evictedEntriesByAge last_read %s = %v, want 0", label, got) + } + } if rows[0].ResidentUnknownBytesByClass[class] != 0 || rows[0].ResidentBytesByClass[class] != 50 { t.Errorf("resident unknown/total = %d/%d, want 0/50", rows[0].ResidentUnknownBytesByClass[class], rows[0].ResidentBytesByClass[class])