mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 13:00:45 +02:00
* s3: commit versioned multipart upload in one transaction CompleteMultipartUpload wrote the version file, flipped the .versions pointer, then removed .uploads/<id> metadata-only as a best-effort post-commit step. A filer error or gateway crash in that window left the upload directory referencing the same chunks as the published object, and the next s3.clean.uploads run purged it with data -- corrupting a committed object. Put the version file, remove the upload directory metadata-only (its chunks are the object's chunks), and recompute the latest pointer in one ObjectTransaction under the object's per-path lock on the owner filer. The mutation order keeps every partial state safe: the chunks stay referenced at all times, and a published object never coexists with the upload directory the cleaner would purge. Unused part entries are freed before the transaction, since the metadata-only directory delete would otherwise leak their chunks. * s3: remove upload directory inside the multipart object PUT The same committed-object/stranded-upload window existed on the suspended and non-versioned paths: writeMultipartObject committed the object, then a best-effort rm dropped .uploads/<id>. Ride the metadata-only removal on the routed PUT itself so the two land in one transaction; the unrouted mkFile fallback keeps post-commit cleanup. * shell: purge completed uploads metadata-only in s3.clean.uploads A leftover .uploads/<id> can outlive a committed object when the completion's metadata-only delete fails or the gateway dies in between; its part entries then share chunks with the live object, and a recursive purge frees them out from under it. Before purging a stale upload, check whether it completed: the object entry or any version file under <key>.versions carrying the upload id. If so, delete with skipChunkDeletion. If the lookup fails, skip the upload for this run rather than risk live chunks. * s3: abort multipart completion when unused part cleanup fails Deleting the upload directory metadata-only erases the only metadata pointing at part entries whose deletion failed, orphaning their chunks. Propagate the error so the completion fails while the upload directory still exists and the request remains retriable. * s3: require the upload directory to exist at multipart commit A delete that does not take the object lock (abort, lifecycle, s3.clean.uploads) can remove .uploads/<id> and its chunks between the prepare step and the commit transaction. The commit now carries an IF_EXISTS precondition on the upload directory so the race fails the request with NoSuchUpload instead of publishing an object over freed chunks. * s3: keep the version file when the upload directory is gone The finalize transaction has no rollback, so a failure at the latest-pointer recompute leaves the version written and .uploads/<id> removed. Deleting the version then destroys the only remaining record of the upload, making a retried CompleteMultipartUpload return NoSuchUpload while the version's chunks leak. Roll back only while the upload directory survives; otherwise keep the version, which a retry resolves through SeaweedFSUploadId and the version reconciler promotes. * s3: keep manifests when a routed object write partially commits For non-versioned and suspended completions the object PUT precedes the upload-directory DELETE, so an error can mean the object entry exists while the response reports failure. Freeing this attempt's manifest chunks then destroys the committed object. Keep them when the object entry survived, and after a failed null-marker finalize which always follows a committed write. * s3: skip the keep-version path on precondition failure A rejected precondition means no mutation ran, so there is no version file to preserve and this attempt's manifests are orphans the error cleanup should free. * s3: keep manifests when the object-existence check itself fails A transient lookup error previously read as absent, letting the error cleanup free manifest chunks a committed object still references. * s3: keep the upload directory when post-commit part cleanup fails Removing it metadata-only after a failed entry delete erases the only reference to the leftover chunks. Leave the directory so the entries keep their chunk references for s3.clean.uploads or manual recovery. * pb: fix filer list entry counting on 32-bit int(limit) wraps to -1 on 386 when limit is math.MaxUint32, so the beyond-limit check discarded every streamed entry. Compare in uint64 instead; the semantics are unchanged on 64-bit platforms. * shell: resolve trailing-slash object keys in s3.clean.uploads Completion stores a key ending in / inside the directory it names (<bucket>/dir/dir), but FullPath+DirAndName on the normalized key looked one level too high. Deriving dir and name with path.Dir and path.Base mirrors getEntryNameAndDir so the completed-upload check finds the entry instead of purging its chunks. * s3: heal a suspended completion hidden behind a delete marker Removing .uploads/<id> inside the commit transaction means a failed finalizeSuspendedNullWrite leaves nothing to retry against: the object entry is committed but the marker still makes the key read as deleted, and a retried CompleteMultipartUpload can only report NoSuchUpload. When the upload directory is gone, check the regular path for an entry carrying the upload id and re-run the marker finalize, so the retry both succeeds and repairs the key. Only suspended buckets can hold this state; anything newer owns the key. * s3: report store errors when resuming a committed multipart object
171 lines
5.6 KiB
Go
171 lines
5.6 KiB
Go
package shell
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
func init() {
|
|
Commands = append(Commands, &commandS3CleanUploads{})
|
|
}
|
|
|
|
type commandS3CleanUploads struct{}
|
|
|
|
func (c *commandS3CleanUploads) Name() string {
|
|
return "s3.clean.uploads"
|
|
}
|
|
|
|
func (c *commandS3CleanUploads) Help() string {
|
|
return `clean up stale multipart uploads
|
|
|
|
Example:
|
|
s3.clean.uploads -timeAgo 1.5h
|
|
|
|
`
|
|
}
|
|
|
|
func (c *commandS3CleanUploads) HasTag(CommandTag) bool {
|
|
return false
|
|
}
|
|
|
|
func (c *commandS3CleanUploads) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
|
bucketCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
|
uploadedTimeAgo := bucketCommand.Duration("timeAgo", 24*time.Hour, "created time before now. \"1.5h\" or \"2h45m\". Valid time units are \"m\", \"h\"")
|
|
if err = bucketCommand.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
signingKey := util.GetViper().GetString("jwt.filer_signing.key")
|
|
|
|
var filerBucketsPath string
|
|
filerBucketsPath, err = readFilerBucketsPath(commandEnv)
|
|
if err != nil {
|
|
return fmt.Errorf("read buckets: %w", err)
|
|
}
|
|
|
|
var buckets []string
|
|
err = filer_pb.List(context.Background(), commandEnv, filerBucketsPath, "", func(entry *filer_pb.Entry, isLast bool) error {
|
|
buckets = append(buckets, entry.Name)
|
|
return nil
|
|
}, "", false, math.MaxUint32)
|
|
if err != nil {
|
|
return fmt.Errorf("list buckets under %v: %w", filerBucketsPath, err)
|
|
}
|
|
|
|
for _, bucket := range buckets {
|
|
if err := c.cleanupUploads(commandEnv, writer, filerBucketsPath, bucket, *uploadedTimeAgo, signingKey); err != nil {
|
|
fmt.Fprintf(writer, "failed cleanup uploads for bucket %s: %v", bucket, err)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (c *commandS3CleanUploads) cleanupUploads(commandEnv *CommandEnv, writer io.Writer, filerBucketsPath string, bucket string, timeAgo time.Duration, signingKey string) error {
|
|
uploadsDir := filerBucketsPath + "/" + bucket + "/" + s3_constants.MultipartUploadsFolder
|
|
var staleUploads []*filer_pb.Entry
|
|
now := time.Now()
|
|
err := filer_pb.List(context.Background(), commandEnv, uploadsDir, "", func(entry *filer_pb.Entry, isLast bool) error {
|
|
ctime := time.Unix(entry.Attributes.Crtime, 0)
|
|
if ctime.Add(timeAgo).Before(now) {
|
|
staleUploads = append(staleUploads, entry)
|
|
}
|
|
return nil
|
|
}, "", false, math.MaxUint32)
|
|
if err != nil {
|
|
return fmt.Errorf("list uploads under %v: %w", uploadsDir, err)
|
|
}
|
|
|
|
var encodedJwt security.EncodedJwt
|
|
if signingKey != "" {
|
|
encodedJwt = security.GenJwtForFilerServer(security.SigningKey(signingKey), 15*60)
|
|
}
|
|
|
|
for _, staleUpload := range staleUploads {
|
|
// A completed upload's part entries share chunks with the finished
|
|
// object, so purging their data corrupts it. Completion normally
|
|
// removes this directory itself; a survivor means that cleanup failed
|
|
// and only the metadata should go. An undecidable lookup is left for
|
|
// the next run rather than risk live chunks.
|
|
completed, checkErr := c.uploadCompleted(commandEnv, filerBucketsPath+"/"+bucket, staleUpload)
|
|
if checkErr != nil {
|
|
fmt.Fprintf(writer, "skip %s: %v\n", staleUpload.Name, checkErr)
|
|
continue
|
|
}
|
|
deleteUrl := fmt.Sprintf("http://%s%s/%s?recursive=true&ignoreRecursiveError=true", commandEnv.option.FilerAddress.ToHttpAddress(), uploadsDir, staleUpload.Name)
|
|
if completed {
|
|
deleteUrl += "&skipChunkDeletion=true"
|
|
}
|
|
fmt.Fprintf(writer, "purge %s\n", deleteUrl)
|
|
|
|
err = util_http.Delete(deleteUrl, string(encodedJwt))
|
|
if err != nil && err.Error() != "" {
|
|
return fmt.Errorf("purge %s/%s: %v", uploadsDir, staleUpload.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// uploadCompleted reports whether the upload assembled into an object: the
|
|
// object entry, or any version file under <key>.versions, still carries the
|
|
// upload id completion stamps on it.
|
|
func (c *commandS3CleanUploads) uploadCompleted(filerClient filer_pb.FilerClient, bucketDir string, upload *filer_pb.Entry) (bool, error) {
|
|
objectKey := string(upload.Extended[s3_constants.ExtMultipartObjectKey])
|
|
if objectKey == "" {
|
|
return false, nil
|
|
}
|
|
// Derive the object location the same way completion's getEntryNameAndDir
|
|
// does: a trailing-slash key stores the object inside the directory it
|
|
// names, so FullPath+DirAndName would look one level too high.
|
|
name := path.Base(objectKey)
|
|
dir := path.Dir(objectKey)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
objectDir := util.FullPath(bucketDir + "/" + dir)
|
|
|
|
completed := false
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
resp, err := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{Directory: string(objectDir), Name: name})
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Entry != nil && string(resp.Entry.Extended[s3_constants.SeaweedFSUploadId]) == upload.Name {
|
|
completed = true
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil || completed {
|
|
return completed, err
|
|
}
|
|
|
|
err = filer_pb.List(context.Background(), filerClient, string(objectDir)+"/"+name+s3_constants.VersionsFolder, "", func(entry *filer_pb.Entry, isLast bool) error {
|
|
if string(entry.Extended[s3_constants.SeaweedFSUploadId]) == upload.Name {
|
|
completed = true
|
|
}
|
|
return nil
|
|
}, "", false, math.MaxUint32)
|
|
if err != nil && (errors.Is(err, filer_pb.ErrNotFound) || strings.Contains(err.Error(), filer_pb.ErrNotFound.Error())) {
|
|
return false, nil
|
|
}
|
|
return completed, err
|
|
}
|