225 lines
5.2 KiB
Go
225 lines
5.2 KiB
Go
package disk
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDiskFS_Basic(t *testing.T) {
|
|
t.Parallel()
|
|
td := t.TempDir()
|
|
d := New(td, 10*1024*1024)
|
|
if d.Name() != "DiskFS" {
|
|
t.Error("name")
|
|
}
|
|
|
|
w, err := d.Create("k1", 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Write([]byte("hello disk cache test data here"))
|
|
w.Close()
|
|
|
|
if d.Size() < 30 { // actual may differ slightly from declared
|
|
t.Errorf("size too small %d", d.Size())
|
|
}
|
|
|
|
r, err := d.Open("k1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := io.ReadAll(r)
|
|
r.Close()
|
|
if len(data) < 10 {
|
|
t.Error("read small")
|
|
}
|
|
|
|
d.Delete("k1")
|
|
if _, err := d.Open("k1"); err == nil {
|
|
t.Error("deleted still readable")
|
|
}
|
|
}
|
|
|
|
func TestDiskFS_EvictAndLazyStat(t *testing.T) {
|
|
t.Parallel()
|
|
td := t.TempDir()
|
|
d := New(td, 400)
|
|
// create files that will be evicted
|
|
for i := 0; i < 5; i++ {
|
|
w, _ := d.Create("f"+string(rune('0'+i)), 120)
|
|
w.Write(make([]byte, 120))
|
|
w.Close()
|
|
}
|
|
ev := d.EvictLRU(200)
|
|
if ev == 0 {
|
|
t.Log("no evict (size calc async or snapshot tolerance?)")
|
|
}
|
|
// lazy stat should still work for remaining; batch eviction may be approximate under heavy pressure
|
|
if d.Size() > d.Capacity()*2 { // generous for async bg size
|
|
t.Errorf("disk size %d >> cap after evict", d.Size())
|
|
}
|
|
}
|
|
|
|
func TestDiskFS_Concurrent(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip()
|
|
}
|
|
t.Parallel()
|
|
td := t.TempDir()
|
|
d := New(td, 50*1024*1024)
|
|
var wg sync.WaitGroup
|
|
var ops int64
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 30; j++ {
|
|
key := "d" + string(rune(id+'a')) + string(rune(j))
|
|
w, e := d.Create(key, 256)
|
|
if e == nil {
|
|
w.Write(make([]byte, 256))
|
|
w.Close()
|
|
atomic.AddInt64(&ops, 1)
|
|
}
|
|
if r, e := d.Open(key); e == nil {
|
|
io.Copy(io.Discard, r)
|
|
r.Close()
|
|
atomic.AddInt64(&ops, 1)
|
|
}
|
|
d.Delete(key)
|
|
if j%7 == 0 {
|
|
d.EvictLRU(1024)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
// Bounded poll instead of fixed sleep for bg size calc goroutine settlement (robust to variance; issue7)
|
|
deadline := time.Now().Add(300 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
if d.Size() <= d.Capacity() {
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
if d.Size() > d.Capacity() {
|
|
t.Errorf("concurrent disk size exceeded: %d", d.Size())
|
|
}
|
|
}
|
|
|
|
func BenchmarkDiskFS_CreateOpen(b *testing.B) {
|
|
td := b.TempDir()
|
|
d := New(td, 128*1024*1024)
|
|
data := make([]byte, 8192)
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
key := "bd" + string(rune(i%500))
|
|
w, _ := d.Create(key, 8192)
|
|
w.Write(data)
|
|
w.Close()
|
|
r, _ := d.Open(key)
|
|
io.Copy(io.Discard, r)
|
|
r.Close()
|
|
d.Delete(key)
|
|
}
|
|
}
|
|
|
|
func TestDiskFS_EvictVariantsAndInvalid(t *testing.T) {
|
|
t.Parallel()
|
|
td := t.TempDir()
|
|
d := New(td, 600)
|
|
for i := 0; i < 4; i++ {
|
|
w, _ := d.Create("dv"+string(rune('0'+i)), 120)
|
|
w.Write(make([]byte, 120))
|
|
w.Close()
|
|
}
|
|
_ = d.EvictBySize(80, false) // largest
|
|
_ = d.EvictFIFO(50)
|
|
_ = d.EvictLFU(30)
|
|
_ = d.EvictHybrid(30)
|
|
|
|
// invalids (sanitized in Create/Open)
|
|
if _, err := d.Create("", 1); err == nil {
|
|
t.Error("empty")
|
|
}
|
|
if _, err := d.Create("/abs/bad", 1); err == nil {
|
|
t.Error("abs")
|
|
}
|
|
if _, err := d.Open("missing"); err == nil {
|
|
t.Error("missing open")
|
|
}
|
|
_ = d.Delete("missing")
|
|
_, _ = d.Stat("missing")
|
|
}
|
|
|
|
// TestEvict_ConcurrentCloseDuringEviction exercises Creates, Opens, and Closes (which mutate *FileInfo and size under lock)
|
|
// concurrently with all Evict* (LRU + non-LRU scalar snapshot paths) on DiskFS under pressure.
|
|
// Sufficient goroutines/iterations to hit prior race windows for Issues 1-3. Asserts size invariant with
|
|
// Documented epsilon tolerance for raw DiskFS (background size calc + snapshot tolerance during batch eviction). -race must pass.
|
|
func TestEvict_ConcurrentCloseDuringEviction(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip()
|
|
}
|
|
t.Parallel()
|
|
td := t.TempDir()
|
|
cap := int64(256 * 1024)
|
|
d := New(td, cap)
|
|
var wg sync.WaitGroup
|
|
const nWriters = 4
|
|
const nEvictors = 3
|
|
const iters = 25
|
|
for i := 0; i < nWriters; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < iters; j++ {
|
|
key := "r" + string(rune('0'+id%5)) + "/" + string(rune('0'+j%10))
|
|
w, err := d.Create(key, 8192)
|
|
if err == nil {
|
|
w.Write(make([]byte, 4096))
|
|
w.Close() // exercises Close size mutation path concurrent with evicts
|
|
}
|
|
if r, err := d.Open(key); err == nil {
|
|
io.Copy(io.Discard, r)
|
|
r.Close()
|
|
}
|
|
if j%4 == 0 {
|
|
d.Delete(key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
for i := 0; i < nEvictors; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < iters*2; j++ {
|
|
// Cycle through strategies to cover all snapshot + re-fetch + LRU-Lock paths
|
|
switch j % 6 {
|
|
case 0:
|
|
d.EvictLRU(4096)
|
|
case 1:
|
|
d.EvictBySize(4096, true)
|
|
case 2:
|
|
d.EvictBySize(4096, false)
|
|
case 3:
|
|
d.EvictFIFO(4096)
|
|
case 4:
|
|
d.EvictLFU(4096)
|
|
default:
|
|
d.EvictHybrid(4096)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
// Final size <= cap with epsilon (raw DiskFS allows small over per bg size + snapshot design; see TestDiskFS_Concurrent and memory +50 pattern)
|
|
if sz := d.Size(); sz > cap+2048 {
|
|
t.Errorf("final size %d exceeded cap %d + epsilon tolerance after concurrent close+evict", sz, cap)
|
|
}
|
|
}
|