mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-23 14:30:46 +02:00
* volume: never compact a volume tiered to remote storage A tiered volume's data backend is the remote object, but compaction reads the local .dat path. When tiering removed the local .dat, every vacuum of a tiered volume with garbage fails with "no such file or directory", and the master's sweep and the worker's vacuum task keep selecting it. When the local .dat was kept, the compaction succeeds against the stale copy and the commit reloads the volume to read the remote object (old layout) through the compacted index, so live needles stop reading. Mirror the Rust volume server (#11545): - CompactByIndex and CompactByVolumeData refuse a volume with a remote file before creating .cpd/.cpx ("volume N is tiered to remote storage, cannot compact"). - CommitCompact refuses under the data-file locks when the volume became tiered during the copy, and removes .cpd/.cpx. - Store.CheckCompactVolume reports no garbage for a tiered volume, so the master's vacuum and volume.vacuum do not select it. - The worker's vacuum detection skips a volume with a remote copy, as balance and EC detection already do. Tests (each fails on master): TestCompactRefusesTieredVolume, TestCommitCompactRefusesVolumeTieredDuringCopy (on master the commit succeeds and a surviving needle no longer reads), TestPickPrimaryReplicaSkipsTieredVolumes. * volume: skip tiered replicas in vacuum selection, keep their real ratio Reporting a zero garbage ratio for a tiered volume hid it from volume.tier.compact, whose preflight reads that ratio while the volume is still remote, and did not exclude it from a vacuum with a zero threshold. Report the real ratio again, as the Rust volume server does, and have the master's vacuum check skip a replica whose volume info names remote storage, whatever the threshold or request. When CommitCompact refuses a volume tiered during the copy, also close the temporary needle map and remove .cpldb: nothing else would, since no later vacuum selects the volume. The tiered-compaction test no longer depends on random needle sizes reaching a 0.3 garbage ratio. * volume tier: do not attach a copy of a .dat a compaction replaced The upload copies the .dat, then records the remote file in the .vif and switches the backend. A compaction commit in between swaps in a compacted .dat and index; the upload then served the old-layout remote object through the new index, so live needles stopped reading. CommitCompact's tiered check cannot see this: the volume is not tiered until the switch. AttachRemoteDatFile records the remote file and switches the backend under the data-file locks, and refuses when the backend is no longer the file that was copied. The upload then removes the unused remote copy. * volume tier: keep a committed .vif entry when only the dir fsync fails AttachRemoteDatFile rolled back the in-memory remote entry on any SaveVolumeInfo error. A NotCrashDurableError comes after the rename, so the .vif on disk already names the copy; with the entry gone, the upload handler deleted that object, and a reload would serve from a missing remote file. Keep the entry and switch to the copy as the .vif says, and still return the error so the caller keeps the local .dat. * vacuum worker: filter tiered replicas instead of skipping the volume A partially tiered volume is the leftover of an interrupted volume.tier.upload; skipping the whole volume left garbage on its local replicas unreclaimed. Per-replica filtering matches the master's batchVacuumVolumeCheck and is safe: a tiered replica refuses both compaction and commit. * filer: sync leveldb entry writes when the path rule asks for fsync The fsync storage rule only reached the volume upload; the metadata writes behind an acknowledged create, update or delete still went to the leveldb journal with Sync:false, so a power loss could roll back an acknowledged bucket or object entry. A write-side flag on the entry context carries the request: the filer sets it from the path's storage rule in CreateEntry, UpdateEntry, EnsureDirectoryEntry and DeleteEntryMetaAndData, and the HTTP write path sets it for an explicit ?fsync=true. The leveldb stores write with Sync:true when the flag is present. * volume: flush the .idx before acknowledging a durable write A write with fsync synced only the .dat: the index row written on publish stayed in the page cache, so a power loss could leave the needle's bytes durable with nothing indexing them. The durable paths now run one .idx sync after publish, before the requests complete — one per syncWrite and one per batch run, paired with the .dat sync, the same split the Rust volume server makes between flush_dat and flush_idx. A failed .idx sync cannot be rolled back, so it stops the volume taking writes and fails the requests instead of acknowledging an index entry that may not survive. --------- Co-authored-by: Javier de la Torre <jatorre@lakehousebox.com>
374 lines
12 KiB
Go
374 lines
12 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/constants"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
var (
|
|
OS_UID = uint32(os.Getuid())
|
|
OS_GID = uint32(os.Getgid())
|
|
|
|
// ErrReadOnly is filer_pb.ErrReadOnly so errors.Is matches on either side
|
|
// of the gRPC boundary.
|
|
ErrReadOnly = filer_pb.ErrReadOnly
|
|
)
|
|
|
|
type FilerPostResult struct {
|
|
Name string `json:"name,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func (fs *FilerServer) assignNewFileInfo(ctx context.Context, so *operation.StorageOption, expectedDataSize uint64) (fileId, urlLocation string, auth security.EncodedJwt, err error) {
|
|
|
|
stats.FilerHandlerCounter.WithLabelValues(stats.ChunkAssign).Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerRequestHistogram.WithLabelValues(stats.ChunkAssign).Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
ar, altRequest := so.ToAssignRequests(1)
|
|
ar.ExpectedDataSize = expectedDataSize
|
|
if altRequest != nil {
|
|
altRequest.ExpectedDataSize = expectedDataSize
|
|
}
|
|
|
|
// Use a context that ignores cancellation from the request context
|
|
assignCtx := context.WithoutCancel(ctx)
|
|
|
|
assignResult, ae := operation.Assign(assignCtx, fs.filer.GetMaster, fs.grpcDialOption, ar, altRequest)
|
|
if ae != nil {
|
|
glog.ErrorfCtx(ctx, "failing to assign a file id: %v", ae)
|
|
err = ae
|
|
return
|
|
}
|
|
fileId = assignResult.Fid
|
|
assignUrl := assignResult.Url
|
|
// Prefer same data center
|
|
if fs.option.DataCenter != "" {
|
|
for _, repl := range assignResult.Replicas {
|
|
if repl.DataCenter == fs.option.DataCenter {
|
|
assignUrl = repl.Url
|
|
break
|
|
}
|
|
}
|
|
}
|
|
urlLocation = "http://" + assignUrl + "/" + assignResult.Fid
|
|
if so.Fsync {
|
|
urlLocation += "?fsync=true"
|
|
}
|
|
auth = assignResult.Auth
|
|
return
|
|
}
|
|
|
|
func (fs *FilerServer) PostHandler(w http.ResponseWriter, r *http.Request, contentLength int64) {
|
|
ctx := r.Context()
|
|
|
|
// Match storage rules on the decoded path the entry is written to. The raw
|
|
// request-target is percent-encoded on the wire, so a rule on "/只读/" would
|
|
// never see "/%E5%8F%AA%E8%AF%BB/".
|
|
destination := r.URL.Path
|
|
headerDestination := r.Header.Get(s3_constants.SeaweedStorageDestinationHeader)
|
|
if headerDestination != "" {
|
|
destination = headerDestination
|
|
}
|
|
|
|
// The destination header picks storage rules for a logical destination, but
|
|
// the entry is written at r.URL.Path. Enforce the read-only/quota rule on the
|
|
// actual write path too, so the header cannot route a write into a read-only
|
|
// location.
|
|
if headerDestination != "" && fs.filer.FilerConf.MatchStorageRule(r.URL.Path).ReadOnly {
|
|
writeJsonError(w, r, http.StatusInsufficientStorage, ErrReadOnly)
|
|
return
|
|
}
|
|
|
|
query := r.URL.Query()
|
|
so, err := fs.detectStorageOption0(ctx, destination,
|
|
query.Get("collection"),
|
|
query.Get("replication"),
|
|
query.Get("ttl"),
|
|
query.Get("disk"),
|
|
query.Get("fsync"),
|
|
query.Get("dataCenter"),
|
|
query.Get("rack"),
|
|
query.Get("dataNode"),
|
|
query.Get("saveInside"),
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, ErrReadOnly) {
|
|
writeJsonError(w, r, http.StatusInsufficientStorage, err)
|
|
} else {
|
|
glog.V(1).InfolnCtx(ctx, "post", r.RequestURI, ":", err.Error())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if so.Fsync {
|
|
ctx = filer.WithSyncWrite(ctx)
|
|
}
|
|
|
|
if util.FullPath(r.URL.Path).IsLongerFileName(so.MaxFileNameLength) {
|
|
glog.V(1).InfolnCtx(ctx, "post", r.RequestURI, ": ", "entry name too long")
|
|
w.WriteHeader(http.StatusRequestURITooLong)
|
|
return
|
|
}
|
|
|
|
// When DiskType is empty,use filer's -disk
|
|
if so.DiskType == "" {
|
|
so.DiskType = fs.option.DiskType
|
|
}
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/etc") {
|
|
so.SaveInside = true
|
|
}
|
|
|
|
if query.Has("mv.from") {
|
|
fs.move(ctx, w, r, so)
|
|
} else if query.Has("cp.from") {
|
|
fs.copy(ctx, w, r, so)
|
|
} else if isExtractZip(r) {
|
|
fs.extractZipArchive(ctx, w, r)
|
|
} else {
|
|
fs.autoChunk(ctx, w, r, contentLength, so)
|
|
}
|
|
|
|
util_http.CloseRequest(r)
|
|
|
|
}
|
|
|
|
func (fs *FilerServer) move(ctx context.Context, w http.ResponseWriter, r *http.Request, so *operation.StorageOption) {
|
|
src := r.URL.Query().Get("mv.from")
|
|
dst := r.URL.Path
|
|
|
|
glog.V(2).InfofCtx(ctx, "FilerServer.move %v to %v", src, dst)
|
|
|
|
var err error
|
|
if src, err = clearName(src); err != nil {
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if dst, err = clearName(dst); err != nil {
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
src = strings.TrimRight(src, "/")
|
|
if src == "" {
|
|
err = fmt.Errorf("invalid source '/'")
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
srcPath := util.FullPath(src)
|
|
dstPath := util.FullPath(dst)
|
|
if dstPath.IsLongerFileName(so.MaxFileNameLength) {
|
|
err = fmt.Errorf("dst name to long")
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
srcEntry, err := fs.filer.FindEntry(ctx, srcPath)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to get src entry '%s', err: %s", src, err)
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
wormEnforced, err := fs.wormEnforcedForEntry(ctx, src)
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
} else if wormEnforced {
|
|
// you cannot move a worm file or directory
|
|
err = fmt.Errorf("cannot move write-once entry from '%s' to '%s': %s", src, dst, constants.ErrMsgOperationNotPermitted)
|
|
writeJsonError(w, r, http.StatusForbidden, err)
|
|
return
|
|
}
|
|
|
|
oldDir, oldName := srcPath.DirAndName()
|
|
newDir, newName := dstPath.DirAndName()
|
|
newName = util.Nvl(newName, oldName)
|
|
|
|
dstEntry, err := fs.filer.FindEntry(ctx, util.FullPath(strings.TrimRight(dst, "/")))
|
|
if err != nil && err != filer_pb.ErrNotFound {
|
|
err = fmt.Errorf("failed to get dst entry '%s', err: %s", dst, err)
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if err == nil && !dstEntry.IsDirectory() && srcEntry.IsDirectory() {
|
|
err = fmt.Errorf("move: cannot overwrite non-directory '%s' with directory '%s'", dst, src)
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
_, err = fs.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
|
|
OldDirectory: oldDir,
|
|
OldName: oldName,
|
|
NewDirectory: newDir,
|
|
NewName: newName,
|
|
})
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to move entry from '%s' to '%s', err: %s", src, dst, err)
|
|
writeJsonError(w, r, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// curl -X DELETE http://localhost:8888/path/to
|
|
// curl -X DELETE http://localhost:8888/path/to?recursive=true
|
|
// curl -X DELETE http://localhost:8888/path/to?recursive=true&ignoreRecursiveError=true
|
|
// curl -X DELETE http://localhost:8888/path/to?recursive=true&skipChunkDeletion=true
|
|
func (fs *FilerServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
isRecursive := r.FormValue("recursive") == "true"
|
|
if !isRecursive && fs.option.recursiveDelete {
|
|
if r.FormValue("recursive") != "false" {
|
|
isRecursive = true
|
|
}
|
|
}
|
|
ignoreRecursiveError := r.FormValue("ignoreRecursiveError") == "true"
|
|
skipChunkDeletion := r.FormValue("skipChunkDeletion") == "true"
|
|
|
|
objectPath := r.URL.Path
|
|
if len(r.URL.Path) > 1 && strings.HasSuffix(objectPath, "/") {
|
|
objectPath = objectPath[0 : len(objectPath)-1]
|
|
}
|
|
|
|
wormEnforced, err := fs.wormEnforcedForEntry(context.TODO(), objectPath)
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
} else if wormEnforced {
|
|
writeJsonError(w, r, http.StatusForbidden, errors.New(constants.ErrMsgOperationNotPermitted))
|
|
return
|
|
}
|
|
|
|
// The delete and the commit record it emits must serialize with peer
|
|
// replay of this path, same as the gRPC DeleteEntry handler.
|
|
fullPath := util.FullPath(objectPath)
|
|
if fs.entryLockTable != nil {
|
|
pathLock := fs.entryLockTable.AcquireLock("DeleteHandler", fullPath, util.ExclusiveLock)
|
|
defer fs.entryLockTable.ReleaseLock(fullPath, pathLock)
|
|
}
|
|
|
|
err = fs.filer.DeleteEntryMetaAndData(context.Background(), fullPath, isRecursive, ignoreRecursiveError, !skipChunkDeletion, false, nil, 0)
|
|
if err != nil && err != filer_pb.ErrNotFound {
|
|
glog.V(1).Infoln("deleting", objectPath, ":", err.Error())
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (fs *FilerServer) detectStorageOption(ctx context.Context, requestURI, qCollection, qReplication string, ttlSeconds int32, diskType, dataCenter, rack, dataNode string) (*operation.StorageOption, error) {
|
|
|
|
rule := fs.filer.FilerConf.MatchStorageRule(requestURI)
|
|
|
|
if rule.ReadOnly {
|
|
// Name the read-only prefix so the caller knows which path is locked and why.
|
|
// MatchStorageRule leaves LocationPrefix empty when several rules merge; fall back to the request path.
|
|
prefix := rule.LocationPrefix
|
|
if prefix == "" {
|
|
prefix = requestURI
|
|
}
|
|
return nil, fmt.Errorf("%w: %s (e.g. bucket over quota)", ErrReadOnly, prefix)
|
|
}
|
|
|
|
// Use local variable instead of mutating shared rule
|
|
maxFileNameLength := rule.MaxFileNameLength
|
|
if maxFileNameLength == 0 {
|
|
maxFileNameLength = fs.filer.MaxFilenameLength
|
|
}
|
|
|
|
// required by buckets folder
|
|
bucketDefaultCollection := ""
|
|
if strings.HasPrefix(requestURI, fs.filer.DirBucketsPath+"/") {
|
|
bucketDefaultCollection = fs.filer.DetectBucket(util.FullPath(requestURI))
|
|
}
|
|
|
|
if ttlSeconds == 0 {
|
|
ttl, err := needle.ReadTTL(rule.GetTtl())
|
|
if err != nil {
|
|
glog.ErrorfCtx(ctx, "fail to parse %s ttl setting %s: %v", rule.LocationPrefix, rule.Ttl, err)
|
|
}
|
|
ttlSeconds = int32(ttl.Minutes()) * 60
|
|
}
|
|
|
|
collection := util.Nvl(qCollection, rule.Collection, bucketDefaultCollection, fs.option.Collection)
|
|
|
|
// A placement overlay steers a bound collection's new volumes onto a tier.
|
|
// It sits between the explicit request and the filer.conf rule: it overrides
|
|
// the rule but yields to a value the caller asked for outright. Only a
|
|
// steered collection contributes values; an unsteered one clears them so it
|
|
// falls straight through to the rule.
|
|
overlayDisk, overlayReplication, overlayDataCenter, overlaySteered := fs.filer.ResolvePlacement(collection)
|
|
if !overlaySteered {
|
|
overlayDisk, overlayReplication, overlayDataCenter = "", "", ""
|
|
} else {
|
|
glog.V(4).InfofCtx(ctx, "placement overlay steers collection %s: disk=%q replication=%q dataCenter=%q",
|
|
collection, overlayDisk, overlayReplication, overlayDataCenter)
|
|
}
|
|
|
|
return &operation.StorageOption{
|
|
Replication: util.Nvl(qReplication, overlayReplication, rule.Replication, fs.option.DefaultReplication),
|
|
Collection: collection,
|
|
DataCenter: util.Nvl(dataCenter, overlayDataCenter, rule.DataCenter, fs.option.DataCenter),
|
|
Rack: util.Nvl(rack, rule.Rack, fs.option.Rack),
|
|
DataNode: util.Nvl(dataNode, rule.DataNode, fs.option.DataNode),
|
|
TtlSeconds: ttlSeconds,
|
|
DiskType: util.Nvl(diskType, overlayDisk, rule.DiskType),
|
|
Fsync: rule.Fsync,
|
|
VolumeGrowthCount: rule.VolumeGrowthCount,
|
|
MaxFileNameLength: maxFileNameLength,
|
|
// A location that survives from a request parameter, a placement
|
|
// overlay, or the path rule is a contract; only a bare filer default
|
|
// is a preference that may still be widened.
|
|
EnforceLocation: dataCenter != "" || overlayDataCenter != "" || rule.DataCenter != "" ||
|
|
rack != "" || rule.Rack != "" || dataNode != "" || rule.DataNode != "",
|
|
}, nil
|
|
}
|
|
|
|
func (fs *FilerServer) detectStorageOption0(ctx context.Context, requestURI, qCollection, qReplication string, qTtl string, diskType string, fsync string, dataCenter, rack, dataNode, saveInside string) (*operation.StorageOption, error) {
|
|
|
|
ttl, err := needle.ReadTTL(qTtl)
|
|
if err != nil {
|
|
glog.ErrorfCtx(ctx, "fail to parse ttl %s: %v", qTtl, err)
|
|
}
|
|
|
|
so, err := fs.detectStorageOption(ctx, requestURI, qCollection, qReplication, int32(ttl.Minutes())*60, diskType, dataCenter, rack, dataNode)
|
|
if so != nil {
|
|
if fsync == "false" {
|
|
so.Fsync = false
|
|
} else if fsync == "true" {
|
|
so.Fsync = true
|
|
}
|
|
if saveInside == "true" {
|
|
so.SaveInside = true
|
|
} else {
|
|
so.SaveInside = false
|
|
}
|
|
}
|
|
|
|
return so, err
|
|
}
|