mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-04 11:07:35 +01:00
* remote.uncache: skip a file overwritten since the walk read it The uncache cleared the chunks of the entry its directory walk returned and wrote it back unconditionally. A file overwritten after the walk read it was reset to the remote pointer, and its new chunks were dropped, so an acknowledged write was lost. Write back only while the filer still holds the entry as read, and report the file as skipped otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.meta.sync: keep files written locally after the directory listing meta.sync lists each local directory once and works from that listing. A changed remote file was written back over the listed entry unconditionally, and a name missing from the listing was created without O_EXCL, so a file written locally in between was replaced by the remote pointer either way. Update only while the filer still holds the listed entry, create exclusively, and report such files as skipped. The create now also surfaces errors the filer returns in its response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.cache: keep files written locally after the lookup The metadata step looks each file up and then either rewrites it from the remote unconditionally or creates it without O_EXCL, so a local write landing between the lookup and the write was replaced by the remote pointer. Reuse doSaveRemoteEntry, which updates only while the filer still holds the looked-up entry, create exclusively, and report such files as skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.meta.sync: do not descend into a name a local file took When the exclusive create of a remote directory finds the name taken, the sync still descended into it, and creating the remote children under a local file would turn that file into a directory. Descend only when the name now holds a directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.meta.sync: delete only the file version it listed The type-change and -deleteStale paths removed a remote-backed file unconditionally, so a local write landing after the directory listing was deleted with it. Condition the delete on the listed mtime, as fs.verify and volume.fsck do; the filer then keeps a file written since. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.meta.sync: condition directory and mtime-less deletes too The listed-mtime cutoff was only sent for files, so a remote-backed directory replaced locally by a file between the emptiness check and the delete was removed unconditionally, and so was a file listed without an mtime. Send the cutoff for directories as well, and for an entry listed without an mtime keep it once a write has set one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * filer: test that DeleteEntry applies the request's mtime cutoff Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.meta.sync: keep the listed nanoseconds as the cutoff of an epoch mtime An entry listed at the epoch with a sub-second mtime was sent a 1 ns cutoff, so the filer kept it unchanged although the old code deleted it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * remote.meta.sync: report an entry the filer kept instead of deleting it The filer answers a delete it skipped under the mtime cutoff with success, so the sync printed (delete) for an entry a local write kept. deleteLocalEntry now looks the name up after the delete and prints the outcome, and a replaced file that was kept is not recreated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
7.9 KiB
Go
230 lines
7.9 KiB
Go
package shell
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"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"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type MetadataCacheStrategy string
|
|
|
|
const (
|
|
MetadataCacheEager MetadataCacheStrategy = "eager"
|
|
MetadataCacheLazy MetadataCacheStrategy = "lazy"
|
|
)
|
|
|
|
func init() {
|
|
Commands = append(Commands, &commandRemoteMount{})
|
|
}
|
|
|
|
type commandRemoteMount struct {
|
|
}
|
|
|
|
func (c *commandRemoteMount) Name() string {
|
|
return "remote.mount"
|
|
}
|
|
|
|
func (c *commandRemoteMount) Help() string {
|
|
return `mount remote storage and optionally pull its metadata
|
|
|
|
# assume a remote storage is configured to name "cloud1"
|
|
remote.configure -name=cloud1 -type=s3 -s3.access_key=xxx -s3.secret_key=yyy
|
|
|
|
# mount and pull one bucket (full upfront metadata sync)
|
|
remote.mount -dir=/xxx -remote=cloud1/bucket
|
|
# mount without upfront sync; metadata is fetched lazily on access
|
|
remote.mount -dir=/xxx -remote=cloud1/bucket -metadataStrategy=lazy
|
|
# mount and pull one directory in the bucket
|
|
remote.mount -dir=/xxx -remote=cloud1/bucket/dir1
|
|
# mount with on-demand directory listing cached for 5 minutes
|
|
remote.mount -dir=/xxx -remote=cloud1/bucket -listingCacheTTL=300
|
|
# mount as a streaming source: reads go to the remote instead of waiting for the local cache
|
|
remote.mount -dir=/xxx -remote=cloud1/bucket -cacheWait=0
|
|
|
|
# after mount, start a separate process to write updates to remote storage
|
|
weed filer.remote.sync -filer=<filerHost>:<filerPort> -dir=/xxx
|
|
|
|
`
|
|
}
|
|
|
|
func (c *commandRemoteMount) HasTag(CommandTag) bool {
|
|
return false
|
|
}
|
|
|
|
func (c *commandRemoteMount) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
|
|
|
remoteMountCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
|
|
|
dir := remoteMountCommand.String("dir", "", "a directory in filer")
|
|
nonEmpty := remoteMountCommand.Bool("nonempty", false, "allows the mounting over a non-empty directory")
|
|
metadataStrategy := remoteMountCommand.String("metadataStrategy", string(MetadataCacheEager), "lazy: skip upfront metadata pull; eager: full metadata pull (default)")
|
|
remote := remoteMountCommand.String("remote", "", "a directory in remote storage, ex. <storageName>/<bucket>/path/to/dir")
|
|
listingCacheTTL := remoteMountCommand.Int("listingCacheTTL", 0, "seconds to cache remote directory listings (0 = disabled)")
|
|
cacheWait := remoteMountCommand.Duration("cacheWait", -1, "how long a read of an uncached object waits for the local cache, ex. 0 or 500ms (default: by object size)")
|
|
|
|
if err = remoteMountCommand.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
if *dir == "" {
|
|
_, err = listExistingRemoteStorageMounts(commandEnv, writer)
|
|
return err
|
|
}
|
|
|
|
// find configuration for remote storage
|
|
remoteConf, err := filer.ReadRemoteStorageConf(commandEnv.option.GrpcDialOption, commandEnv.FilerAddress(), remote_storage.ParseLocationName(*remote))
|
|
if err != nil {
|
|
return fmt.Errorf("find configuration for %s: %v", *remote, err)
|
|
}
|
|
|
|
remoteStorageLocation, err := remote_storage.ParseRemoteLocation(remoteConf.Type, *remote)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
remoteStorageLocation.ListingCacheTtlSeconds = int32(*listingCacheTTL)
|
|
if *cacheWait >= 0 {
|
|
waitMs := cacheWait.Milliseconds()
|
|
if waitMs > math.MaxInt32 {
|
|
return fmt.Errorf("cacheWait %v is too long", *cacheWait)
|
|
}
|
|
// truncating to 0 would read as "never wait" instead of the asked-for wait
|
|
if waitMs == 0 && *cacheWait > 0 {
|
|
return fmt.Errorf("cacheWait %v is shorter than 1ms", *cacheWait)
|
|
}
|
|
remoteStorageLocation.CacheWaitMs = proto.Int32(int32(waitMs))
|
|
}
|
|
|
|
strategy := MetadataCacheStrategy(strings.ToLower(*metadataStrategy))
|
|
if strategy != MetadataCacheLazy && strategy != MetadataCacheEager {
|
|
return fmt.Errorf("metadataStrategy must be %s or %s, got %q", MetadataCacheLazy, MetadataCacheEager, *metadataStrategy)
|
|
}
|
|
|
|
if err = ensureMountDirectory(commandEnv, *dir, *nonEmpty, remoteConf); err != nil {
|
|
return fmt.Errorf("mount setup: %w", err)
|
|
}
|
|
|
|
if strategy == MetadataCacheEager {
|
|
if err = pullMetadata(commandEnv, writer, util.FullPath(*dir), remoteStorageLocation, util.FullPath(*dir), remoteConf, false, false); err != nil {
|
|
return fmt.Errorf("cache metadata: %w", err)
|
|
}
|
|
}
|
|
|
|
// store a mount configuration in filer
|
|
if err = filer.InsertMountMapping(commandEnv, *dir, remoteStorageLocation); err != nil {
|
|
return fmt.Errorf("save mount mapping: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func listExistingRemoteStorageMounts(commandEnv *CommandEnv, writer io.Writer) (mappings *remote_pb.RemoteStorageMapping, err error) {
|
|
|
|
// read current mapping
|
|
mappings, err = filer.ReadMountMappings(commandEnv.option.GrpcDialOption, commandEnv.FilerAddress())
|
|
if err != nil {
|
|
return mappings, err
|
|
}
|
|
|
|
jsonPrintln(writer, mappings)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
func jsonPrintln(writer io.Writer, message proto.Message) error {
|
|
return filer.ProtoToText(writer, message)
|
|
}
|
|
|
|
func ensureMountDirectory(commandEnv *CommandEnv, dir string, nonEmpty bool, remoteConf *remote_pb.RemoteConf) error {
|
|
return commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
parent, name := util.FullPath(dir).DirAndName()
|
|
_, lookupErr := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: parent,
|
|
Name: name,
|
|
})
|
|
if lookupErr != nil {
|
|
if errors.Is(lookupErr, filer_pb.ErrNotFound) {
|
|
_, createErr := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
|
Directory: parent,
|
|
Entry: &filer_pb.Entry{
|
|
Name: name,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: time.Now().Unix(),
|
|
Crtime: time.Now().Unix(),
|
|
FileMode: uint32(0755 | os.ModeDir),
|
|
},
|
|
RemoteEntry: &filer_pb.RemoteEntry{
|
|
StorageName: remoteConf.Name,
|
|
},
|
|
},
|
|
})
|
|
return createErr
|
|
}
|
|
return lookupErr
|
|
}
|
|
|
|
mountToDirIsEmpty := true
|
|
listErr := filer_pb.SeaweedList(context.Background(), client, dir, "", func(entry *filer_pb.Entry, isLast bool) error {
|
|
mountToDirIsEmpty = false
|
|
return nil
|
|
}, "", false, 1)
|
|
|
|
if listErr != nil {
|
|
return fmt.Errorf("list %s: %v", dir, listErr)
|
|
}
|
|
|
|
if !mountToDirIsEmpty {
|
|
if !nonEmpty {
|
|
return fmt.Errorf("dir %s is not empty", dir)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// if an entry has synchronized metadata but has not synchronized content
|
|
//
|
|
// entry.Attributes.FileSize == entry.RemoteEntry.RemoteSize
|
|
// entry.Attributes.Mtime == entry.RemoteEntry.RemoteMtime
|
|
// entry.RemoteEntry.LastLocalSyncTsNs == 0
|
|
//
|
|
// if an entry has synchronized metadata but has synchronized content before
|
|
//
|
|
// entry.Attributes.FileSize == entry.RemoteEntry.RemoteSize
|
|
// entry.Attributes.Mtime == entry.RemoteEntry.RemoteMtime
|
|
// entry.RemoteEntry.LastLocalSyncTsNs > 0
|
|
//
|
|
// if an entry has synchronized metadata but has new updates
|
|
//
|
|
// entry.Attributes.Mtime * 1,000,000,000 > entry.RemoteEntry.LastLocalSyncTsNs
|
|
//
|
|
// The entry is written only while the filer still holds existingEntry; stale
|
|
// reports that it changed since it was read and nothing was written.
|
|
func doSaveRemoteEntry(client filer_pb.SeaweedFilerClient, localDir string, existingEntry *filer_pb.Entry, remoteEntry *filer_pb.RemoteEntry) (stale bool, err error) {
|
|
readEntry := proto.Clone(existingEntry).(*filer_pb.Entry)
|
|
existingEntry.RemoteEntry = remoteEntry
|
|
existingEntry.Attributes.FileSize = uint64(remoteEntry.RemoteSize)
|
|
existingEntry.Attributes.Mtime = remoteEntry.RemoteMtime
|
|
existingEntry.Attributes.Md5 = nil
|
|
existingEntry.Attributes.TtlSec = 0 // Remote entries should not have TTL
|
|
existingEntry.Extended = filer.MergeRemoteContentEncoding(remoteEntry, existingEntry.Extended)
|
|
existingEntry.Chunks = nil
|
|
existingEntry.Content = nil
|
|
return updateEntryIfUnchanged(context.Background(), client, localDir, existingEntry, readEntry)
|
|
}
|