mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-18 12:08:32 +02:00
* filer: create parent directories for lazily fetched remote entries A remote-mounted object first read through the filer was persisted without its parent directories, so remote.uncache and the remote cache evictor, which walk down from the mount directory, never reached it: its cached chunks could not be evicted. Persist the missing parents with the entry. With those directories in place, the empty-folder cleaner leaves folders under a remote mount alone: deleting one removes its whole prefix on the remote, which can still hold objects that were never read locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: keep remote objects at parent keys and remote prefixes of empty folders Look up a lazily fetched entry's missing parents through lazy fetch before creating them, so a remote object stored at a parent's key is materialized and promoted to a prefix object rather than hidden behind a new directory. Deleting a folder that is empty in the filer is metadata-only under a remote mount, both for a non-recursive delete and for the expiry cleanup of empty parents: the mount is populated lazily, so the folder can still hold remote objects never read, and removing its remote directory deletes every object under that prefix. This replaces the empty-folder cleaner's own guard, which covered only one of those paths. A metadata-only delete records no remote-deletion tombstone, locally or from its metadata event: nothing is owed to the remote, so lazy reads stay open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: stop the lazy parent lookup at a store error Only a parent the store reports as not found is missing; a parent it cannot read ends the walk, so the lookup does not stat ancestors above it on the remote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: stat each missing remote parent once A lazy fetch that finds k missing parents asked FindEntry for each, and every nested fetch re-walked its own still-missing ancestors, re-stating remote keys already proven absent: 1+k(k+1)/2 remote stats for one first read. The flagged parent lookups now skip their own walk; their entries are created by the same CreateEntry that already persists the chain. * filer: materialize parents for lazily listed remote entries maybeLazyListFromRemote persisted children and the listing directory with skipCreateParentDir, so a first access that goes through the list path still left entries unreachable to a walk from the mount directory. Materialize the missing ancestors the same way the lazy fetch does, and let the synced_at directory create its parent chain. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
348 lines
13 KiB
Go
348 lines
13 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/remote_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/remote_storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
type lazyFetchContextKey struct{}
|
|
|
|
// lazyParentsContextKey marks a FindEntry issued while materializing missing
|
|
// parents: that fetch's own parent walk would only re-stat the same gaps.
|
|
type lazyParentsContextKey struct{}
|
|
|
|
// maybeLazyFetchFromRemote is called by FindEntry when the store returns no
|
|
// entry for p. If p is under a remote-storage mount, it stats the remote
|
|
// object, builds a filer Entry from the result, and persists it via
|
|
// CreateEntry together with its missing parent directories: remote.uncache
|
|
// and cache eviction find entries by walking down from the mount directory.
|
|
//
|
|
// On a CreateEntry failure after a successful StatFile the in-memory entry is
|
|
// still returned (availability over consistency); the singleflight key is
|
|
// forgotten so the next lookup retries the filer write.
|
|
//
|
|
// Returns nil without error when: p is not under a remote mount; the remote
|
|
// reports the object does not exist; or any other remote error occurs.
|
|
func (f *Filer) maybeLazyFetchFromRemote(ctx context.Context, p util.FullPath) (*Entry, error) {
|
|
// Prevent recursive invocation: CreateEntry calls FindEntry, which would
|
|
// re-enter this function and deadlock on the singleflight key.
|
|
if ctx.Value(lazyFetchContextKey{}) != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
if f.RemoteStorage == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(p)
|
|
if remoteLoc == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// A startup tombstone rebuild may still be replaying the meta log; wait
|
|
// for it so a pending delete cannot resurrect here.
|
|
if done := f.remoteTombstonesDone.Load(); done != nil {
|
|
select {
|
|
case <-*done:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
if f.isRemoteDeletionPending(ctx, p, mountDir) {
|
|
glog.V(2).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s deleted locally, remote delete pending", p)
|
|
return nil, nil
|
|
}
|
|
|
|
remoteConf, found := f.RemoteStorage.FindRemoteStorageConf(p)
|
|
if !found {
|
|
return nil, nil
|
|
}
|
|
|
|
relPath := strings.TrimPrefix(string(p), string(mountDir))
|
|
if relPath != "" && !strings.HasPrefix(relPath, "/") {
|
|
relPath = "/" + relPath
|
|
}
|
|
base := strings.TrimSuffix(remoteLoc.Path, "/")
|
|
remotePath := "/" + strings.TrimLeft(base+relPath, "/")
|
|
|
|
objectLoc := &remote_pb.RemoteStorageLocation{
|
|
Name: remoteLoc.Name,
|
|
Bucket: remoteLoc.Bucket,
|
|
Path: remotePath,
|
|
}
|
|
|
|
type lazyFetchResult struct {
|
|
entry *Entry
|
|
}
|
|
|
|
key := string(p)
|
|
val, err, _ := f.lazyFetchGroup.Do(key, func() (interface{}, error) {
|
|
buildCtx := context.WithoutCancel(ctx)
|
|
client, clientErr := f.buildRemoteStorageClient(buildCtx, remoteConf)
|
|
if clientErr != nil {
|
|
glog.V(1).InfofCtx(ctx, "maybeLazyFetchFromRemote: reject %s: %v", p, clientErr)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
remoteEntry, statErr := client.StatFile(objectLoc)
|
|
if statErr != nil {
|
|
if errors.Is(statErr, remote_storage.ErrRemoteObjectNotFound) {
|
|
glog.V(3).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s not found in remote", p)
|
|
} else {
|
|
glog.Warningf("maybeLazyFetchFromRemote: stat %s failed: %v", p, statErr)
|
|
}
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
if remoteEntry == nil {
|
|
glog.V(3).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s StatFile returned nil entry", p)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
|
|
mtime := time.Unix(remoteEntry.RemoteMtime, 0)
|
|
entry := &Entry{
|
|
FullPath: p,
|
|
Attr: Attr{
|
|
Mtime: mtime,
|
|
Crtime: mtime,
|
|
Mode: 0644,
|
|
FileSize: uint64(remoteEntry.RemoteSize),
|
|
},
|
|
Extended: MergeRemoteContentEncoding(remoteEntry, nil),
|
|
Remote: remoteEntry,
|
|
}
|
|
|
|
persistBaseCtx, cancelPersist := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancelPersist()
|
|
persistCtx := context.WithValue(persistBaseCtx, lazyFetchContextKey{}, true)
|
|
// A delete may have landed while StatFile was in flight; re-check so
|
|
// the fetched object cannot resurrect a path whose delete is pending.
|
|
if f.isRemoteDeletionPending(persistCtx, p, mountDir) {
|
|
glog.V(2).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s deleted during remote stat", p)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
if ctx.Value(lazyParentsContextKey{}) == nil {
|
|
f.fetchMissingRemoteParents(persistBaseCtx, mountDir, p)
|
|
}
|
|
saveErr := f.CreateEntry(persistCtx, entry, nil, false, false, nil, false, f.MaxFilenameLength)
|
|
if saveErr != nil {
|
|
glog.Warningf("maybeLazyFetchFromRemote: failed to persist filer entry for %s: %v", p, saveErr)
|
|
f.lazyFetchGroup.Forget(key)
|
|
return lazyFetchResult{entry}, nil
|
|
}
|
|
|
|
// A delete records its tombstone before removing the entry, so a
|
|
// tombstone visible now means the insert raced a delete that already
|
|
// ran: retract the persisted entry so the path stays deleted.
|
|
if f.isRemoteDeletionPending(persistCtx, p, mountDir) {
|
|
glog.V(2).InfofCtx(ctx, "maybeLazyFetchFromRemote: %s deleted while persisting", p)
|
|
f.lazyFetchGroup.Forget(key)
|
|
f.retractLazyRemoteEntry(persistCtx, entry)
|
|
return lazyFetchResult{nil}, nil
|
|
}
|
|
|
|
return lazyFetchResult{entry}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, ok := val.(lazyFetchResult)
|
|
if !ok {
|
|
return nil, fmt.Errorf("maybeLazyFetchFromRemote: unexpected singleflight result type %T for %s", val, p)
|
|
}
|
|
return result.entry, nil
|
|
}
|
|
|
|
// fetchMissingRemoteParents looks up p's missing parents under mountDir, top
|
|
// down, through lazy fetch. A remote object stored at a parent's key is then
|
|
// materialized first, so creating p promotes it to a prefix object instead of
|
|
// hiding it behind a plain directory. Each missing parent costs one remote
|
|
// stat, once per directory.
|
|
func (f *Filer) fetchMissingRemoteParents(ctx context.Context, mountDir, p util.FullPath) {
|
|
var missing []util.FullPath
|
|
for dir, _ := p.DirAndName(); len(dir) > len(mountDir); dir, _ = util.FullPath(dir).DirAndName() {
|
|
if _, err := f.Store.FindEntry(ctx, util.FullPath(dir)); !errors.Is(err, filer_pb.ErrNotFound) {
|
|
break
|
|
}
|
|
missing = append(missing, util.FullPath(dir))
|
|
}
|
|
parentCtx := context.WithValue(ctx, lazyParentsContextKey{}, true)
|
|
for i := len(missing) - 1; i >= 0; i-- {
|
|
f.FindEntry(parentCtx, missing[i])
|
|
}
|
|
}
|
|
|
|
// retractLazyRemoteEntry deletes the entry at entry.FullPath only when it is
|
|
// still the entry a lazy remote read just materialized — a concurrent write
|
|
// may have replaced it, and deleting by path alone would take that write down.
|
|
func (f *Filer) retractLazyRemoteEntry(ctx context.Context, entry *Entry) {
|
|
existing, findErr := f.FindEntry(ctx, entry.FullPath)
|
|
if findErr != nil || existing == nil {
|
|
return
|
|
}
|
|
// The stored entry must still be exactly what this read materialized —
|
|
// an intervening write (appended chunks, touched attributes) means a
|
|
// real update owns the path now.
|
|
if !proto.Equal(existing.ToProtoEntry(), entry.ToProtoEntry()) {
|
|
return
|
|
}
|
|
if err := f.doDeleteEntryMetaAndData(ctx, existing, false, false, nil); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
|
glog.Warningf("retractLazyRemoteEntry %s: %v", entry.FullPath, err)
|
|
}
|
|
}
|
|
|
|
func (f *Filer) maybeDeleteFromRemote(ctx context.Context, entry *Entry) (bool, error) {
|
|
if entry == nil || f.RemoteStorage == nil {
|
|
return false, nil
|
|
}
|
|
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(entry.FullPath)
|
|
if remoteLoc == nil {
|
|
return false, nil
|
|
}
|
|
|
|
if !entry.IsDirectory() && entry.Remote == nil {
|
|
return false, nil
|
|
}
|
|
|
|
remoteConf, found := f.RemoteStorage.GetRemoteStorageConf(remoteLoc.Name)
|
|
if !found {
|
|
return false, fmt.Errorf("resolve remote storage client for %s: not found", entry.FullPath)
|
|
}
|
|
client, clientErr := f.buildRemoteStorageClient(ctx, remoteConf)
|
|
if clientErr != nil {
|
|
return false, fmt.Errorf("resolve remote storage client for %s: %w", entry.FullPath, clientErr)
|
|
}
|
|
if client == nil {
|
|
return false, fmt.Errorf("resolve remote storage client for %s: initialization failed", entry.FullPath)
|
|
}
|
|
|
|
objectLoc := MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, entry.FullPath)
|
|
|
|
if entry.IsDirectory() {
|
|
if err := client.RemoveDirectory(objectLoc); err != nil {
|
|
if errors.Is(err, remote_storage.ErrRemoteObjectNotFound) {
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("remove remote directory %s: %w", entry.FullPath, err)
|
|
}
|
|
glog.V(3).InfofCtx(ctx, "maybeDeleteFromRemote: deleted directory %s from remote", entry.FullPath)
|
|
return true, nil
|
|
}
|
|
|
|
if err := client.DeleteFile(objectLoc); err != nil {
|
|
if errors.Is(err, remote_storage.ErrRemoteObjectNotFound) {
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("delete remote file %s: %w", entry.FullPath, err)
|
|
}
|
|
|
|
glog.V(3).InfofCtx(ctx, "maybeDeleteFromRemote: deleted %s from remote", entry.FullPath)
|
|
return true, nil
|
|
}
|
|
|
|
// DeleteFromRemote deletes the object, or for a directory every object, the
|
|
// entry maps to on its mounted remote storage.
|
|
func (f *Filer) DeleteFromRemote(ctx context.Context, entry *Entry) error {
|
|
_, err := f.maybeDeleteFromRemote(ctx, entry)
|
|
return err
|
|
}
|
|
|
|
// ExtRemoteRenameSourceKey marks a remote-only entry whose rename committed
|
|
// while its copy to the new key was still pending: it holds the path whose
|
|
// object remote-only reads must serve until the copy stamps the entry.
|
|
const ExtRemoteRenameSourceKey = "Seaweed-X-Remote-Rename-Source"
|
|
|
|
// RemoteSourcePath returns the path whose remote object holds the content of
|
|
// the remote-only entry at p: the renamed-from path a pending deferred copy
|
|
// still reads, or p itself. A marker leading out of p's mount is ignored, so
|
|
// whoever can set extended attributes cannot point reads at another mount.
|
|
func (f *Filer) RemoteSourcePath(entry *Entry, p util.FullPath) util.FullPath {
|
|
src := util.FullPath(entry.Extended[ExtRemoteRenameSourceKey])
|
|
if src == "" || f.RemoteStorage == nil {
|
|
return p
|
|
}
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(p)
|
|
if remoteLoc == nil {
|
|
return p
|
|
}
|
|
if srcMountDir, _ := f.RemoteStorage.FindMountDirectory(src); srcMountDir != mountDir {
|
|
return p
|
|
}
|
|
return src
|
|
}
|
|
|
|
// RemoteReadPath returns the dir and name whose object a remote-only read of
|
|
// the entry at dir/name serves.
|
|
func (f *Filer) RemoteReadPath(entry *Entry, dir, name string) (string, string) {
|
|
return f.RemoteSourcePath(entry, util.NewFullPath(dir, name)).DirAndName()
|
|
}
|
|
|
|
// CopyRemoteOnlyEntry copies the remote object of a renamed entry whose
|
|
// content exists only on the remote to the key of newPath, and returns the
|
|
// RemoteEntry of the copy. The rename deletes the old key along with the old
|
|
// entry, and with no local data nothing would write the new key. It returns
|
|
// nil when no copy is needed: the entry has local data, which the remote sync
|
|
// uploads to the new key, it is empty, which the sync writes as an empty
|
|
// object, or it is not under a remote mount. A rename that
|
|
// moves such an entry out of its mount, or onto a remote that cannot copy, is
|
|
// refused, since it would delete the only copy of the content.
|
|
func (f *Filer) CopyRemoteOnlyEntry(ctx context.Context, entry *Entry, oldPath, newPath util.FullPath) (*filer_pb.RemoteEntry, error) {
|
|
src, dst, copier, err := f.RemoteCopyLocations(ctx, entry, oldPath, newPath)
|
|
if err != nil || copier == nil {
|
|
return nil, err
|
|
}
|
|
remoteEntry, err := copier.CopyFile(src, dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("copy remote %s to %s: %w", remote_storage.FormatLocation(src), remote_storage.FormatLocation(dst), err)
|
|
}
|
|
glog.V(3).InfofCtx(ctx, "CopyRemoteOnlyEntry: copied %s to %s", remote_storage.FormatLocation(src), remote_storage.FormatLocation(dst))
|
|
return remoteEntry, nil
|
|
}
|
|
|
|
// RemoteCopyLocations resolves the object locations a remote-only rename copy
|
|
// of entry between oldPath and newPath would use, and the client able to copy
|
|
// it. A nil copier means the entry needs no copy; an error means it needs one
|
|
// that cannot be served, which a rename must refuse before committing.
|
|
func (f *Filer) RemoteCopyLocations(ctx context.Context, entry *Entry, oldPath, newPath util.FullPath) (src, dst *remote_pb.RemoteStorageLocation, copier remote_storage.RemoteStorageObjectCopier, err error) {
|
|
if f.RemoteStorage == nil || entry.IsDirectory() || !entry.IsInRemoteOnly() || len(entry.Content) > 0 {
|
|
return nil, nil, nil, nil
|
|
}
|
|
|
|
mountDir, remoteLoc := f.RemoteStorage.FindMountDirectory(oldPath)
|
|
if remoteLoc == nil {
|
|
return nil, nil, nil, nil
|
|
}
|
|
if newMountDir, _ := f.RemoteStorage.FindMountDirectory(newPath); newMountDir != mountDir {
|
|
return nil, nil, nil, fmt.Errorf("%s exists only on the remote of mount %s and cannot be renamed out of it; cache it first", oldPath, mountDir)
|
|
}
|
|
|
|
remoteConf, found := f.RemoteStorage.GetRemoteStorageConf(remoteLoc.Name)
|
|
if !found {
|
|
return nil, nil, nil, fmt.Errorf("resolve remote storage client for %s: not found", oldPath)
|
|
}
|
|
client, clientErr := f.buildRemoteStorageClient(ctx, remoteConf)
|
|
if clientErr != nil {
|
|
return nil, nil, nil, fmt.Errorf("resolve remote storage client for %s: %w", oldPath, clientErr)
|
|
}
|
|
copier, ok := client.(remote_storage.RemoteStorageObjectCopier)
|
|
if !ok {
|
|
return nil, nil, nil, fmt.Errorf("%s exists only on the remote and remote storage type %s cannot copy it to the new name; cache it first", oldPath, remoteConf.Type)
|
|
}
|
|
|
|
src = MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, f.RemoteSourcePath(entry, oldPath))
|
|
dst = MapFullPathToRemoteStorageLocation(mountDir, remoteLoc, newPath)
|
|
return src, dst, copier, nil
|
|
}
|