Files
seaweedfs/weed/storage/backend/rclone_backend/rclone_backend.go
T
87332eb60b Cloud/remote storage & tiering: configurable multipart upload/download concurrency (#11319)
* pb: add multipart concurrency fields to RemoteConf and tier move requests

RemoteConf gains upload_concurrency/download_concurrency (0 = client
default); VolumeTierMoveDatToRemote/FromRemote requests gain a
concurrency field (0 = backend default).

* remote storage: honor RemoteConf upload/download concurrency in s3 and azure clients

s3 client: ReadFile passes conf download_concurrency to the downloader,
WriteFile uses upload_concurrency for the uploader; previously
hard-coded 1 upload / 5 download parts. 0 keeps defaults. Same for
azure client.

* storage: plumb concurrency through backend interface and tier upload/download

BackendStorage.CopyFile/DownloadFile take a concurrency hint (<=0 =
backend configured default); s3 backend reads
upload_concurrency/download_concurrency from scaffold config with
parseConcurrency fallback, rclone updated to the new signature. Tier
move gRPC handlers forward the request concurrency to the backend.

* shell: -upload_concurrency/-download_concurrency for remote.configure, -concurrent for volume.tier

remote.configure exposes upload/download concurrency persisted into
RemoteConf; volume.tier move/evict commands forward -concurrent to the
tier move requests. Documented in master-cloud.toml scaffold.

* test: cover concurrency propagation in remote tier integration test

* remote.configure: merge existing config on partial update

Load the stored RemoteConf before saving so a partial update (e.g. only
-upload_concurrency) preserves credentials, endpoints, and type instead
of replacing them with new-config defaults. Only treat a confirmed
ErrNotFound as a new configuration; propagate all other load errors so a
transient filer failure does not overwrite stored settings.

On a type transition, reset backend-specific fields to the destination
type's new-config defaults rather than inheriting the old backend's
empty values. Bound configured concurrency to a sane maximum.

* remote storage: honor configured download concurrency in S3 and Azure

ReadFileWithConcurrency now resolves a zero request override against the
client's configured download_concurrency (new downloadConcurrency()
helpers), so the remote-mount/cache read path honors
RemoteConf.DownloadConcurrency instead of the hard-coded default.

Azure also clamps the resolved value to math.MaxUint16 regardless of
whether the fallback was used, preventing uint16 wraparound when a
configured value exceeds 65535.

* shell: rename -concurrent to -concurrency and validate tier transfer bounds

Rename the -concurrent flag to -concurrency across volume.tier.upload,
volume.tier.download, and volume.tier.compact to match the proto field and
RemoteConf field names. Add validateTierConcurrency to reject values that
would wrap int32 or exceed a 1024 cap before constructing the request.

* server: clamp tier move concurrency in gRPC handlers

Add clampTierConcurrency to both VolumeTierMoveDatToRemote and
VolumeTierMoveDatFromRemote handlers so a direct gRPC caller cannot spawn
an unbounded number of network workers.

* trim verbose comments added with concurrency feature

Remove redundant doc comments on the backend interface, rclone backend,
s3_backend parseConcurrency, and test helpers that restated the obvious.

* remote.configure: apply type defaults before re-parse so explicit flags win

applyTypeDefaults ran after the second flag parse, overwriting explicit
destination flags (e.g. -s3.region=eu-west-1) with new-config defaults.
Move the type-transition default reset before the re-parse so user-supplied
flags override the destination defaults.

* remote.configure: only treat explicit -type as a type transition

The first parse defaults -type to s3, so a concurrency-only update on an
existing non-S3 config captured requestedType=s3 and wrongly triggered a
type transition, resetting the stored backend to S3. Use fs.Visit to
detect whether -type was explicitly supplied; an omitted -type keeps the
stored backend.

---------

Co-authored-by: Jack Meredith <9480542+jackusm@users.noreply.github.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-09-14 22:09:08 -07:00

296 lines
7.2 KiB
Go

//go:build rclone
package rclone_backend
import (
"bytes"
"context"
"fmt"
"io"
"os"
"text/template"
"time"
"github.com/google/uuid"
_ "github.com/rclone/rclone/backend/all"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/object"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func init() {
backend.BackendStorageFactories["rclone"] = &RcloneBackendFactory{}
configfile.Install()
}
type RcloneBackendFactory struct {
}
func (factory *RcloneBackendFactory) StorageType() backend.StorageType {
return "rclone"
}
func (factory *RcloneBackendFactory) BuildStorage(configuration backend.StringProperties, configPrefix string, id string) (backend.BackendStorage, error) {
return newRcloneBackendStorage(configuration, configPrefix, id)
}
type RcloneBackendStorage struct {
id string
remoteName string
keyTemplate *template.Template
keyTemplateText string
fs fs.Fs
}
func newRcloneBackendStorage(configuration backend.StringProperties, configPrefix string, id string) (s *RcloneBackendStorage, err error) {
s = &RcloneBackendStorage{}
s.id = id
s.remoteName = configuration.GetString(configPrefix + "remote_name")
s.keyTemplateText = configuration.GetString(configPrefix + "key_template")
s.keyTemplate, err = template.New("keyTemplate").Parse(s.keyTemplateText)
if err != nil {
return
}
ctx := context.TODO()
accounting.Start(ctx)
fsPath := fmt.Sprintf("%s:", s.remoteName)
s.fs, err = fs.NewFs(ctx, fsPath)
if err != nil {
glog.Errorf("failed to instantiate Rclone filesystem: %s", err)
return
}
glog.V(0).Infof("created backend storage rclone.%s for remote name %s", s.id, s.remoteName)
return
}
func (s *RcloneBackendStorage) ToProperties() map[string]string {
m := make(map[string]string)
m["remote_name"] = s.remoteName
if len(s.keyTemplateText) > 0 {
m["key_template"] = s.keyTemplateText
}
return m
}
func formatKey(key string, storage RcloneBackendStorage) (fKey string, err error) {
var b bytes.Buffer
if len(storage.keyTemplateText) == 0 {
fKey = key
} else {
err = storage.keyTemplate.Execute(&b, key)
if err == nil {
fKey = b.String()
}
}
return
}
func (s *RcloneBackendStorage) NewStorageFile(key string, tierInfo *volume_server_pb.VolumeInfo) backend.BackendStorageFile {
f := &RcloneBackendStorageFile{
backendStorage: s,
key: key,
tierInfo: tierInfo,
}
return f
}
func (s *RcloneBackendStorage) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error) {
randomUuid, err := uuid.NewRandom()
if err != nil {
return key, 0, err
}
key = randomUuid.String()
key, err = formatKey(key, *s)
if err != nil {
return key, 0, err
}
glog.V(1).Infof("copy dat file of %s to remote rclone.%s as %s", f.Name(), s.id, key)
util.Retry("upload via Rclone", func() error {
size, err = uploadViaRclone(s.fs, f.Name(), key, fn)
return err
})
return
}
func uploadViaRclone(rfs fs.Fs, filename string, key string, fn func(progressed int64, percentage float32) error) (fileSize int64, err error) {
ctx := context.TODO()
file, err := os.Open(filename)
if err != nil {
return 0, err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return 0, err
}
info := object.NewStaticObjectInfo(key, stat.ModTime(), stat.Size(), true, nil, rfs)
tr := accounting.NewStats(ctx).NewTransfer(info, rfs)
defer tr.Done(ctx, err)
acc := tr.Account(ctx, file)
pr := ProgressReader{acc: acc, tr: tr, fn: fn}
obj, err := rfs.Put(ctx, &pr, info)
if err != nil {
return 0, err
}
return obj.Size(), err
}
func (s *RcloneBackendStorage) DownloadFile(filename string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error) {
glog.V(1).Infof("download dat file of %s from remote rclone.%s as %s", filename, s.id, key)
util.Retry("download via Rclone", func() error {
size, err = downloadViaRclone(s.fs, filename, key, fn)
return err
})
return
}
var createRcloneDownloadFile = func(filename string) (io.WriteCloser, error) {
return os.Create(filename)
}
func downloadViaRclone(fs fs.Fs, filename string, key string, fn func(progressed int64, percentage float32) error) (fileSize int64, err error) {
ctx := context.TODO()
obj, err := fs.NewObject(ctx, key)
if err != nil {
return 0, err
}
rc, err := obj.Open(ctx)
if err != nil {
return 0, err
}
defer rc.Close()
file, err := createRcloneDownloadFile(filename)
if err != nil {
return 0, err
}
tr := accounting.NewStats(ctx).NewTransfer(obj, fs)
defer func() {
// fsync the .dat before closing so its content is durable before the caller
// trims the remote reference and deletes the shared remote object.
if syncer, ok := file.(interface{ Sync() error }); ok {
if syncErr := syncer.Sync(); err == nil && syncErr != nil {
err = syncErr
}
}
if closeErr := file.Close(); err == nil && closeErr != nil {
err = closeErr
}
tr.Done(ctx, err)
}()
acc := tr.Account(ctx, rc)
pr := ProgressReader{acc: acc, tr: tr, fn: fn}
written, err := io.Copy(file, &pr)
if err != nil {
return 0, err
}
return written, nil
}
func (s *RcloneBackendStorage) DeleteFile(key string) (err error) {
glog.V(1).Infof("delete dat file %s from remote", key)
util.Retry("delete via Rclone", func() error {
err = deleteViaRclone(s.fs, key)
return err
})
return
}
func deleteViaRclone(fs fs.Fs, key string) (err error) {
ctx := context.TODO()
obj, err := fs.NewObject(ctx, key)
if err != nil {
return err
}
return obj.Remove(ctx)
}
type RcloneBackendStorageFile struct {
backendStorage *RcloneBackendStorage
key string
tierInfo *volume_server_pb.VolumeInfo
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) ReadAt(p []byte, off int64) (n int, err error) {
ctx := context.TODO()
obj, err := rcloneBackendStorageFile.backendStorage.fs.NewObject(ctx, rcloneBackendStorageFile.key)
if err != nil {
return 0, err
}
opt := fs.RangeOption{Start: off, End: off + int64(len(p)) - 1}
rc, err := obj.Open(ctx, &opt)
if err != nil {
return 0, err
}
defer rc.Close()
return io.ReadFull(rc, p)
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) WriteAt(p []byte, off int64) (n int, err error) {
panic("not implemented")
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) Truncate(off int64) error {
panic("not implemented")
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) Close() error {
return nil
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) GetStat() (datSize int64, modTime time.Time, err error) {
files := rcloneBackendStorageFile.tierInfo.GetFiles()
if len(files) == 0 {
err = fmt.Errorf("remote file info not found")
return
}
datSize = int64(files[0].FileSize)
modTime = time.Unix(int64(files[0].ModifiedTime), 0)
return
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) Name() string {
return rcloneBackendStorageFile.key
}
func (rcloneBackendStorageFile RcloneBackendStorageFile) Sync() error {
return nil
}