mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 08:18:42 +01:00
* filer: cancel a chunk download that no reader waits for * filer: stop waiting for reader cache budget once cancelled * filer: keep a cancelled download's re-lookups off its context The refresh and retry lookups took the download's context. FilerClient coalesces concurrent lookups of a volume, so a cancelled download leading one failed the other readers waiting on it. Fetches still stop on cancel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: test that cancelling a download leaves its re-lookup running Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: cancel unheld downloads when the reader cache is destroyed ReaderCache.destroy waited for every downloader, so closing a reader waited for a stalled prefetch nobody read. Downloads with no reader or stream on them are now cancelled first; ones still in use keep running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: test that destroying the reader cache cancels an unread prefetch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: cancel a destroyed download once its last holder leaves destroy skipped downloads a reader or stream still held, and once they were out of the map nothing cancelled them, so destroy waited for a fetch nobody used. removeConsumedLocked now cancels any consumable download, whether or not it is still in the map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: test a destroyed download cancelled after its last stream leaves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: release the stream only after destroy dropped the download Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: test a cancelled stream leaving a waiting reader's download running Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
3.4 KiB
Go
142 lines
3.4 KiB
Go
package filer
|
|
|
|
import (
|
|
"container/list"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util/mem"
|
|
)
|
|
|
|
const DefaultReaderCacheMemoryLimit = 256 << 20
|
|
|
|
type ReaderCacheBudget struct {
|
|
sync.Mutex
|
|
limit int64
|
|
used int64
|
|
reservations map[*SingleChunkCacher]int64
|
|
idle list.List
|
|
idleEntries map[*SingleChunkCacher]*list.Element
|
|
changed chan struct{}
|
|
}
|
|
|
|
func NewReaderCacheBudget(limit int64) *ReaderCacheBudget {
|
|
if limit <= 0 {
|
|
limit = DefaultReaderCacheMemoryLimit
|
|
}
|
|
return &ReaderCacheBudget{
|
|
limit: limit,
|
|
reservations: make(map[*SingleChunkCacher]int64),
|
|
idleEntries: make(map[*SingleChunkCacher]*list.Element),
|
|
changed: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (b *ReaderCacheBudget) reserve(s *SingleChunkCacher) error {
|
|
if s.chunkSize < 0 {
|
|
return fmt.Errorf("invalid chunk size %d", s.chunkSize)
|
|
}
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
size := int64(mem.AllocationSize(s.chunkSize))
|
|
if size > b.limit {
|
|
return fmt.Errorf("chunk buffer needs %d bytes, exceeding reader cache budget %d; increase the readerCacheSizeMB budget", size, b.limit)
|
|
}
|
|
for {
|
|
// a cancelled download must not evict a chunk for itself
|
|
if err := s.ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
b.Lock()
|
|
if size <= b.limit-b.used {
|
|
b.used += size
|
|
b.reservations[s] = size
|
|
b.Unlock()
|
|
return nil
|
|
}
|
|
// Prefer evicting an idle chunk no stream is positioned in; fall back
|
|
// to the oldest pinned one so abandoned pins cannot block the budget.
|
|
var victim *SingleChunkCacher
|
|
var entry *list.Element
|
|
pinnedVictim := false
|
|
for e := b.idle.Front(); e != nil; e = e.Next() {
|
|
c := e.Value.(*SingleChunkCacher)
|
|
if atomic.LoadInt32(&c.pins) == 0 {
|
|
victim, entry = c, e
|
|
pinnedVictim = false
|
|
break
|
|
}
|
|
if victim == nil {
|
|
victim, entry = c, e
|
|
pinnedVictim = true
|
|
}
|
|
}
|
|
if entry != nil {
|
|
b.idle.Remove(entry)
|
|
delete(b.idleEntries, victim)
|
|
b.Unlock()
|
|
if pinnedVictim {
|
|
victim.parent.remove(victim)
|
|
} else if !victim.parent.removeUnpinned(victim) {
|
|
// The victim was pinned between selection and removal: keep it
|
|
// evictable so a pin abandoned in that gap cannot wedge the
|
|
// budget, then retry the selection.
|
|
b.Lock()
|
|
if _, ok := b.reservations[victim]; ok && b.idleEntries[victim] == nil {
|
|
b.idleEntries[victim] = b.idle.PushBack(victim)
|
|
}
|
|
b.Unlock()
|
|
}
|
|
continue
|
|
}
|
|
changed := b.changed
|
|
b.Unlock()
|
|
select {
|
|
case <-changed:
|
|
case <-s.ctx.Done():
|
|
return s.ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// canFit reports whether a whole-chunk buffer of this size can ever be
|
|
// reserved. A chunk bigger than the budget cannot be read through the
|
|
// whole-chunk path at all, so callers must fall back to range fetches.
|
|
func (b *ReaderCacheBudget) canFit(size int) bool {
|
|
return b == nil || int64(mem.AllocationSize(size)) <= b.limit
|
|
}
|
|
|
|
func (b *ReaderCacheBudget) complete(s *SingleChunkCacher) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
if _, found := b.reservations[s]; found && b.idleEntries[s] == nil {
|
|
b.idleEntries[s] = b.idle.PushBack(s)
|
|
close(b.changed)
|
|
b.changed = make(chan struct{})
|
|
}
|
|
}
|
|
|
|
func (b *ReaderCacheBudget) release(s *SingleChunkCacher) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
if size, found := b.reservations[s]; found {
|
|
b.used -= size
|
|
delete(b.reservations, s)
|
|
if entry := b.idleEntries[s]; entry != nil {
|
|
b.idle.Remove(entry)
|
|
delete(b.idleEntries, s)
|
|
}
|
|
close(b.changed)
|
|
b.changed = make(chan struct{})
|
|
}
|
|
}
|