package filer import ( "bytes" "container/list" "context" "errors" "sync" "time" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/wdclient" "golang.org/x/sync/semaphore" "golang.org/x/sync/singleflight" ) const ( // persistedLogCacheMaxBytes bounds retained entries regardless of subscriber count. persistedLogCacheMaxBytes = 256 << 20 // persistedLogCacheLoadBudget bounds in-flight fetch+decode bytes, charged // by chunk size: small chunks load wide, full-size ones cap the peak. persistedLogCacheLoadBudget = 128 << 20 // persistedLogCacheIdleTTL frees entries no replay has touched recently, so // the cache holds memory only while subscribers actually replay. persistedLogCacheIdleTTL = 5 * time.Minute // maxLogEntrySize guards the per-entry allocation against a corrupt size prefix. maxLogEntrySize = 1 << 30 ) // errLogChunkIncomplete reports a chunk that does not start and end on record // boundaries; the file is then only readable as a whole byte stream. var errLogChunkIncomplete = errors.New("log chunk does not hold whole records") // persistedLogCache shares decoded metadata-log chunks across concurrent // SubscribeMetadata replays. Chunks are immutable (each log flush uploads one // whole buffer of complete records as a new chunk), so even the actively // written current file shares its flushed chunks. Cached entries are shared // read-only; callers must not mutate them. type persistedLogCache struct { mu sync.Mutex ll *list.List // front = most recently used; values are *logCacheItem index map[string]*list.Element curBytes int64 maxBytes int64 sf singleflight.Group loadSem *semaphore.Weighted } type logCacheItem struct { key string // chunk file id entries []*filer_pb.LogEntry bytes int64 lastUsed time.Time } func newPersistedLogCache(maxBytes int64) *persistedLogCache { c := &persistedLogCache{ ll: list.New(), index: make(map[string]*list.Element), maxBytes: maxBytes, loadSem: semaphore.NewWeighted(persistedLogCacheLoadBudget), } // the filer's cache lives for the process lifetime go c.loopEvictIdle() return c } func (c *persistedLogCache) loopEvictIdle() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for range ticker.C { c.evictIdle(time.Now().Add(-persistedLogCacheIdleTTL)) } } // evictIdle drops every entry last used at or before cutoff. Recency order // makes the idle entries exactly the tail of the LRU list. func (c *persistedLogCache) evictIdle(cutoff time.Time) { c.mu.Lock() defer c.mu.Unlock() for c.ll.Len() > 0 { el := c.ll.Back() if el.Value.(*logCacheItem).lastUsed.After(cutoff) { break } c.removeElement(el) } } type logLoadResult struct { entries []*filer_pb.LogEntry err error } // getOrLoad returns the decoded entries for a chunk, loading once on miss and // coalescing concurrent misses. Only a clean, complete decode is cached: a // chunk-not-found read must be re-probed on later replays, and an incomplete // chunk stays with the streaming fallback. func (c *persistedLogCache) getOrLoad(fileId string, loadBytes int64, load func() ([]*filer_pb.LogEntry, bool, error)) ([]*filer_pb.LogEntry, error) { if entries, ok := c.lookup(fileId); ok { return entries, nil } v, _, _ := c.sf.Do(fileId, func() (interface{}, error) { if entries, ok := c.lookup(fileId); ok { return logLoadResult{entries: entries}, nil } entries, cacheable, loadErr := c.loadGuarded(loadBytes, load) if loadErr == nil && cacheable { c.store(fileId, entries) } return logLoadResult{entries: entries, err: loadErr}, nil }) res := v.(logLoadResult) return res.entries, res.err } func (c *persistedLogCache) loadGuarded(loadBytes int64, load func() ([]*filer_pb.LogEntry, bool, error)) ([]*filer_pb.LogEntry, bool, error) { weight := loadBytes if weight < 1 { weight = 1 } if weight > persistedLogCacheLoadBudget { // never exceeds the semaphore size, or the acquire could not succeed weight = persistedLogCacheLoadBudget } if err := c.loadSem.Acquire(context.Background(), weight); err != nil { return nil, false, err } defer c.loadSem.Release(weight) return load() } func (c *persistedLogCache) lookup(fileId string) ([]*filer_pb.LogEntry, bool) { c.mu.Lock() defer c.mu.Unlock() el, ok := c.index[fileId] if !ok { return nil, false } c.ll.MoveToFront(el) item := el.Value.(*logCacheItem) item.lastUsed = time.Now() return item.entries, true } func (c *persistedLogCache) store(fileId string, entries []*filer_pb.LogEntry) { bytes := estimateEntriesBytes(entries) if bytes > c.maxBytes { // would evict everything else and still not fit; serve unretained return } c.mu.Lock() defer c.mu.Unlock() if el, ok := c.index[fileId]; ok { c.removeElement(el) } el := c.ll.PushFront(&logCacheItem{key: fileId, entries: entries, bytes: bytes, lastUsed: time.Now()}) c.index[fileId] = el c.curBytes += bytes for c.curBytes > c.maxBytes && c.ll.Len() > 1 { c.removeElement(c.ll.Back()) } } // removeElement drops an element from both the list and the index. Caller holds mu. func (c *persistedLogCache) removeElement(el *list.Element) { item := el.Value.(*logCacheItem) c.ll.Remove(el) delete(c.index, item.key) c.curBytes -= item.bytes } // estimateEntriesBytes is deliberately generous so curBytes does not run under // the real retained heap. func estimateEntriesBytes(entries []*filer_pb.LogEntry) int64 { total := int64(len(entries)) * 128 for _, e := range entries { total += int64(len(e.Data)+len(e.Key)) + 16 } return total } // loadLogFileEntries reads one log file chunk from volume servers and decodes // its records. fetchWholeChunk handles lookup, retries, cipher and gzip. func loadLogFileEntries(masterClient *wdclient.MasterClient, chunk *filer_pb.FileChunk) (entries []*filer_pb.LogEntry, cacheable bool, err error) { bytesBuffer := bytesBufferPool.Get().(*bytes.Buffer) bytesBuffer.Reset() defer bytesBufferPool.Put(bytesBuffer) lookupFileIdFn := func(ctx context.Context, fileId string) (targetUrls []string, err error) { return masterClient.LookupFileId(ctx, fileId) } if fetchErr := fetchWholeChunk(context.Background(), bytesBuffer, lookupFileIdFn, chunk.GetFileIdString(), chunk.CipherKey, chunk.IsCompressed); fetchErr != nil { return nil, false, fetchErr } return decodeLogRecords(bytesBuffer.Bytes()) } // decodeLogRecords parses size-prefixed LogEntry records. A buffer that stops // mid-record, or whose size prefix is garbage (also the symptom of starting // mid-record), reports errLogChunkIncomplete with the cleanly decoded prefix. // Since UnmarshalVT is permissive enough to accept misaligned bytes, // records must also satisfy the writer's invariants: never empty, a positive // timestamp, and strictly increasing within one flushed buffer. // UnmarshalVT copies all bytes, so the entries do not alias data. func decodeLogRecords(data []byte) (entries []*filer_pb.LogEntry, cacheable bool, err error) { var lastTsNs int64 for pos := 0; pos < len(data); { if pos+4 > len(data) { return entries, false, errLogChunkIncomplete } size32 := util.BytesToUint32(data[pos : pos+4]) if size32 == 0 || size32 > maxLogEntrySize { return entries, false, errLogChunkIncomplete } size := int(size32) if pos+4+size > len(data) { return entries, false, errLogChunkIncomplete } logEntry := &filer_pb.LogEntry{} if unmarshalErr := logEntry.UnmarshalVT(data[pos+4 : pos+4+size]); unmarshalErr != nil { return entries, false, errLogChunkIncomplete } if logEntry.TsNs <= lastTsNs { return entries, false, errLogChunkIncomplete } lastTsNs = logEntry.TsNs entries = append(entries, logEntry) pos += 4 + size } return entries, true, nil }