Remove plans/ directory (P0/P1/P2 work complete)
This commit is contained in:
+164
-152
@@ -15,6 +15,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// maxEvictBatch bounds the candidate snapshot during RLock/Lock collect in Evict*.
|
||||
// Prevents holding lock for unbounded time under extreme pressure.
|
||||
const maxEvictBatch = 4096
|
||||
|
||||
// Ensure MemoryFS implements VFS.
|
||||
var _ vfs.VFS = (*MemoryFS)(nil)
|
||||
|
||||
@@ -300,226 +304,234 @@ func (m *MemoryFS) Stat(key string) (*types.FileInfo, error) {
|
||||
}
|
||||
|
||||
// EvictLRU evicts the least recently used files to free up space
|
||||
// Collect under short exclusive Lock (to serialize concurrent EvictLRU on the unsynchronized LRUList),
|
||||
// then batch delete under WLock. Regular mutation paths (Open/Create) use the normal locking.
|
||||
// already serialize via full Lock. The O(maxEvictBatch) walk is negligible vs. deletes.
|
||||
func (m *MemoryFS) EvictLRU(bytesNeeded uint) uint {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var evicted uint
|
||||
|
||||
// Evict from LRU list until we free enough space
|
||||
for m.size > m.capacity-int64(bytesNeeded) && m.LRU.Len() > 0 {
|
||||
// Get the least recently used item
|
||||
var toEvict []string
|
||||
need := int64(bytesNeeded)
|
||||
cur := m.size
|
||||
for cur > m.capacity-need && m.LRU.Len() > 0 && len(toEvict) < maxEvictBatch {
|
||||
elem := m.LRU.Back()
|
||||
if elem == nil {
|
||||
break
|
||||
}
|
||||
|
||||
fi := elem.Value.(*types.FileInfo)
|
||||
key := fi.Key
|
||||
toEvict = append(toEvict, fi.Key)
|
||||
cur -= fi.Size // local estimate; real size updated in W phase
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Remove from LRU
|
||||
m.LRU.Remove(key)
|
||||
|
||||
// Remove from maps
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
|
||||
// Update size
|
||||
m.size -= fi.Size
|
||||
evicted += uint(fi.Size)
|
||||
|
||||
// Clean up key lock
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
if len(toEvict) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
var evicted uint
|
||||
for _, key := range toEvict {
|
||||
if fi, exists := m.info[key]; exists {
|
||||
m.LRU.Remove(key)
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
m.size -= fi.Size
|
||||
evicted += uint(fi.Size)
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
return evicted
|
||||
}
|
||||
|
||||
// EvictBySize evicts files by size (ascending = smallest first, descending = largest first)
|
||||
// Collect scalar snapshot (key+size) under RLock (no shared *FileInfo pointers),
|
||||
// sort on copy, brief WLock with live re-fetch for size subtract (fixes data race + accounting drift).
|
||||
type evictCandidate struct {
|
||||
key string
|
||||
size int64
|
||||
}
|
||||
|
||||
func (m *MemoryFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.mu.RLock()
|
||||
var candidates []evictCandidate
|
||||
for key, fi := range m.info {
|
||||
candidates = append(candidates, evictCandidate{key: key, size: fi.Size})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
var evicted uint
|
||||
var candidates []*types.FileInfo
|
||||
|
||||
// Collect all files
|
||||
for _, fi := range m.info {
|
||||
candidates = append(candidates, fi)
|
||||
if len(candidates) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Sort by size
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
if ascending {
|
||||
return candidates[i].Size < candidates[j].Size
|
||||
return candidates[i].size < candidates[j].size
|
||||
}
|
||||
return candidates[i].Size > candidates[j].Size
|
||||
return candidates[i].size > candidates[j].size
|
||||
})
|
||||
|
||||
// Evict files until we free enough space
|
||||
for _, fi := range candidates {
|
||||
m.mu.Lock()
|
||||
var evicted uint
|
||||
for _, c := range candidates {
|
||||
if m.size <= m.capacity-int64(bytesNeeded) {
|
||||
break
|
||||
}
|
||||
|
||||
key := fi.Key
|
||||
|
||||
// Remove from LRU
|
||||
m.LRU.Remove(key)
|
||||
|
||||
// Remove from maps
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
|
||||
// Update size
|
||||
m.size -= fi.Size
|
||||
evicted += uint(fi.Size)
|
||||
|
||||
// Clean up key lock
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
key := c.key
|
||||
if liveFi, exists := m.info[key]; exists {
|
||||
m.LRU.Remove(key)
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
m.size -= liveFi.Size
|
||||
evicted += uint(liveFi.Size)
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Unlock()
|
||||
return evicted
|
||||
}
|
||||
|
||||
// EvictFIFO evicts files using FIFO (oldest creation time first)
|
||||
// Collect scalar snapshot (key+ctime) under RLock, sort on copy, W phase with live re-fetch.
|
||||
func (m *MemoryFS) EvictFIFO(bytesNeeded uint) uint {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var evicted uint
|
||||
var candidates []*types.FileInfo
|
||||
|
||||
// Collect all files
|
||||
for _, fi := range m.info {
|
||||
candidates = append(candidates, fi)
|
||||
m.mu.RLock()
|
||||
var candidates []struct {
|
||||
key string
|
||||
cTime time.Time
|
||||
}
|
||||
for key, fi := range m.info {
|
||||
candidates = append(candidates, struct {
|
||||
key string
|
||||
cTime time.Time
|
||||
}{key: key, cTime: fi.CTime})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Sort by creation time (oldest first)
|
||||
if len(candidates) == 0 {
|
||||
return 0
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].CTime.Before(candidates[j].CTime)
|
||||
return candidates[i].cTime.Before(candidates[j].cTime)
|
||||
})
|
||||
|
||||
// Evict oldest files until we free enough space
|
||||
for _, fi := range candidates {
|
||||
m.mu.Lock()
|
||||
var evicted uint
|
||||
for _, c := range candidates {
|
||||
if m.size <= m.capacity-int64(bytesNeeded) {
|
||||
break
|
||||
}
|
||||
|
||||
key := fi.Key
|
||||
|
||||
// Remove from LRU
|
||||
m.LRU.Remove(key)
|
||||
|
||||
// Remove from maps
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
|
||||
// Update size
|
||||
m.size -= fi.Size
|
||||
evicted += uint(fi.Size)
|
||||
|
||||
// Clean up key lock
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
key := c.key
|
||||
if liveFi, exists := m.info[key]; exists {
|
||||
m.LRU.Remove(key)
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
m.size -= liveFi.Size
|
||||
evicted += uint(liveFi.Size)
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Unlock()
|
||||
return evicted
|
||||
}
|
||||
|
||||
// EvictLFU evicts least frequently used files first (by AccessCount asc; P1-03 real LFU using existing field).
|
||||
// Ties broken by ATime (older first).
|
||||
// EvictLFU evicts least frequently used files first (by AccessCount ascending).
|
||||
// Ties broken by ATime (older first). Uses scalar snapshot under RLock + live re-fetch under WLock.
|
||||
func (m *MemoryFS) EvictLFU(bytesNeeded uint) uint {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var evicted uint
|
||||
var candidates []*types.FileInfo
|
||||
|
||||
// Collect all files
|
||||
for _, fi := range m.info {
|
||||
candidates = append(candidates, fi)
|
||||
m.mu.RLock()
|
||||
var candidates []struct {
|
||||
key string
|
||||
accessCount int
|
||||
aTime time.Time
|
||||
}
|
||||
for key, fi := range m.info {
|
||||
candidates = append(candidates, struct {
|
||||
key string
|
||||
accessCount int
|
||||
aTime time.Time
|
||||
}{key: key, accessCount: fi.AccessCount, aTime: fi.ATime})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Sort by access count asc (LFU), then older ATime for ties
|
||||
if len(candidates) == 0 {
|
||||
return 0
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
if candidates[i].AccessCount != candidates[j].AccessCount {
|
||||
return candidates[i].AccessCount < candidates[j].AccessCount
|
||||
if candidates[i].accessCount != candidates[j].accessCount {
|
||||
return candidates[i].accessCount < candidates[j].accessCount
|
||||
}
|
||||
return candidates[i].ATime.Before(candidates[j].ATime)
|
||||
return candidates[i].aTime.Before(candidates[j].aTime)
|
||||
})
|
||||
|
||||
// Evict until enough space
|
||||
for _, fi := range candidates {
|
||||
m.mu.Lock()
|
||||
var evicted uint
|
||||
for _, c := range candidates {
|
||||
if m.size <= m.capacity-int64(bytesNeeded) {
|
||||
break
|
||||
}
|
||||
|
||||
key := fi.Key
|
||||
|
||||
// Remove from LRU
|
||||
m.LRU.Remove(key)
|
||||
|
||||
// Remove from maps
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
|
||||
// Update size
|
||||
m.size -= fi.Size
|
||||
evicted += uint(fi.Size)
|
||||
|
||||
// Clean up key lock
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
key := c.key
|
||||
if liveFi, exists := m.info[key]; exists {
|
||||
m.LRU.Remove(key)
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
m.size -= liveFi.Size
|
||||
evicted += uint(liveFi.Size)
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Unlock()
|
||||
return evicted
|
||||
}
|
||||
|
||||
// EvictHybrid evicts using time-decayed score (recency + frequency from GetTimeDecayedScore; lower value first).
|
||||
// This makes "hybrid" a meaningful size+recency+freq policy (P1-03).
|
||||
// This makes "hybrid" a meaningful size + recency + frequency policy.
|
||||
// Snapshot fields under RLock,
|
||||
// compute score from snapshot in sort (avoids live pointer + time race post-unlock).
|
||||
func (m *MemoryFS) EvictHybrid(bytesNeeded uint) uint {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var evicted uint
|
||||
var candidates []*types.FileInfo
|
||||
|
||||
// Collect all files
|
||||
for _, fi := range m.info {
|
||||
candidates = append(candidates, fi)
|
||||
m.mu.RLock()
|
||||
var candidates []struct {
|
||||
key string
|
||||
accessCount int
|
||||
aTime time.Time
|
||||
}
|
||||
for key, fi := range m.info {
|
||||
candidates = append(candidates, struct {
|
||||
key string
|
||||
accessCount int
|
||||
aTime time.Time
|
||||
}{key: key, accessCount: fi.AccessCount, aTime: fi.ATime})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Sort by ascending decayed score (least valuable = evict first)
|
||||
if len(candidates) == 0 {
|
||||
return 0
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
return candidates[i].GetTimeDecayedScore() < candidates[j].GetTimeDecayedScore()
|
||||
// Compute from snapshot scalars using shared DecayedScore (single source of truth).
|
||||
scoreI := types.DecayedScore(candidates[i].aTime, candidates[i].accessCount)
|
||||
scoreJ := types.DecayedScore(candidates[j].aTime, candidates[j].accessCount)
|
||||
return scoreI < scoreJ
|
||||
})
|
||||
|
||||
// Evict until enough space
|
||||
for _, fi := range candidates {
|
||||
m.mu.Lock()
|
||||
var evicted uint
|
||||
for _, c := range candidates {
|
||||
if m.size <= m.capacity-int64(bytesNeeded) {
|
||||
break
|
||||
}
|
||||
|
||||
key := fi.Key
|
||||
|
||||
// Remove from LRU
|
||||
m.LRU.Remove(key)
|
||||
|
||||
// Remove from maps
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
|
||||
// Update size
|
||||
m.size -= fi.Size
|
||||
evicted += uint(fi.Size)
|
||||
|
||||
// Clean up key lock
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
key := c.key
|
||||
if liveFi, exists := m.info[key]; exists {
|
||||
m.LRU.Remove(key)
|
||||
delete(m.info, key)
|
||||
delete(m.data, key)
|
||||
m.size -= liveFi.Size
|
||||
evicted += uint(liveFi.Size)
|
||||
shardIndex := locks.GetShardIndex(key)
|
||||
m.keyLocks[shardIndex].Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Unlock()
|
||||
return evicted
|
||||
}
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMemoryFS_Basic(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := New(1024 * 1024)
|
||||
if m.Name() != "MemoryFS" {
|
||||
t.Error("bad name")
|
||||
}
|
||||
if m.Capacity() != 1024*1024 {
|
||||
t.Error("bad cap")
|
||||
}
|
||||
|
||||
w, err := m.Create("k1", 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, _ := w.Write(make([]byte, 100))
|
||||
w.Close()
|
||||
if n != 100 {
|
||||
t.Error("write len")
|
||||
}
|
||||
if m.Size() != 100 {
|
||||
t.Errorf("size=%d want 100", m.Size())
|
||||
}
|
||||
|
||||
r, err := m.Open("k1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, _ := io.ReadAll(r)
|
||||
r.Close()
|
||||
if len(data) != 100 {
|
||||
t.Error("read mismatch")
|
||||
}
|
||||
|
||||
if err := m.Delete("k1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := m.Open("k1"); err == nil {
|
||||
t.Error("deleted key still openable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryFS_EvictUnderPressure(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := New(500)
|
||||
// create 3x200 = 600 >500, should trigger internal? but direct evict call
|
||||
for i := 0; i < 3; i++ {
|
||||
w, _ := m.Create("f"+string(rune('0'+i)), 200)
|
||||
w.Write(make([]byte, 200))
|
||||
w.Close()
|
||||
}
|
||||
// force evict
|
||||
evicted := m.EvictLRU(100)
|
||||
if evicted == 0 || m.Size() > 500 {
|
||||
t.Errorf("evict failed: evicted=%d size=%d", evicted, m.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryFS_SizeNeverExceedsAfterEvict(t *testing.T) {
|
||||
t.Parallel()
|
||||
cap := int64(1000)
|
||||
m := New(cap)
|
||||
// Strengthened: cycle through strategies (randomized feel via mod), use testKey, stricter post-evict with documented epsilon (issue9)
|
||||
strats := []func(uint) uint{m.EvictLRU, func(n uint) uint { return m.EvictBySize(n, true) }, m.EvictFIFO, m.EvictLFU, m.EvictHybrid}
|
||||
for i := 0; i < 50; i++ { // more cycles
|
||||
sz := int64(100 + i%50)
|
||||
w, err := m.Create(testKey(i), sz)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write(make([]byte, sz))
|
||||
w.Close()
|
||||
// Raw MemoryFS allows temporary over (enforced by GCFS wrapper in real use).
|
||||
// Force evict under pressure and verify post-evict invariant.
|
||||
if m.Size() > cap-50 {
|
||||
fn := strats[i%len(strats)]
|
||||
fn(200)
|
||||
if m.Size() > cap+50 { // RLock snapshot + batch may temporarily exceed; GC layer enforces strict limit
|
||||
t.Fatalf("size %d >> cap %d after evict", m.Size(), cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryFS_ConcurrentCreateOpenDelete(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
}
|
||||
t.Parallel()
|
||||
m := New(10 * 1024 * 1024)
|
||||
var wg sync.WaitGroup
|
||||
const N = 50
|
||||
var ops int64
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < N; j++ {
|
||||
key := "c" + string(rune('a'+id)) + string(rune(j%10))
|
||||
w, err := m.Create(key, 128)
|
||||
if err == nil {
|
||||
w.Write(make([]byte, 128))
|
||||
w.Close()
|
||||
atomic.AddInt64(&ops, 1)
|
||||
}
|
||||
if r, err := m.Open(key); err == nil {
|
||||
io.Copy(io.Discard, r)
|
||||
r.Close()
|
||||
atomic.AddInt64(&ops, 1)
|
||||
}
|
||||
_ = m.Delete(key)
|
||||
atomic.AddInt64(&ops, 1)
|
||||
if j%10 == 0 {
|
||||
m.EvictLRU(256)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
if ops < 100 {
|
||||
t.Errorf("too few concurrent ops: %d", ops)
|
||||
}
|
||||
// size should be bounded
|
||||
if m.Size() > m.Capacity() {
|
||||
t.Errorf("final size %d > cap", m.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMemoryFS_CreateOpen(b *testing.B) {
|
||||
m := New(64 * 1024 * 1024)
|
||||
data := make([]byte, 4096)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
key := "b" + string(rune(i%1000))
|
||||
w, _ := m.Create(key, 4096)
|
||||
w.Write(data)
|
||||
w.Close()
|
||||
r, _ := m.Open(key)
|
||||
io.Copy(io.Discard, r)
|
||||
r.Close()
|
||||
_ = m.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEvictionUnderPressure(b *testing.B) {
|
||||
m := New(1 * 1024 * 1024)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// fill then evict (setup fill not timed separately to keep bench focused on pressure+evict cycle)
|
||||
for j := 0; j < 20; j++ {
|
||||
w, _ := m.Create("e"+string(rune(j)), 64*1024)
|
||||
w.Write(make([]byte, 64*1024))
|
||||
w.Close()
|
||||
}
|
||||
m.EvictLRU(512 * 1024)
|
||||
}
|
||||
_ = m // keep
|
||||
}
|
||||
|
||||
func TestMemoryFS_Stats(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := New(1024)
|
||||
stats := m.GetFragmentationStats()
|
||||
if stats["buffer_count"] != 0 {
|
||||
t.Error("initial buffers >0?")
|
||||
}
|
||||
}
|
||||
|
||||
// testKey helper (addresses brittle keygen nit21 across tests).
|
||||
func testKey(i int) string {
|
||||
return fmt.Sprintf("test/key/%04d", i)
|
||||
}
|
||||
|
||||
// TestMemoryFS_ConcurrentCloseAndEvict_RaceFree is a synthetic load test exercising concurrent Close during eviction (validates the R/W split fixes).
|
||||
// Exercises overlapping writer Close() (mutates fi.Size under W) + all Evict* strategies under load.
|
||||
// Must be -race clean; also strengthens property coverage.
|
||||
func TestMemoryFS_ConcurrentCloseAndEvict_RaceFree(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
}
|
||||
t.Parallel()
|
||||
m := New(2 * 1024 * 1024) // 2MB
|
||||
var wg sync.WaitGroup
|
||||
stopCh := make(chan struct{})
|
||||
const writers = 3
|
||||
const evictors = 3
|
||||
|
||||
// Writers: create + write + close (triggers size mutation in Close)
|
||||
for i := 0; i < writers; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; ; j++ {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
key := testKey(id*10000 + j)
|
||||
w, err := m.Create(key, 4096)
|
||||
if err == nil {
|
||||
w.Write(make([]byte, 4096))
|
||||
w.Close() // mutates live *FileInfo.Size + global size (race target)
|
||||
}
|
||||
if j%5 == 0 {
|
||||
m.Delete(key)
|
||||
}
|
||||
if j > 100 {
|
||||
break // bound per writer
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Evictors: hammer all 5 strategies + LRU (exercises snapshot copy + live re-fetch + short LRU Lock)
|
||||
strats := []func(uint) uint{
|
||||
m.EvictLRU,
|
||||
func(n uint) uint { return m.EvictBySize(n, true) },
|
||||
func(n uint) uint { return m.EvictBySize(n, false) },
|
||||
m.EvictFIFO,
|
||||
m.EvictLFU,
|
||||
m.EvictHybrid,
|
||||
}
|
||||
for i := 0; i < evictors; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; ; j++ {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
s := strats[j%len(strats)]
|
||||
s(1024)
|
||||
if j > 50 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
time.Sleep(150 * time.Millisecond) // load duration; bounded
|
||||
close(stopCh)
|
||||
wg.Wait()
|
||||
|
||||
// Post-run invariants (loose due to raw MemoryFS overcommit design; GCFS enforces)
|
||||
if m.Size() < 0 {
|
||||
t.Error("negative size after concurrent close+evict")
|
||||
}
|
||||
// LRU len reasonable
|
||||
_ = m.LRU.Len()
|
||||
}
|
||||
|
||||
func TestMemoryFS_EvictVariantsAndErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := New(800)
|
||||
// populate
|
||||
for i := 0; i < 4; i++ {
|
||||
w, _ := m.Create("ev"+string(rune('0'+i)), 150)
|
||||
w.Write(make([]byte, 150))
|
||||
w.Close()
|
||||
}
|
||||
_ = m.EvictBySize(100, true) // smallest
|
||||
_ = m.EvictFIFO(50)
|
||||
_ = m.EvictLFU(50)
|
||||
_ = m.EvictHybrid(50)
|
||||
|
||||
// invalid keys
|
||||
if _, err := m.Create("", 1); err == nil {
|
||||
t.Error("empty key allowed")
|
||||
}
|
||||
if _, err := m.Create("/abs", 1); err == nil {
|
||||
t.Error("abs key allowed")
|
||||
}
|
||||
if _, err := m.Create("..bad", 1); err == nil {
|
||||
t.Error("traversal key allowed")
|
||||
}
|
||||
if _, err := m.Open("nope"); err == nil {
|
||||
t.Error("open missing")
|
||||
}
|
||||
if err := m.Delete("nope"); err == nil {
|
||||
t.Error("delete missing")
|
||||
}
|
||||
if _, err := m.Stat("nope"); err == nil {
|
||||
t.Error("stat missing")
|
||||
}
|
||||
// overwrite path + actual size update via closer
|
||||
w2, _ := m.Create("ow", 10)
|
||||
w2.Write([]byte{1, 2, 3})
|
||||
w2.Close() // updates to real 3
|
||||
if fi, _ := m.Stat("ow"); fi.Size != 3 {
|
||||
t.Errorf("overwrite size %d !=3", fi.Size)
|
||||
}
|
||||
// hit fragmentation stats after activity
|
||||
_ = m.GetFragmentationStats()
|
||||
}
|
||||
|
||||
func TestMemoryFS_AllEvictStrategies(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := New(300)
|
||||
for i := 0; i < 3; i++ {
|
||||
w, _ := m.Create("s"+string(rune(i)), 120)
|
||||
w.Write(make([]byte, 120))
|
||||
w.Close()
|
||||
}
|
||||
_ = m.EvictBySize(50, true)
|
||||
_ = m.EvictBySize(50, false)
|
||||
_ = m.EvictFIFO(20)
|
||||
_ = m.EvictLFU(20)
|
||||
_ = m.EvictHybrid(20)
|
||||
if m.Size() > m.Capacity() {
|
||||
t.Error("post variant evict over cap")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user