package meta_cache import ( "context" "errors" "math" "os" "sync" "time" "golang.org/x/sync/singleflight" "fmt" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/filer/leveldb" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/util" "google.golang.org/protobuf/proto" ) // need to have logic similar to FilerStoreWrapper // e.g. fill fileId field for chunks type MetaCache struct { root util.FullPath localStore filer.VirtualFilerStore leveldbStore *leveldb.LevelDBStore // direct reference for batch operations sync.RWMutex uidGidMapper *UidGidMapper markCachedFn func(fullpath util.FullPath) isCachedFn func(fullpath util.FullPath) bool invalidateFunc func(EntryInvalidation) onDirectoryUpdate func(dir util.FullPath) pinnedChildFn func(*filer.Entry) bool // a child a rebuild must not drop (local-only, not yet on the filer); nil disables visitGroup singleflight.Group // deduplicates concurrent EnsureVisited calls for the same path applyCh chan metadataApplyRequest applyDone chan struct{} applyStateMu sync.Mutex applyClosed bool buildingDirs map[util.FullPath]*directoryBuildState dedupRing dedupRingBuffer includeSystemEntries bool // oversizedDirs are directories the mount refused to cache for their size. // Their listings read through to the filer, and a later visit fails fast // instead of streaming to the limit again to rediscover them. oversizedDirs map[util.FullPath]struct{} // dirVersionFloors is each cached directory's listing snapshot: the // version of every child the listing covered, present or absent, unless // a later event gave that child its own record. One map write per build // instead of a record per child. dirVersionFloors map[util.FullPath]int64 // dirSections is each cached directory's listing split into name-range // sections, so a churn burst invalidates one section instead of the // whole listing. See meta_cache_sections.go. dirSections map[util.FullPath]*sectionList // Entry invalidations run on a worker, not inline on the apply loop: // invalidateFunc takes the fh lock, which a flush can hold while waiting on // the apply loop (flushMetadataToFiler -> applyLocalMetadataEvent), so inline // invalidation deadlocks the mount. invalidateWorker *util.AsyncBatchWorker[EntryInvalidation] } var errMetaCacheClosed = errors.New("metadata cache is shut down") type MetadataResponseApplyOptions struct { NotifyDirectories bool InvalidateEntries bool } var ( LocalMetadataResponseApplyOptions = MetadataResponseApplyOptions{ NotifyDirectories: true, } SubscriberMetadataResponseApplyOptions = MetadataResponseApplyOptions{ NotifyDirectories: true, InvalidateEntries: true, } ) type directoryBuildState struct { bufferedEvents []*filer_pb.SubscribeMetadataResponse } const recentEventDedupWindow = 4096 type metadataApplyRequestKind int const ( metadataApplyEvent metadataApplyRequestKind = iota metadataBeginBuild metadataCompleteBuild metadataAbortBuild metadataPurgeDir metadataSectionRefresh metadataShutdown ) type metadataApplyRequest struct { ctx context.Context kind metadataApplyRequestKind resp *filer_pb.SubscribeMetadataResponse options MetadataResponseApplyOptions buildPath util.FullPath snapshotTsNs int64 sections *sectionList // the completed build's section table refresh *sectionRefresh // one section re-listing to reconcile resetFn func() done chan error } func NewMetaCache(dbFolder string, uidGidMapper *UidGidMapper, root util.FullPath, includeSystemEntries bool, markCachedFn func(path util.FullPath), isCachedFn func(path util.FullPath) bool, invalidateFunc func(EntryInvalidation), onDirectoryUpdate func(dir util.FullPath)) *MetaCache { leveldbStore, virtualStore := openMetaStore(dbFolder) mc := &MetaCache{ root: root, localStore: virtualStore, leveldbStore: leveldbStore, markCachedFn: markCachedFn, isCachedFn: isCachedFn, uidGidMapper: uidGidMapper, onDirectoryUpdate: onDirectoryUpdate, includeSystemEntries: includeSystemEntries, invalidateFunc: invalidateFunc, applyCh: make(chan metadataApplyRequest, 128), applyDone: make(chan struct{}), buildingDirs: make(map[util.FullPath]*directoryBuildState), dedupRing: newDedupRingBuffer(), dirVersionFloors: make(map[util.FullPath]int64), dirSections: make(map[util.FullPath]*sectionList), oversizedDirs: make(map[util.FullPath]struct{}), } mc.invalidateWorker = util.NewAsyncBatchWorker(func(batch []EntryInvalidation) { for _, invalidation := range batch { mc.invalidateFunc(invalidation) } }) go mc.runApplyLoop() return mc } func openMetaStore(dbFolder string) (*leveldb.LevelDBStore, filer.VirtualFilerStore) { os.RemoveAll(dbFolder) os.MkdirAll(dbFolder, 0755) store := &leveldb.LevelDBStore{} config := &cacheConfig{ dir: dbFolder, } if err := store.Initialize(config, ""); err != nil { glog.Fatalf("Failed to initialize metadata cache store for %s: %+v", store.GetName(), err) } return store, filer.NewFilerStoreWrapper(store) } // InsertEntry stores an entry at the filer log position it reflects. Zero means // local content no log position describes, which records that explicitly so the // entry does not inherit its directory's listing floor. func (mc *MetaCache) InsertEntry(ctx context.Context, entry *filer.Entry, versionTsNs int64) error { mc.Lock() defer mc.Unlock() return mc.doInsertEntry(ctx, entry, versionTsNs) } func (mc *MetaCache) doInsertEntry(ctx context.Context, entry *filer.Entry, versionTsNs int64) error { if err := mc.localStore.InsertEntry(ctx, entry); err != nil { return err } mc.setEntryVersionLocked(ctx, entry.FullPath, versionTsNs) return nil } // doBatchInsertEntries inserts multiple entries using LevelDB's batch write. // This is more efficient than inserting entries one by one. func (mc *MetaCache) doBatchInsertEntries(ctx context.Context, entries []*filer.Entry) error { return mc.leveldbStore.BatchInsertEntries(ctx, entries) } func (mc *MetaCache) AtomicUpdateEntryFromFiler(ctx context.Context, oldPath util.FullPath, newEntry *filer.Entry) error { mc.Lock() defer mc.Unlock() return mc.atomicUpdateEntryFromFilerLocked(ctx, oldPath, newEntry, false, 0) } func (mc *MetaCache) atomicUpdateEntryFromFilerLocked(ctx context.Context, oldPath util.FullPath, newEntry *filer.Entry, allowUncachedInsert bool, versionTsNs int64) error { entry, err := mc.localStore.FindEntry(ctx, oldPath) if err != nil && err != filer_pb.ErrNotFound { glog.Errorf("Metacache: find entry error: %v", err) return err } vacatingOldPath := oldPath != "" && !(newEntry != nil && oldPath == newEntry.FullPath) if entry != nil && vacatingOldPath { ctx = context.WithValue(ctx, "OP", "MV") glog.V(3).Infof("DeleteEntry %s", oldPath) if err := mc.localStore.DeleteEntry(ctx, oldPath); err != nil { return err } } if vacatingOldPath { // A deletion is a fact about the path with no entry left to carry it: // without a tombstone a delayed older event resurrects it, and the // deletion's own redelivery is dedup-suppressed. Only for cached // parents — an uncached one gates its own inserts, so a tombstone // there would just accumulate. oldDir, _ := oldPath.DirAndName() if versionTsNs != 0 && (allowUncachedInsert || mc.isCachedFn(util.FullPath(oldDir))) { mc.setEntryTombstoneLocked(ctx, oldPath, versionTsNs) } else { mc.clearEntryVersionLocked(ctx, oldPath) } } if newEntry != nil { newDir, _ := newEntry.DirAndName() if allowUncachedInsert || mc.isCachedFn(util.FullPath(newDir)) { glog.V(3).Infof("InsertEntry %s/%s", newDir, newEntry.Name()) if err := mc.localStore.InsertEntry(ctx, newEntry); err != nil { return err } mc.setEntryVersionLocked(ctx, newEntry.FullPath, versionTsNs) } } return nil } func (mc *MetaCache) shouldHideEntry(fullpath util.FullPath) bool { if mc.includeSystemEntries { return false } dir, name := fullpath.DirAndName() return IsHiddenSystemEntry(dir, name) } func (mc *MetaCache) purgeEntryLocked(ctx context.Context, fullpath util.FullPath, isDirectory bool) error { if fullpath == "" { return nil } if err := mc.localStore.DeleteEntry(ctx, fullpath); err != nil { return err } if isDirectory { if err := mc.localStore.DeleteFolderChildren(ctx, fullpath); err != nil { return err } } return nil } func (mc *MetaCache) ApplyMetadataResponse(ctx context.Context, resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) error { if resp == nil || resp.EventNotification == nil { return nil } clonedResp := proto.Clone(resp).(*filer_pb.SubscribeMetadataResponse) return mc.applyMetadataResponseEnqueue(ctx, clonedResp, options) } // ApplyMetadataResponseOwned is like ApplyMetadataResponse but takes ownership // of resp without cloning. The caller must not use resp after this call. func (mc *MetaCache) ApplyMetadataResponseOwned(ctx context.Context, resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) error { if resp == nil || resp.EventNotification == nil { return nil } return mc.applyMetadataResponseEnqueue(ctx, resp, options) } // ApplyMetadataResponseOwnedAsync enqueues resp without waiting, for callers holding a // lock the apply loop's invalidateFunc also needs. Best-effort: the subscription re-delivers. func (mc *MetaCache) ApplyMetadataResponseOwnedAsync(resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) { if resp == nil || resp.EventNotification == nil { return } req := metadataApplyRequest{ ctx: context.Background(), kind: metadataApplyEvent, resp: resp, options: options, done: make(chan error, 1), } select { case mc.applyCh <- req: default: } } func (mc *MetaCache) applyMetadataResponseEnqueue(ctx context.Context, resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) error { if ctx == nil { ctx = context.Background() } req := metadataApplyRequest{ // Use a non-cancellable context for the queued mutation so a // cancelled caller doesn't abort the apply loop mid-write. ctx: context.Background(), kind: metadataApplyEvent, resp: resp, options: options, done: make(chan error, 1), } if err := mc.enqueueApplyRequest(req); err != nil { return err } select { case err := <-req.done: return err case <-ctx.Done(): return ctx.Err() } } func (mc *MetaCache) BeginDirectoryBuild(ctx context.Context, dirPath util.FullPath) error { return mc.enqueueAndWait(ctx, metadataApplyRequest{ kind: metadataBeginBuild, buildPath: dirPath, }) } func (mc *MetaCache) CompleteDirectoryBuild(ctx context.Context, dirPath util.FullPath, snapshotTsNs int64, sectionBounds []string) error { return mc.enqueueAndWait(ctx, metadataApplyRequest{ kind: metadataCompleteBuild, buildPath: dirPath, snapshotTsNs: snapshotTsNs, sections: newSectionTable(sectionBounds), }) } func (mc *MetaCache) AbortDirectoryBuild(ctx context.Context, dirPath util.FullPath) error { return mc.enqueueAndWait(ctx, metadataApplyRequest{ kind: metadataAbortBuild, buildPath: dirPath, }) } // PurgeDirectoryChildren asynchronously clears a directory's cached children and // resets its cached flag (resetFn) via the apply loop. Asynchronous so callers // like kernel Forget don't block; see purgeDirectoryChildrenNow for why off-loop // callers must route through here rather than wiping the store directly. func (mc *MetaCache) PurgeDirectoryChildren(dirPath util.FullPath, resetFn func()) { _ = mc.enqueueApplyRequest(metadataApplyRequest{ ctx: context.Background(), kind: metadataPurgeDir, buildPath: dirPath, resetFn: resetFn, done: make(chan error, 1), }) } func (mc *MetaCache) UpdateEntry(ctx context.Context, entry *filer.Entry) error { mc.Lock() defer mc.Unlock() if err := mc.localStore.UpdateEntry(ctx, entry); err != nil { return err } mc.markEntryUnversionedLocked(ctx, entry.FullPath) return nil } // TouchDirMtimeCtime updates the mtime and ctime of a directory entry // directly in the local metadata cache store. This avoids a filer RPC // round-trip and the associated metadata event that would invalidate // recently cached child entries. func (mc *MetaCache) TouchDirMtimeCtime(ctx context.Context, dirPath util.FullPath, now time.Time) error { mc.Lock() defer mc.Unlock() entry, err := mc.localStore.FindEntry(ctx, dirPath) if err != nil { return err } if entry == nil { return nil } entry.Attr.Mtime = now entry.Attr.Ctime = now if err := mc.localStore.UpdateEntry(ctx, entry); err != nil { return err } mc.markEntryUnversionedLocked(ctx, dirPath) return nil } // FindEntry returns the entry together with the filer log position it // reflects: the position of the write that produced it, its directory's // listing snapshot when the listing covered it, or zero for local content no // log position describes. func (mc *MetaCache) FindEntry(ctx context.Context, fp util.FullPath) (entry *filer.Entry, versionTsNs int64, err error) { mc.RLock() defer mc.RUnlock() entry, err = mc.localStore.FindEntry(ctx, fp) if err != nil { return nil, 0, err } if isTtlExpired(entry) { return nil, 0, filer_pb.ErrNotFound } mc.mapIdFromFilerToLocal(entry) recordTsNs, _, unversioned := mc.entryVersionRecordLocked(ctx, fp) if unversioned { return entry, 0, nil } return entry, mc.entryVersionFloorLocked(fp, recordTsNs), nil } // entryVersionKeyPrefix namespaces per-entry version records apart from entry // keys. Keys encode parent-NUL-name, so a directory's direct children form one // contiguous range: pruning scans exactly them, never a subtree. const ( entryVersionTombstone = 1 // the path was deleted at the recorded position entryVersionUnversioned = 2 // local content no log position describes ) const entryVersionKeyPrefix = "\x00mount.entry.ver\x00" func entryVersionKey(fp util.FullPath) []byte { dir, name := fp.DirAndName() return []byte(entryVersionKeyPrefix + dir + "\x00" + name) } func entryVersionChildPrefix(dirPath util.FullPath) []byte { return []byte(entryVersionKeyPrefix + string(dirPath) + "\x00") } // setEntryVersionLocked records the log position an entry write reflects. // Zero (an unversioned local write) clears the claim: the new content is not // proven at the old position. func (mc *MetaCache) setEntryVersionLocked(ctx context.Context, fp util.FullPath, tsNs int64) { if tsNs == 0 { mc.markEntryUnversionedLocked(ctx, fp) return } value := make([]byte, 8) util.Uint64toBytes(value, uint64(tsNs)) if err := mc.localStore.KvPut(ctx, entryVersionKey(fp), value); err != nil { glog.V(1).Infof("set entry version %s: %v", fp, err) } } // markEntryUnversionedLocked records that a local write replaced an entry's // content with state no log position describes. Distinct from having no // record at all: a path with no record is one the directory listing covered, // so the listing snapshot is its version, while this content is not covered // by anything and must not inherit that floor. func (mc *MetaCache) markEntryUnversionedLocked(ctx context.Context, fp util.FullPath) { value := make([]byte, 9) value[8] = entryVersionUnversioned if err := mc.localStore.KvPut(ctx, entryVersionKey(fp), value); err != nil { glog.V(1).Infof("mark entry unversioned %s: %v", fp, err) } } // setEntryTombstoneLocked records a versioned deletion. The ninth byte marks // a tombstone, which fences even with no entry present. // // Lifetime: a recreate at the same name overwrites the key, so repeating names // are self-limiting; distinct names (unique temp files, rotated logs) each keep // a record until the directory is rebuilt or evicted, which prunes everything // at or below the new snapshot. The ceiling is therefore the number of // distinct names deleted in a cached directory since its last listing. func (mc *MetaCache) setEntryTombstoneLocked(ctx context.Context, fp util.FullPath, tsNs int64) { value := make([]byte, 9) util.Uint64toBytes(value, uint64(tsNs)) value[8] = entryVersionTombstone if err := mc.localStore.KvPut(ctx, entryVersionKey(fp), value); err != nil { glog.V(1).Infof("set entry tombstone %s: %v", fp, err) } } func (mc *MetaCache) clearEntryVersionLocked(ctx context.Context, fp util.FullPath) { if err := mc.localStore.KvDelete(ctx, entryVersionKey(fp)); err != nil { glog.V(4).Infof("clear entry version %s: %v", fp, err) } } func (mc *MetaCache) getEntryVersionRecordLocked(ctx context.Context, fp util.FullPath) (tsNs int64, tombstone bool) { tsNs, tombstone, _ = mc.entryVersionRecordLocked(ctx, fp) return } func (mc *MetaCache) entryVersionRecordLocked(ctx context.Context, fp util.FullPath) (tsNs int64, tombstone, unversioned bool) { value, err := mc.localStore.KvGet(ctx, entryVersionKey(fp)) if err != nil || len(value) < 8 { return 0, false, false } if len(value) == 9 { tombstone = value[8] == entryVersionTombstone unversioned = value[8] == entryVersionUnversioned } return int64(util.BytesToUint64(value[:8])), tombstone, unversioned } // entryVersionBlocksLocked reports whether a write at tsNs is already // reflected at fp. The path's version is its own record or, lacking one, the // listing floors — which cover names a listing saw present and absent alike. // A tombstone fences with no entry present, and a later floor outranks it; a // plain record only counts while its entry exists: records linger after a // bulk folder wipe and must not fence a recreate. func (mc *MetaCache) entryVersionBlocksLocked(ctx context.Context, fp util.FullPath, tsNs int64) bool { recordTsNs, tombstone, unversioned := mc.entryVersionRecordLocked(ctx, fp) if unversioned { // Local content no log position describes: fence nothing, so any // event can correct it. return false } if !tombstone && !mc.entryExistsLocked(ctx, fp) { recordTsNs = 0 } return mc.entryVersionFloorLocked(fp, recordTsNs) >= tsNs } // entryExistsLocked reports whether fp has a live entry, applying the same TTL // expiry the read path does so both agree on what "exists" means. func (mc *MetaCache) entryExistsLocked(ctx context.Context, fp util.FullPath) bool { entry, err := mc.localStore.FindEntry(ctx, fp) if err != nil || entry == nil { return false } return !isTtlExpired(entry) } // entryVersionFloorLocked raises a path's own version record to its // directory's listing floor or its section's refresh floor: each listing // covered every name in its range at its snapshot, so a name without a later // record of its own is versioned at the snapshot. func (mc *MetaCache) entryVersionFloorLocked(fp util.FullPath, recordTsNs int64) int64 { dir, name := fp.DirAndName() if floor := mc.dirVersionFloors[util.FullPath(dir)]; floor > recordTsNs { recordTsNs = floor } if sl := mc.dirSections[util.FullPath(dir)]; sl != nil { if floor := sl.floorOf(name); floor > recordTsNs { recordTsNs = floor } } return recordTsNs } func isTtlExpired(entry *filer.Entry) bool { return entry.TtlSec > 0 && entry.Crtime.Add(time.Duration(entry.TtlSec)*time.Second).Before(time.Now()) } // pruneSupersededTombstonesLocked drops direct-child tombstones at or below // the listing snapshot: the absence floor now fences what they fenced. // Tombstones above it (deletions the listing has not seen) survive; deeper // descendants answer to their own directory's floor. func (mc *MetaCache) pruneSupersededTombstonesLocked(ctx context.Context, dirPath util.FullPath, snapshotTsNs int64) { var superseded [][]byte if err := mc.leveldbStore.VisitKvPrefix(ctx, entryVersionChildPrefix(dirPath), func(key, value []byte) error { if len(value) != 9 || value[8] != entryVersionTombstone { return nil } if int64(util.BytesToUint64(value[:8])) <= snapshotTsNs { superseded = append(superseded, key) } return nil }); err != nil { glog.V(1).Infof("prune tombstones %s: %v", dirPath, err) return } for _, key := range superseded { if err := mc.localStore.KvDelete(ctx, key); err != nil { glog.V(1).Infof("prune tombstone %s: %v", string(key), err) } } } // deleteChildVersionRecordsLocked drops a directory's direct-child version // records when it is evicted. An uncached directory reads through to the filer // and gates its own inserts, so its records fence nothing — keeping them only // grows the store. A rebuild re-derives the floor and tombstones. func (mc *MetaCache) deleteChildVersionRecordsLocked(ctx context.Context, dirPath util.FullPath) { var keys [][]byte if err := mc.leveldbStore.VisitKvPrefix(ctx, entryVersionChildPrefix(dirPath), func(key, value []byte) error { keys = append(keys, key) return nil }); err != nil { glog.V(1).Infof("collect version records %s: %v", dirPath, err) return } for _, key := range keys { if err := mc.localStore.KvDelete(ctx, key); err != nil { glog.V(1).Infof("delete version record %s: %v", string(key), err) } } } func (mc *MetaCache) DeleteEntry(ctx context.Context, fp util.FullPath) (err error) { mc.Lock() defer mc.Unlock() if err = mc.localStore.DeleteEntry(ctx, fp); err != nil { return err } mc.clearEntryVersionLocked(ctx, fp) return nil } func (mc *MetaCache) DeleteFolderChildren(ctx context.Context, fp util.FullPath) (err error) { mc.Lock() defer mc.Unlock() delete(mc.dirVersionFloors, fp) delete(mc.dirSections, fp) mc.deleteChildVersionRecordsLocked(ctx, fp) return mc.localStore.DeleteFolderChildren(ctx, fp) } // SetPinnedChildFn installs a predicate reporting whether a child holds // local-only state a rebuild must not discard. See deleteFolderChildrenForRebuild. func (mc *MetaCache) SetPinnedChildFn(fn func(*filer.Entry) bool) { mc.pinnedChildFn = fn } // deleteFolderChildrenForRebuild clears a directory's cached children ahead of a // rebuild, but keeps any child flagged pinned by pinnedChildFn — a local-only // create not yet flushed to the filer. A rebuild refills from a filer listing // that does not include such a create; a blind wipe would drop it and then // markCachedFn publishes the directory authoritatively cached without a file the // client created, so it vanishes from the mount. func (mc *MetaCache) deleteFolderChildrenForRebuild(ctx context.Context, dirPath util.FullPath) error { mc.Lock() defer mc.Unlock() if mc.pinnedChildFn == nil { return mc.localStore.DeleteFolderChildren(ctx, dirPath) } var pinned []*filer.Entry if _, err := mc.localStore.ListDirectoryEntries(ctx, dirPath, "", true, math.MaxInt64, func(entry *filer.Entry) (bool, error) { if mc.pinnedChildFn(entry) { pinned = append(pinned, entry) } return true, nil }); err != nil { return err } if err := mc.localStore.DeleteFolderChildren(ctx, dirPath); err != nil { return err } if len(pinned) > 0 { return mc.doBatchInsertEntries(ctx, pinned) } return nil } // ListDirectoryEntries reports the last name the store reached, which is not // the last name handed to eachEntryFunc: an expired child is dropped here after // the store has already spent it against limit. A caller paginating by count // would read a short batch as the end of the directory, and one resuming from // the last name it saw would re-read the dropped ones forever. func (mc *MetaCache) ListDirectoryEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) { mc.RLock() defer mc.RUnlock() if !mc.isCachedFn(dirPath) { // if this request comes after renaming, it should be fine glog.Warningf("unsynchronized dir: %v", dirPath) } return mc.localStore.ListDirectoryEntries(ctx, dirPath, startFileName, includeStartFile, limit, func(entry *filer.Entry) (bool, error) { if entry.TtlSec > 0 && entry.Crtime.Add(time.Duration(entry.TtlSec)*time.Second).Before(time.Now()) { return true, nil } mc.mapIdFromFilerToLocal(entry) return eachEntryFunc(entry) }) } func (mc *MetaCache) markOversized(dirPath util.FullPath) { mc.Lock() defer mc.Unlock() mc.oversizedDirs[dirPath] = struct{}{} } func (mc *MetaCache) isOversized(dirPath util.FullPath) bool { mc.RLock() defer mc.RUnlock() _, found := mc.oversizedDirs[dirPath] return found } func (mc *MetaCache) Shutdown() { done := make(chan error, 1) mc.applyStateMu.Lock() if !mc.applyClosed { mc.applyClosed = true mc.applyCh <- metadataApplyRequest{ kind: metadataShutdown, done: done, } } mc.applyStateMu.Unlock() select { case <-done: case <-mc.applyDone: } <-mc.applyDone // The apply loop is the only dispatcher of entry invalidations; with it // stopped, drain and stop the invalidate worker before closing the store. mc.invalidateWorker.Shutdown() mc.Lock() defer mc.Unlock() mc.localStore.Shutdown() } func (mc *MetaCache) mapIdFromFilerToLocal(entry *filer.Entry) { entry.Attr.Uid, entry.Attr.Gid = mc.uidGidMapper.FilerToLocal(entry.Attr.Uid, entry.Attr.Gid) } func (mc *MetaCache) Debug() { if debuggable, ok := mc.localStore.(filer.Debuggable); ok { println("start debugging") debuggable.Debug(os.Stderr) } } // IsDirectoryCached returns true if the directory has been fully cached // (i.e., all entries have been loaded via EnsureVisited or ReadDir). func (mc *MetaCache) IsDirectoryCached(dirPath util.FullPath) bool { return mc.isCachedFn(dirPath) } func (mc *MetaCache) noteDirectoryUpdate(dirPath util.FullPath) { if mc.onDirectoryUpdate != nil { mc.onDirectoryUpdate(dirPath) } } func (mc *MetaCache) enqueueAndWait(ctx context.Context, req metadataApplyRequest) error { if ctx == nil { ctx = context.Background() } // Use a non-cancellable context for the queued operation so a // cancelled caller doesn't abort a build/complete mid-way. req.ctx = context.Background() req.done = make(chan error, 1) if err := mc.enqueueApplyRequest(req); err != nil { return err } select { case err := <-req.done: return err case <-ctx.Done(): return ctx.Err() } } func (mc *MetaCache) enqueueApplyRequest(req metadataApplyRequest) error { mc.applyStateMu.Lock() if mc.applyClosed { mc.applyStateMu.Unlock() return errMetaCacheClosed } // Release the mutex before the potentially-blocking channel send so that // Shutdown can still acquire it to set applyClosed when the channel is full. mc.applyStateMu.Unlock() select { case mc.applyCh <- req: return nil case <-mc.applyDone: return errMetaCacheClosed } } func (mc *MetaCache) runApplyLoop() { defer close(mc.applyDone) for req := range mc.applyCh { req.done <- mc.handleApplyRequest(req) close(req.done) if req.kind == metadataShutdown { mc.drainApplyCh() return } } } // drainApplyCh non-blockingly drains any remaining requests from applyCh // after a shutdown sentinel, signalling each caller so they don't block. func (mc *MetaCache) drainApplyCh() { for { select { case req := <-mc.applyCh: req.done <- errMetaCacheClosed close(req.done) default: return } } } func (mc *MetaCache) handleApplyRequest(req metadataApplyRequest) error { switch req.kind { case metadataApplyEvent: return mc.applyMetadataResponseNow(req.ctx, req.resp, req.options) case metadataBeginBuild: return mc.beginDirectoryBuildNow(req.buildPath) case metadataCompleteBuild: return mc.completeDirectoryBuildNow(req.ctx, req.buildPath, req.snapshotTsNs, req.sections) case metadataAbortBuild: return mc.abortDirectoryBuildNow(req.buildPath) case metadataPurgeDir: return mc.purgeDirectoryChildrenNow(req.ctx, req.buildPath, req.resetFn) case metadataSectionRefresh: return mc.applySectionRefreshNow(req.ctx, req.buildPath, req.refresh) case metadataShutdown: return nil default: return nil } } // EntryInvalidation describes one path's metadata change for an open-handle // refresh. type EntryInvalidation struct { Path util.FullPath Entry *filer_pb.Entry // entry now at path per the event; nil when the path was vacated TsNs int64 // the event's filer log position; 0 for locally built events Deleted bool // vacated by a delete, not a rename away — the file lives on elsewhere // RenamedTo is the destination when a rename vacated this path: the file // lives on there, so an open handle follows it rather than being orphaned. RenamedTo util.FullPath // Signatures from the event. The filer that logged it appends its own, so // this identifies the clock domain TsNs belongs to. Signatures []int32 // WasDirectory records what used to be at a vacated path. Entry is nil // once the path is empty, so this is the only thing left saying whether a // directory or a file went away. WasDirectory bool } type metadataResponseSideEffects struct { dirsToNotify []util.FullPath invalidations []EntryInvalidation } func (mc *MetaCache) applyMetadataResponseNow(ctx context.Context, resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) error { if mc.shouldSkipDuplicateEvent(resp) { return nil } immediateEvents, bufferedEvents := mc.routeMetadataResponse(resp) if len(bufferedEvents) == 0 { return mc.applyMetadataResponseDirect(ctx, resp, options, false) } for _, immediateEvent := range immediateEvents { if err := mc.applyMetadataResponseDirect(ctx, immediateEvent, MetadataResponseApplyOptions{}, false); err != nil { return err } } // Apply side effects but skip directory notifications for dirs that are // currently being built. Notifying a building dir can trigger // markDirectoryReadThrough → DeleteFolderChildren, wiping entries that // EnsureVisited already inserted, leaving an incomplete cache. mc.applyMetadataSideEffectsSkippingBuildingDirs(resp, options) for buildDir, events := range bufferedEvents { state := mc.buildingDirs[buildDir] if state == nil { continue } state.bufferedEvents = append(state.bufferedEvents, events...) } return nil } func (mc *MetaCache) applyMetadataResponseDirect(ctx context.Context, resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions, allowUncachedInsert bool) error { if _, err := mc.applyMetadataResponseLocked(ctx, resp, options, allowUncachedInsert); err != nil { return err } mc.applyMetadataSideEffects(resp, options) return nil } func (mc *MetaCache) applyMetadataSideEffects(resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) { sideEffects := metadataResponseSideEffects{} if options.NotifyDirectories { sideEffects.dirsToNotify = collectDirectoryNotifications(resp) } if options.InvalidateEntries { sideEffects.invalidations = collectEntryInvalidations(resp) } for _, dirPath := range sideEffects.dirsToNotify { mc.noteDirectoryUpdate(dirPath) } mc.invalidateWorker.Enqueue(sideEffects.invalidations...) } // applyMetadataSideEffectsSkippingBuildingDirs is like applyMetadataSideEffects // but suppresses directory notifications for dirs currently in buildingDirs. // This prevents markDirectoryReadThrough from wiping entries mid-build. func (mc *MetaCache) applyMetadataSideEffectsSkippingBuildingDirs(resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions) { sideEffects := metadataResponseSideEffects{} if options.NotifyDirectories { sideEffects.dirsToNotify = collectDirectoryNotifications(resp) } if options.InvalidateEntries { sideEffects.invalidations = collectEntryInvalidations(resp) } for _, dirPath := range sideEffects.dirsToNotify { if _, building := mc.buildingDirs[dirPath]; !building { mc.noteDirectoryUpdate(dirPath) } } mc.invalidateWorker.Enqueue(sideEffects.invalidations...) } // WaitForEntryInvalidations blocks until every invalidation enqueued so far // has been processed by the invalidate worker. Intended for tests and // shutdown paths that need the previously-synchronous behavior. func (mc *MetaCache) WaitForEntryInvalidations() { mc.invalidateWorker.Drain() } func (mc *MetaCache) applyMetadataResponseLocked(ctx context.Context, resp *filer_pb.SubscribeMetadataResponse, options MetadataResponseApplyOptions, allowUncachedInsert bool) (metadataResponseSideEffects, error) { message := resp.GetEventNotification() if message == nil { return metadataResponseSideEffects{}, nil } var oldPath util.FullPath var newPath util.FullPath var newEntry *filer.Entry hideNewPath := false if message.OldEntry != nil { oldPath = util.NewFullPath(resp.Directory, message.OldEntry.Name) } if message.NewEntry != nil { dir := resp.Directory if message.NewParentPath != "" { dir = message.NewParentPath } newPath = util.NewFullPath(dir, message.NewEntry.Name) hideNewPath = mc.shouldHideEntry(newPath) if !hideNewPath { newEntry = filer.FromPbEntry(dir, message.NewEntry) } } mc.Lock() // Last-writer-wins per entry: an event at or below an entry's version is // already reflected in it, and applying it would roll the entry back while // the version keeps the newer claim. Each half is gated independently. if resp.TsNs != 0 { if oldPath != "" && mc.entryVersionBlocksLocked(ctx, oldPath, resp.TsNs) { oldPath = "" } if newEntry != nil && mc.entryVersionBlocksLocked(ctx, newEntry.FullPath, resp.TsNs) { newEntry = nil } } // Only foreign churn counts toward section invalidation: this mount's own // writes are ground truth for its cache. if options.InvalidateEntries { now := time.Now() if oldPath != "" { mc.noteSectionChangeLocked(oldPath, now) } if newEntry != nil && newEntry.FullPath != oldPath { mc.noteSectionChangeLocked(newEntry.FullPath, now) } } err := mc.atomicUpdateEntryFromFilerLocked(ctx, oldPath, newEntry, allowUncachedInsert, resp.TsNs) if err == nil && hideNewPath { if purgeErr := mc.purgeEntryLocked(ctx, newPath, message.NewEntry.IsDirectory); purgeErr != nil { err = purgeErr } } // When a directory is deleted or moved, remove its cached descendants // so stale children cannot be served from the local cache. if err == nil && oldPath != "" && message.OldEntry != nil && message.OldEntry.IsDirectory { isDelete := message.NewEntry == nil isMove := message.NewEntry != nil && (message.NewParentPath != resp.Directory || message.NewEntry.Name != message.OldEntry.Name) if isDelete || isMove { delete(mc.dirVersionFloors, oldPath) delete(mc.dirSections, oldPath) if deleteErr := mc.localStore.DeleteFolderChildren(ctx, oldPath); deleteErr != nil { glog.V(2).Infof("delete descendants of %s: %v", oldPath, deleteErr) } } } mc.Unlock() if err != nil { return metadataResponseSideEffects{}, err } return metadataResponseSideEffects{}, nil } func (mc *MetaCache) beginDirectoryBuildNow(dirPath util.FullPath) error { if _, found := mc.buildingDirs[dirPath]; found { return nil } mc.buildingDirs[dirPath] = &directoryBuildState{} return nil } func (mc *MetaCache) abortDirectoryBuildNow(dirPath util.FullPath) error { delete(mc.buildingDirs, dirPath) return nil } // purgeDirectoryChildrenNow runs in the apply loop, serialized with // completeDirectoryBuildNow's markCachedFn, so no build publish interleaves // between resetFn (clears the cached flag) and the store wipe. Skipping a // building directory avoids deleting entries the build inserted but hasn't yet // published. Together these keep a directory from ending up flagged cached over // an empty store — which hides every file in it though they remain on the filer. func (mc *MetaCache) purgeDirectoryChildrenNow(ctx context.Context, dirPath util.FullPath, resetFn func()) error { if mc.isBuildingDir(dirPath) { return nil } if resetFn != nil { resetFn() } mc.Lock() defer mc.Unlock() delete(mc.dirVersionFloors, dirPath) delete(mc.dirSections, dirPath) mc.deleteChildVersionRecordsLocked(ctx, dirPath) return mc.localStore.DeleteFolderChildren(ctx, dirPath) } func (mc *MetaCache) completeDirectoryBuildNow(ctx context.Context, dirPath util.FullPath, snapshotTsNs int64, sections *sectionList) error { state := mc.buildingDirs[dirPath] delete(mc.buildingDirs, dirPath) if state == nil { return nil } // The listing covered every child at its snapshot, so one directory floor // versions them all — a child only needs its own record once a later event // touches it. An unversioned listing (pre-upgrade filer) instead clears the // children's records, or a re-inserted entry would inherit a stale one. mc.Lock() mc.dirSections[dirPath] = sections if snapshotTsNs != 0 { mc.dirVersionFloors[dirPath] = snapshotTsNs mc.pruneSupersededTombstonesLocked(ctx, dirPath, snapshotTsNs) } else { delete(mc.dirVersionFloors, dirPath) mc.deleteChildVersionRecordsLocked(ctx, dirPath) } mc.Unlock() for _, event := range state.bufferedEvents { // When the server provided a snapshot timestamp, skip events that // the listing already included. When snapshotTsNs == 0 (empty // directory — server returned no entries and no snapshot), replay // ALL buffered events to avoid dropping mutations due to // client/server clock skew. if snapshotTsNs != 0 && event.TsNs != 0 && event.TsNs <= snapshotTsNs { continue } if err := mc.applyMetadataResponseDirect(ctx, event, MetadataResponseApplyOptions{}, true); err != nil { return err } } mc.markCachedFn(dirPath) // Re-invalidate every buffered event: each ran against a mid-build store, // so a handle can hold older state than the completed directory. After // markCachedFn, versioned at the snapshot to outrank the mid-build install. for _, event := range state.bufferedEvents { if event.TsNs < snapshotTsNs { event.TsNs = snapshotTsNs } mc.applyMetadataSideEffects(event, MetadataResponseApplyOptions{InvalidateEntries: true}) } return nil } func (mc *MetaCache) routeMetadataResponse(resp *filer_pb.SubscribeMetadataResponse) ([]*filer_pb.SubscribeMetadataResponse, map[util.FullPath][]*filer_pb.SubscribeMetadataResponse) { message := resp.GetEventNotification() if message == nil { return []*filer_pb.SubscribeMetadataResponse{resp}, nil } oldDir, hasOld := metadataOldParentDir(resp) newDir, hasNew := metadataNewParentDir(resp) oldBuilding := hasOld && mc.isBuildingDir(oldDir) newBuilding := hasNew && mc.isBuildingDir(newDir) if !oldBuilding && !newBuilding { return []*filer_pb.SubscribeMetadataResponse{resp}, nil } bufferedEvents := make(map[util.FullPath][]*filer_pb.SubscribeMetadataResponse) var immediateEvents []*filer_pb.SubscribeMetadataResponse if hasOld && hasNew && oldDir != newDir { deleteEvent := metadataDeleteFragment(resp) createEvent := metadataCreateFragment(resp) if oldBuilding { bufferedEvents[oldDir] = append(bufferedEvents[oldDir], deleteEvent) } else { immediateEvents = append(immediateEvents, deleteEvent) } if newBuilding { bufferedEvents[newDir] = append(bufferedEvents[newDir], createEvent) } else { immediateEvents = append(immediateEvents, createEvent) } return immediateEvents, bufferedEvents } targetDir := newDir if hasOld { targetDir = oldDir } if mc.isBuildingDir(targetDir) { bufferedEvents[targetDir] = append(bufferedEvents[targetDir], resp) return nil, bufferedEvents } return []*filer_pb.SubscribeMetadataResponse{resp}, nil } func (mc *MetaCache) isBuildingDir(dirPath util.FullPath) bool { _, found := mc.buildingDirs[dirPath] return found } func metadataOldParentDir(resp *filer_pb.SubscribeMetadataResponse) (util.FullPath, bool) { if resp.GetEventNotification() == nil || resp.EventNotification.OldEntry == nil { return "", false } return util.FullPath(resp.Directory), true } func metadataNewParentDir(resp *filer_pb.SubscribeMetadataResponse) (util.FullPath, bool) { if resp.GetEventNotification() == nil || resp.EventNotification.NewEntry == nil { return "", false } newDir := resp.Directory if resp.EventNotification.NewParentPath != "" { newDir = resp.EventNotification.NewParentPath } return util.FullPath(newDir), true } func metadataDeleteFragment(resp *filer_pb.SubscribeMetadataResponse) *filer_pb.SubscribeMetadataResponse { if resp.GetEventNotification() == nil || resp.EventNotification.OldEntry == nil { return nil } return &filer_pb.SubscribeMetadataResponse{ Directory: resp.Directory, EventNotification: &filer_pb.EventNotification{ OldEntry: proto.Clone(resp.EventNotification.OldEntry).(*filer_pb.Entry), }, TsNs: resp.TsNs, } } func metadataCreateFragment(resp *filer_pb.SubscribeMetadataResponse) *filer_pb.SubscribeMetadataResponse { if resp.GetEventNotification() == nil || resp.EventNotification.NewEntry == nil { return nil } newDir := resp.Directory if resp.EventNotification.NewParentPath != "" { newDir = resp.EventNotification.NewParentPath } return &filer_pb.SubscribeMetadataResponse{ Directory: newDir, EventNotification: &filer_pb.EventNotification{ NewEntry: proto.Clone(resp.EventNotification.NewEntry).(*filer_pb.Entry), NewParentPath: newDir, }, TsNs: resp.TsNs, } } func metadataEventDedupKey(resp *filer_pb.SubscribeMetadataResponse) string { var oldName, newName, newParent string hasOld, hasNew := false, false if msg := resp.GetEventNotification(); msg != nil { if msg.OldEntry != nil { oldName = msg.OldEntry.Name hasOld = true } if msg.NewEntry != nil { newName = msg.NewEntry.Name hasNew = true newParent = msg.NewParentPath } } // Encode event shape (create/delete/update/rename) so structurally // different events with the same names are not collapsed. var shape byte switch { case hasOld && hasNew: if resp.Directory != newParent && newParent != "" { shape = 'R' // rename across directories } else { shape = 'U' // update in place } case hasOld: shape = 'D' // delete case hasNew: shape = 'C' // create } return fmt.Sprintf("%d|%c|%s|%s|%s|%s", resp.TsNs, shape, resp.Directory, oldName, newParent, newName) } func (mc *MetaCache) shouldSkipDuplicateEvent(resp *filer_pb.SubscribeMetadataResponse) bool { if resp == nil || resp.TsNs == 0 { return false } key := metadataEventDedupKey(resp) return !mc.dedupRing.Add(key) } type dedupRingBuffer struct { keys [recentEventDedupWindow]string head int size int set map[string]struct{} } func newDedupRingBuffer() dedupRingBuffer { return dedupRingBuffer{ set: make(map[string]struct{}, recentEventDedupWindow), } } func (r *dedupRingBuffer) Add(key string) bool { if _, found := r.set[key]; found { return false // duplicate } if r.size == recentEventDedupWindow { evicted := r.keys[r.head] delete(r.set, evicted) } else { r.size++ } r.keys[r.head] = key r.set[key] = struct{}{} r.head = (r.head + 1) % recentEventDedupWindow return true // new entry } func collectDirectoryNotifications(resp *filer_pb.SubscribeMetadataResponse) []util.FullPath { message := resp.GetEventNotification() if message == nil { return nil } // At most 3 dirs: old parent, new parent, new child (if directory). // Use a fixed slice with linear dedup to avoid map allocation. var dirs [3]util.FullPath n := 0 addUnique := func(p util.FullPath) { for i := 0; i < n; i++ { if dirs[i] == p { return } } dirs[n] = p n++ } if message.OldEntry != nil { oldPath := util.NewFullPath(resp.Directory, message.OldEntry.Name) parent, _ := oldPath.DirAndName() addUnique(util.FullPath(parent)) } if message.NewEntry != nil { newDir := resp.Directory if message.NewParentPath != "" { newDir = message.NewParentPath } newPath := util.NewFullPath(newDir, message.NewEntry.Name) parent, _ := newPath.DirAndName() addUnique(util.FullPath(parent)) if message.NewEntry.IsDirectory { addUnique(newPath) } } return dirs[:n] } func collectEntryInvalidations(resp *filer_pb.SubscribeMetadataResponse) []EntryInvalidation { message := resp.GetEventNotification() if message == nil { return nil } var invalidations []EntryInvalidation signatures := message.Signatures if message.OldEntry != nil && message.NewEntry != nil { oldKey := util.NewFullPath(resp.Directory, message.OldEntry.Name) // Normalize NewParentPath: empty means same directory as resp.Directory newDir := resp.Directory if message.NewParentPath != "" { newDir = message.NewParentPath } if message.OldEntry.Name != message.NewEntry.Name || resp.Directory != newDir { newKey := util.NewFullPath(newDir, message.NewEntry.Name) invalidations = append(invalidations, EntryInvalidation{Path: oldKey, TsNs: resp.TsNs, Signatures: signatures, RenamedTo: newKey, WasDirectory: message.OldEntry.IsDirectory}) invalidations = append(invalidations, EntryInvalidation{Path: newKey, Entry: message.NewEntry, TsNs: resp.TsNs, Signatures: signatures}) } else { invalidations = append(invalidations, EntryInvalidation{Path: oldKey, Entry: message.NewEntry, TsNs: resp.TsNs, Signatures: signatures}) } return invalidations } if filer_pb.IsCreate(resp) && message.NewEntry != nil { newDir := resp.Directory if message.NewParentPath != "" { newDir = message.NewParentPath } newKey := util.NewFullPath(newDir, message.NewEntry.Name) invalidations = append(invalidations, EntryInvalidation{Path: newKey, Entry: message.NewEntry, TsNs: resp.TsNs, Signatures: signatures}) } if filer_pb.IsDelete(resp) && message.OldEntry != nil { oldKey := util.NewFullPath(resp.Directory, message.OldEntry.Name) invalidations = append(invalidations, EntryInvalidation{Path: oldKey, TsNs: resp.TsNs, Deleted: true, Signatures: signatures, WasDirectory: message.OldEntry.IsDirectory}) } return invalidations }