mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-29 11:15:34 +00:00
* cluster: never broadcast an empty lock ring An empty member list is never a usable ring state, but a delayed RemoveServer on a former leader can fire after the new leader already broadcast the recovered ring. That late broadcast carries a newer wall-clock version, so clients accept the empty ring and permanently reject the good one. Skip the broadcast entirely when the member list is empty, keeping the last non-empty snapshot for reconnecting clients. * cluster: periodically rebroadcast the lock ring Ring updates are purely event-driven, so one lost or poisoned update is permanent until the next membership change — with a single filer that may never come. Re-arm a per-group timer after every broadcast so the current leader keeps re-sending the ring; clients reject nothing newer than their last accepted version, so a re-sent snapshot always heals a stale view. * filer,s3api: reset the lock ring on master change Ring versions are per-master monotonic — each master stamps wall-clock nanoseconds — so a late high-version update accepted from a former leader makes the new leader's snapshot look stale forever. Detect a leader change across the reconnect gap (currentMaster is cleared between attempts, so remember the last served master) and reset the ring to bootstrap state so the new leader's view always applies. * cluster: fail lock acquisition when no lock server exists retryUntilLocked loops forever, so a filer reporting an empty lock ring wedges every append write indefinitely. Bound only the "no lock server found" case — ordinary contention is still waited out since the holder releases eventually. The constructors now return nil on failure: the filer append path and S3 object writes fail fast, while mounts degrade to their existing lockless mode. * cluster: reset only the ring version on master change Ring versions are per-master monotonic, so a version gate reset is all a leader change needs. Clearing the whole ring made every filer its own write owner until the next update and dropped the prior-owner window for keys the new leader remaps; the last ring now keeps routing until the new leader's snapshot transitions off it. * cluster: skip redundant ring installs and defer rebroadcasts An unchanged member list now only bumps the accepted version instead of installing a snapshot: periodic rebroadcasts no longer fire the topology-change callback or restart the prior-owner window. And a rebroadcast that lands inside a membership stabilization window yields to the pending timer rather than publishing an intermediate ring. * cluster,mount: bound lock unavailability, fail ops that cannot lock Only 'lock already owned' contention retries without bound now; every other failure — no lock server, or a dead ring member refusing connections — shares the same unavailability budget, so a ring naming departed filers can no longer hang a lock forever. Mount open-write, create, and rename fail with EAGAIN when the required lock cannot be acquired instead of proceeding without cross-mount serialization. * cluster: check pending stabilization inside the broadcast critical section rebroadcast released the mutex between the pending-timer check and nextBroadcastUpdate, so a membership change arriving in the gap could arm a stabilization timer while the rebroadcast emitted an intermediate ring. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: acquire path locks before mutating create/rename state Create took the DLM lock only after the filer create, so a lock failure returned EAGAIN with an eagerly persisted file left behind. Rename marked source handles renamed before acquiring locks, so a failed acquisition left them suppressing old-path flushes for a rename that never happened. Both now take the locks first; the create's lock is released again if the entry race loses to another creator and AcquireHandle takes over. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep the old-path lock when rename lock migration fails The migration stopped the handle's lock before acquiring the replacement, so a nil result left the handle writing with no lock at all. Acquiring the new-path lock first means failure keeps the existing lock instead of reporting success with serialization dropped. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: skip new-path rename lock when a handle already holds it A target file open for write on this mount already carries a lock on newPath; the lock manager does not grant a second lock to the same owner, so the rename would wait on itself until the handle closed. Also avoid locking twice when old and new paths coincide. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: hand the rename's target lock to the migrating handle The rename holds a lock on newPath for its duration, so the response migration's fresh acquisition waited on that same lock until the handle released — under fhLockTable, blocking the handle's own close. Adopt the rename's lock directly; nested move responses still acquire their own. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: move the replaced target's lock to the renamed handle When the target path was already locked by an open handle on this mount, the migrated source handle kept only its stale old-path lock — the target's close would then release the last lock on the new path while the renamed handle was still open. Adopt the replaced handle's lock instead. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: stop the handle lock inside the fh lock on release ReleaseHandle stopped fh.dlmLock before taking the fhLockTable slot, so a rename migration holding that slot could still observe and adopt a lock that was already stopping. Stopping under the fh lock makes the transfer serialize against the release. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: claim the replaced target's lock for the renamed handle When the target path is already locked by an open handle on this mount, adopting it at migration time keeps the renamed path protected after that handle closes, without waiting on a lock this mount already holds. If the handle was released mid-migration the claimed lock is stopped, and a fresh acquire covers the case where it was already gone. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: claim the target handle's lock before the rename runs Skipping the new-path lock when a handle already holds it let that handle's close release the lock mid-rename, leaving the path unguarded until the response migrated it. Take over the lock at check time and hold it for the rename's duration: the response adopts it for the migrating handle, or it returns to the target handle / is released on failure. The target handle lookup also falls back to the entry's stored inode for a forgotten path mapping. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: read handle locks only under the fh lock during rename The loose dlmLock reads raced ReleaseHandle, which now mutates the lock inside the handle lock; check and claim it under the same hold. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
985 lines
36 KiB
Go
985 lines
36 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/cluster"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// fencedFindEntry reads an entry with an exact log-position fence: the stamp
|
|
// and the read share the path lock mutations hold across write and notify, so
|
|
// every event at or below the fence is in the entry and none above it are.
|
|
// Any read whose result a client versions must go through here.
|
|
func (fs *FilerServer) fencedFindEntry(ctx context.Context, path util.FullPath) (entry *filer.Entry, logTsNs int64, err error) {
|
|
pathLock := fs.entryLockTable.AcquireLock("fencedFindEntry", path, util.SharedLock)
|
|
logTsNs = time.Now().UnixNano()
|
|
entry, err = fs.filer.FindEntry(ctx, path)
|
|
fs.entryLockTable.ReleaseLock(path, pathLock)
|
|
return
|
|
}
|
|
|
|
func (fs *FilerServer) LookupDirectoryEntry(ctx context.Context, req *filer_pb.LookupDirectoryEntryRequest) (*filer_pb.LookupDirectoryEntryResponse, error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "LookupDirectoryEntry %s", filepath.Join(req.Directory, req.Name))
|
|
|
|
entry, logTsNs, err := fs.fencedFindEntry(ctx, util.JoinPath(req.Directory, req.Name))
|
|
|
|
if err == filer_pb.ErrNotFound {
|
|
return &filer_pb.LookupDirectoryEntryResponse{LogTsNs: logTsNs, LogSignature: fs.filer.Signature}, err
|
|
}
|
|
if err != nil {
|
|
glog.V(3).InfofCtx(ctx, "LookupDirectoryEntry %s: %+v, ", filepath.Join(req.Directory, req.Name), err)
|
|
return nil, err
|
|
}
|
|
|
|
return &filer_pb.LookupDirectoryEntryResponse{
|
|
Entry: entry.ToProtoEntry(),
|
|
LogTsNs: logTsNs,
|
|
LogSignature: fs.filer.Signature,
|
|
}, nil
|
|
}
|
|
|
|
func (fs *FilerServer) ListEntries(req *filer_pb.ListEntriesRequest, stream filer_pb.SeaweedFiler_ListEntriesServer) (err error) {
|
|
|
|
glog.V(4).Infof("ListEntries %v", req)
|
|
|
|
limit := int(req.Limit)
|
|
if limit == 0 {
|
|
limit = fs.option.DirListingLimit
|
|
}
|
|
|
|
paginationLimit := filer.PaginationSize
|
|
if limit < paginationLimit {
|
|
paginationLimit = limit
|
|
}
|
|
|
|
lastFileName := req.StartFromFileName
|
|
includeLastFile := req.InclusiveStartFrom
|
|
snapshotTsNs := req.SnapshotTsNs
|
|
if snapshotTsNs == 0 {
|
|
snapshotTsNs = time.Now().UnixNano()
|
|
}
|
|
sentSnapshot := false
|
|
var listErr error
|
|
for limit > 0 {
|
|
var hasEntries bool
|
|
lastFileName, listErr = fs.filer.StreamListDirectoryEntries(stream.Context(), util.FullPath(req.Directory), lastFileName, includeLastFile, int64(paginationLimit), req.Prefix, "", "", func(entry *filer.Entry) (bool, error) {
|
|
hasEntries = true
|
|
pbEntry := entry.ToProtoEntry()
|
|
if req.OmitChunks {
|
|
// Stamp the size before dropping the only other thing carrying
|
|
// it. Most stores fold the chunk extents into FileSize when they
|
|
// decode, but one that keeps entries as JSON rather than as an
|
|
// encoded Entry never re-derives it, and the caller would be
|
|
// left with a zero. The entries are still read whole, because
|
|
// expiring one here deletes its data and that needs the chunks.
|
|
if pbEntry.Attributes != nil {
|
|
pbEntry.Attributes.FileSize = filer.FileSize(pbEntry)
|
|
}
|
|
pbEntry.Chunks = nil
|
|
}
|
|
resp := &filer_pb.ListEntriesResponse{
|
|
Entry: pbEntry,
|
|
}
|
|
if !sentSnapshot {
|
|
resp.SnapshotTsNs = snapshotTsNs
|
|
sentSnapshot = true
|
|
}
|
|
if err = stream.Send(resp); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
limit--
|
|
if limit == 0 {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
if listErr != nil {
|
|
return listErr
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !hasEntries {
|
|
break
|
|
}
|
|
|
|
includeLastFile = false
|
|
|
|
}
|
|
|
|
// No snapshot-only response for empty directories: many consumers (Java
|
|
// FilerClient, S3 listing) treat any response as an entry. The trailer
|
|
// carries it instead; older clients ignore trailers.
|
|
stream.SetTrailer(metadata.Pairs(filer_pb.ListSnapshotTsNsTrailerKey, strconv.FormatInt(snapshotTsNs, 10)))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (fs *FilerServer) LookupVolume(ctx context.Context, req *filer_pb.LookupVolumeRequest) (*filer_pb.LookupVolumeResponse, error) {
|
|
|
|
resp := &filer_pb.LookupVolumeResponse{
|
|
LocationsMap: make(map[string]*filer_pb.Locations),
|
|
}
|
|
|
|
// Use master client's lookup with fallback - it handles cache and master query
|
|
vidLocations, err := fs.filer.MasterClient.LookupVolumeIdsWithFallback(ctx, req.VolumeIds)
|
|
|
|
// Convert wdclient.Location to filer_pb.Location
|
|
// Return partial results even if there was an error
|
|
for vidString, locations := range vidLocations {
|
|
resp.LocationsMap[vidString] = &filer_pb.Locations{
|
|
Locations: wdclientLocationsToPb(locations),
|
|
}
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
// wdclientLocationsToPb converts the wdclient's internal Location entries
|
|
// (carrying DataInRemote and grpc-port metadata) to the protobuf form served
|
|
// by the filer gRPC API, preserving the fields the lookup client uses to
|
|
// prefer a local replica over a remote-tiered one.
|
|
func wdclientLocationsToPb(locations []wdclient.Location) []*filer_pb.Location {
|
|
locs := make([]*filer_pb.Location, 0, len(locations))
|
|
for _, loc := range locations {
|
|
locs = append(locs, &filer_pb.Location{
|
|
Url: loc.Url,
|
|
PublicUrl: loc.PublicUrl,
|
|
GrpcPort: uint32(loc.GrpcPort),
|
|
DataCenter: loc.DataCenter,
|
|
DataInRemote: loc.DataInRemote,
|
|
})
|
|
}
|
|
return locs
|
|
}
|
|
|
|
func (fs *FilerServer) lookupFileId(ctx context.Context, fileId string) (targetUrls []string, err error) {
|
|
fid, err := needle.ParseFileIdFromString(fileId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
locations, found := fs.filer.MasterClient.GetLocations(uint32(fid.VolumeId))
|
|
if !found || len(locations) == 0 {
|
|
return nil, fmt.Errorf("not found volume %d in %s", fid.VolumeId, fileId)
|
|
}
|
|
for _, loc := range locations {
|
|
targetUrls = append(targetUrls, fmt.Sprintf("http://%s/%s", loc.Url, fileId))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (fs *FilerServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (resp *filer_pb.CreateEntryResponse, err error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "CreateEntry %v/%v", req.Directory, req.Entry.Name)
|
|
if len(req.Entry.HardLinkId) > 0 {
|
|
glog.V(4).InfofCtx(ctx, "CreateEntry %s/%s with HardLinkId %x counter=%d", req.Directory, req.Entry.Name, req.Entry.HardLinkId, req.Entry.HardLinkCounter)
|
|
}
|
|
|
|
resp = &filer_pb.CreateEntryResponse{}
|
|
|
|
// An exclusive or conditional create is a read-then-write that the per-path
|
|
// lock below only makes atomic on this filer, while the store's insert is an
|
|
// upsert. Route it to the entry's ring owner so one filer's lock arbitrates
|
|
// every creator cluster-wide; is_moved bounds this to one hop. Plain creates
|
|
// are upserts either way and stay local.
|
|
if !req.IsMoved && (req.OExcl || conditionIsSet(req.Condition)) {
|
|
fullpath := util.NewFullPath(req.Directory, req.Entry.Name)
|
|
// Held apart from the named resp, which the local path below writes into:
|
|
// a failed forward must not leave it nil.
|
|
var ownerResp *filer_pb.CreateEntryResponse
|
|
handled, forwardErr := fs.forwardToWriteOwner(ctx, entryRouteKey(fullpath), func(owner pb.ServerAddress) error {
|
|
glog.V(2).InfofCtx(ctx, "CreateEntry %s: forwarding to owner %s", fullpath, owner)
|
|
req.IsMoved = true
|
|
return pb.WithFilerClient(false, 0, owner, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
forwarded, e := client.CreateEntry(ctx, req)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
ownerResp = forwarded
|
|
return nil
|
|
})
|
|
})
|
|
if handled {
|
|
if forwardErr != nil {
|
|
return &filer_pb.CreateEntryResponse{}, forwardErr
|
|
}
|
|
return ownerResp, nil
|
|
}
|
|
}
|
|
|
|
chunks, garbage, err2 := fs.cleanupChunks(ctx, util.Join(req.Directory, req.Entry.Name), nil, req.Entry)
|
|
if err2 != nil {
|
|
return &filer_pb.CreateEntryResponse{}, fmt.Errorf("CreateEntry cleanupChunks %s %s: %v", req.Directory, req.Entry.Name, err2)
|
|
}
|
|
|
|
newEntry := filer.FromPbEntry(req.Directory, req.Entry)
|
|
newEntry.Chunks = chunks
|
|
so, err := fs.applyStorageDefaultsToEntry(ctx, newEntry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Serialize concurrent mutations to the same path on this filer so the
|
|
// read (existence/condition) and the write are atomic. Callers route a
|
|
// key's writes to this owner filer, making this local lock sufficient.
|
|
fullpath := newEntry.FullPath
|
|
pathLock := fs.entryLockTable.AcquireLock("CreateEntry", fullpath, util.ExclusiveLock)
|
|
defer fs.entryLockTable.ReleaseLock(fullpath, pathLock)
|
|
|
|
// Fence stamped under the lock: a no-op create returns no event, but the
|
|
// acknowledged state still reflects everything at or below this.
|
|
resp.LogTsNs = time.Now().UnixNano()
|
|
resp.LogSignature = fs.filer.Signature
|
|
|
|
// Evaluate the optional precondition against the current entry while the
|
|
// path lock is held, so the check and the write are atomic on this filer.
|
|
// The fetched entry is then handed to CreateEntry below so it does not look
|
|
// the same path up again under the lock.
|
|
var existing *filer.Entry
|
|
if conditionIsSet(req.Condition) {
|
|
current, findErr := fs.filer.FindEntry(ctx, fullpath)
|
|
if findErr != nil && findErr != filer_pb.ErrNotFound {
|
|
return &filer_pb.CreateEntryResponse{}, fmt.Errorf("CreateEntry condition check %s: %w", fullpath, findErr)
|
|
}
|
|
if findErr == filer_pb.ErrNotFound {
|
|
current = nil
|
|
}
|
|
if !writeConditionSatisfied(req.Condition, current) {
|
|
glog.V(3).InfofCtx(ctx, "CreateEntry %s: precondition failed: %v", fullpath, req.Condition)
|
|
return &filer_pb.CreateEntryResponse{
|
|
Error: "precondition failed",
|
|
ErrorCode: filer_pb.FilerError_PRECONDITION_FAILED,
|
|
}, nil
|
|
}
|
|
existing = current
|
|
}
|
|
|
|
ctx, eventSink := filer.WithMetadataEventSink(ctx)
|
|
createErr := fs.filer.CreateEntry(ctx, newEntry, existing, req.OExcl, req.IsFromOtherCluster, req.Signatures, req.SkipCheckParentDirectory, so.MaxFileNameLength)
|
|
|
|
if createErr == nil {
|
|
fs.filer.DeleteChunksNotRecursive(garbage)
|
|
resp.MetadataEvent = eventSink.Last()
|
|
} else {
|
|
glog.V(3).InfofCtx(ctx, "CreateEntry %s: %v", filepath.Join(req.Directory, req.Entry.Name), createErr)
|
|
resp.Error = createErr.Error()
|
|
switch {
|
|
case errors.Is(createErr, filer_pb.ErrEntryNameTooLong):
|
|
resp.ErrorCode = filer_pb.FilerError_ENTRY_NAME_TOO_LONG
|
|
case errors.Is(createErr, filer_pb.ErrParentIsFile):
|
|
resp.ErrorCode = filer_pb.FilerError_PARENT_IS_FILE
|
|
case errors.Is(createErr, filer_pb.ErrExistingIsDirectory):
|
|
resp.ErrorCode = filer_pb.FilerError_EXISTING_IS_DIRECTORY
|
|
case errors.Is(createErr, filer_pb.ErrExistingIsFile):
|
|
resp.ErrorCode = filer_pb.FilerError_EXISTING_IS_FILE
|
|
case errors.Is(createErr, filer_pb.ErrEntryAlreadyExists):
|
|
resp.ErrorCode = filer_pb.FilerError_ENTRY_ALREADY_EXISTS
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ObjectTransaction applies an ordered list of entry mutations atomically with
|
|
// respect to other writers of the same object, by holding the per-path lock on
|
|
// lock_key for the whole call. The optional condition is checked first, against
|
|
// the entry at lock_key. This lets a caller describe a multi-entry object
|
|
// operation (e.g. delete the null version + write a delete marker + flip the
|
|
// latest pointer) as one request, replacing a distributed lock held across
|
|
// several RPCs. Callers must route the object's writes to its owner filer for
|
|
// the lock to be authoritative.
|
|
func (fs *FilerServer) ObjectTransaction(ctx context.Context, req *filer_pb.ObjectTransactionRequest) (*filer_pb.ObjectTransactionResponse, error) {
|
|
if req.LockKey == "" {
|
|
return &filer_pb.ObjectTransactionResponse{Error: "lock_key is required"}, nil
|
|
}
|
|
|
|
// Route-by-key: if this filer is not the ring owner of route_key, forward the
|
|
// whole transaction to the owner so its per-path lock is the single
|
|
// serialization point — even when the caller's ring view was stale. is_moved
|
|
// bounds this to one hop: a forwarded transaction is applied locally, so two
|
|
// filers that disagree on the owner during a ring change cannot loop.
|
|
if req.RouteKey != "" && !req.IsMoved {
|
|
// Rebuild rather than copy the request struct (it carries a mutex); the
|
|
// pointer/slice fields are shared since the original is not mutated.
|
|
forwarded := &filer_pb.ObjectTransactionRequest{
|
|
LockKey: req.LockKey,
|
|
Condition: req.Condition,
|
|
Mutations: req.Mutations,
|
|
IsFromOtherCluster: req.IsFromOtherCluster,
|
|
Signatures: req.Signatures,
|
|
ConditionKey: req.ConditionKey,
|
|
RouteKey: req.RouteKey,
|
|
IsMoved: true,
|
|
}
|
|
var resp *filer_pb.ObjectTransactionResponse
|
|
handled, err := fs.forwardToWriteOwner(ctx, req.RouteKey, func(owner pb.ServerAddress) error {
|
|
glog.V(2).InfofCtx(ctx, "ObjectTransaction %s: forwarding to owner %s", req.LockKey, owner)
|
|
return pb.WithFilerClient(false, 0, owner, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
var e error
|
|
resp, e = client.ObjectTransaction(ctx, forwarded)
|
|
return e
|
|
})
|
|
})
|
|
if handled {
|
|
if err != nil {
|
|
return &filer_pb.ObjectTransactionResponse{}, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
lockPath := util.FullPath(req.LockKey)
|
|
pathLock := fs.entryLockTable.AcquireLock("ObjectTransaction", lockPath, util.ExclusiveLock)
|
|
defer fs.entryLockTable.ReleaseLock(lockPath, pathLock)
|
|
|
|
if conditionIsSet(req.Condition) {
|
|
// The condition is evaluated against condition_key when set (e.g. a
|
|
// version entry whose WORM guards gate the delete), while the lock stays
|
|
// on lock_key (the object, serializing the pointer recompute).
|
|
conditionPath := lockPath
|
|
if req.ConditionKey != "" {
|
|
conditionPath = util.FullPath(req.ConditionKey)
|
|
}
|
|
current, findErr := fs.filer.FindEntry(ctx, conditionPath)
|
|
if findErr != nil && findErr != filer_pb.ErrNotFound {
|
|
return &filer_pb.ObjectTransactionResponse{}, fmt.Errorf("ObjectTransaction condition %s: %w", conditionPath, findErr)
|
|
}
|
|
if findErr == filer_pb.ErrNotFound {
|
|
current = nil
|
|
}
|
|
if !writeConditionSatisfied(req.Condition, current) {
|
|
glog.V(3).InfofCtx(ctx, "ObjectTransaction %s: precondition failed", conditionPath)
|
|
return &filer_pb.ObjectTransactionResponse{
|
|
Error: "precondition failed",
|
|
ErrorCode: filer_pb.FilerError_PRECONDITION_FAILED,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
for i, m := range req.Mutations {
|
|
if err := fs.applyObjectMutation(ctx, m, req.IsFromOtherCluster, req.Signatures); err != nil {
|
|
glog.V(2).InfofCtx(ctx, "ObjectTransaction %s mutation %d (%v): %v", lockPath, i, m.Type, err)
|
|
return &filer_pb.ObjectTransactionResponse{Error: fmt.Sprintf("mutation %d: %v", i, err)}, nil
|
|
}
|
|
}
|
|
|
|
return &filer_pb.ObjectTransactionResponse{}, nil
|
|
}
|
|
|
|
// ObjectTransactionBatch applies several object transactions in one round trip,
|
|
// each under its own per-path lock and independent of the others. A failed
|
|
// transaction (precondition or mutation error) is reported in its own response
|
|
// without aborting the rest, matching S3 multi-object semantics where each key
|
|
// succeeds or fails on its own.
|
|
func (fs *FilerServer) ObjectTransactionBatch(ctx context.Context, req *filer_pb.ObjectTransactionBatchRequest) (*filer_pb.ObjectTransactionBatchResponse, error) {
|
|
if req == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "request is required")
|
|
}
|
|
resp := &filer_pb.ObjectTransactionBatchResponse{
|
|
Responses: make([]*filer_pb.ObjectTransactionResponse, 0, len(req.Transactions)),
|
|
}
|
|
for _, txn := range req.Transactions {
|
|
// Stop early if the caller went away; the request still holds the
|
|
// unprocessed transactions, so it is retried rather than lost.
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if txn == nil {
|
|
resp.Responses = append(resp.Responses, &filer_pb.ObjectTransactionResponse{Error: "nil transaction"})
|
|
continue
|
|
}
|
|
one, err := fs.ObjectTransaction(ctx, txn)
|
|
if err != nil {
|
|
// A transport-level error on one transaction is surfaced as that
|
|
// transaction's error; the batch RPC itself still succeeds.
|
|
one = &filer_pb.ObjectTransactionResponse{Error: err.Error()}
|
|
}
|
|
resp.Responses = append(resp.Responses, one)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// applyStorageDefaultsToEntry enforces the path's storage rule (read-only
|
|
// prefixes reject the write) and fills in the rule TTL when the entry carries
|
|
// none. The returned option carries the same TTL as the entry, so chunks a
|
|
// caller still has to place expire with it.
|
|
func (fs *FilerServer) applyStorageDefaultsToEntry(ctx context.Context, entry *filer.Entry) (*operation.StorageOption, error) {
|
|
if entry.Remote != nil {
|
|
entry.TtlSec = 0
|
|
}
|
|
so, err := fs.detectStorageOption(ctx, string(entry.FullPath), "", "", entry.TtlSec, "", "", "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entry.ApplyStorageTtl(so.TtlSeconds)
|
|
return so, nil
|
|
}
|
|
|
|
// applyObjectMutation applies a single mutation while the transaction's path
|
|
// lock is held. PUT entries are expected to be fully prepared by the caller
|
|
// (chunks resolved); mutations here are metadata-scoped. A DELETE of an absent
|
|
// entry and a PATCH of an absent entry are no-ops, so transactions are
|
|
// idempotent on replay.
|
|
func (fs *FilerServer) applyObjectMutation(ctx context.Context, m *filer_pb.ObjectMutation, fromOtherCluster bool, signatures []int32) error {
|
|
switch m.Type {
|
|
case filer_pb.ObjectMutation_PUT:
|
|
if m.Entry == nil {
|
|
return fmt.Errorf("PUT requires an entry")
|
|
}
|
|
newEntry := filer.FromPbEntry(m.Directory, m.Entry)
|
|
so, err := fs.applyStorageDefaultsToEntry(ctx, newEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fs.filer.CreateEntry(ctx, newEntry, nil, false, fromOtherCluster, signatures, false, so.MaxFileNameLength)
|
|
|
|
case filer_pb.ObjectMutation_DELETE:
|
|
fullpath := util.NewFullPath(m.Directory, m.Name)
|
|
err := fs.filer.DeleteEntryMetaAndData(ctx, fullpath, m.IsRecursive, false, m.IsDeleteData, fromOtherCluster, signatures, 0)
|
|
if err != nil && err != filer_pb.ErrNotFound {
|
|
return err
|
|
}
|
|
if m.RemoveEmptyParent {
|
|
// A parent that exists only to hold this child (e.g. a .versions/
|
|
// directory losing its last version) is torn down in the same locked
|
|
// transaction. Best-effort: a non-empty or already-removed parent is
|
|
// the expected no-op, and a failed teardown must not fail the
|
|
// already-applied delete.
|
|
parentErr := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(m.Directory), false, false, false, fromOtherCluster, signatures, 0)
|
|
if parentErr != nil && parentErr != filer_pb.ErrNotFound && !errors.Is(parentErr, filer.ErrNonEmptyFolder) {
|
|
glog.V(1).InfofCtx(ctx, "remove empty parent %s: %v", m.Directory, parentErr)
|
|
}
|
|
}
|
|
return nil
|
|
|
|
case filer_pb.ObjectMutation_PATCH_EXTENDED:
|
|
fullpath := util.NewFullPath(m.Directory, m.Name)
|
|
oldEntry, err := fs.filer.FindEntry(ctx, fullpath)
|
|
if err == filer_pb.ErrNotFound {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Patch a copy so oldEntry still reflects the pre-update state for the
|
|
// metadata notification's diff.
|
|
newEntry := oldEntry.ShallowClone()
|
|
newEntry.Extended = make(map[string][]byte, len(oldEntry.Extended))
|
|
for k, v := range oldEntry.Extended {
|
|
newEntry.Extended[k] = v
|
|
}
|
|
for k, v := range m.SetExtended {
|
|
newEntry.Extended[k] = v
|
|
}
|
|
for _, k := range m.DeleteExtended {
|
|
delete(newEntry.Extended, k)
|
|
}
|
|
if m.SetContent {
|
|
newEntry.Content = m.Content
|
|
// Keep FileSize consistent with content for files; some stores and
|
|
// tools read the attribute directly. Directories carry no file size.
|
|
if !newEntry.IsDirectory() {
|
|
newEntry.FileSize = uint64(len(m.Content))
|
|
}
|
|
}
|
|
if m.TouchMtime {
|
|
newEntry.Attr.Mtime = time.Now()
|
|
}
|
|
if err := fs.filer.UpdateEntry(ctx, oldEntry, newEntry); err != nil {
|
|
return err
|
|
}
|
|
// Emit the metadata event so the update replicates and subscribers see it,
|
|
// matching the UpdateEntry handler.
|
|
fs.filer.NotifyUpdateEvent(ctx, oldEntry, newEntry, true, fromOtherCluster, signatures)
|
|
return nil
|
|
|
|
case filer_pb.ObjectMutation_RECOMPUTE_LATEST:
|
|
return fs.applyRecomputeLatest(ctx, m, fromOtherCluster, signatures)
|
|
|
|
default:
|
|
return fmt.Errorf("unknown mutation type %v", m.Type)
|
|
}
|
|
}
|
|
|
|
// applyRecomputeLatest re-derives the pointer entry (m.Directory/m.Name) from the
|
|
// current contents of recompute.scan_dir, under the transaction's lock. It is
|
|
// mechanical: pick the child that sorts last (descending) or first by name, copy
|
|
// the mapped extended keys from it into the pointer, and store its name under
|
|
// name_to_key. When the scanned directory is empty the pointer keys are cleared.
|
|
// The caller, which knows the versioning scheme, supplies the direction and the
|
|
// key mappings. A missing pointer entry is a no-op (idempotent on replay).
|
|
func (fs *FilerServer) applyRecomputeLatest(ctx context.Context, m *filer_pb.ObjectMutation, fromOtherCluster bool, signatures []int32) error {
|
|
rc := m.Recompute
|
|
if rc == nil {
|
|
return fmt.Errorf("RECOMPUTE_LATEST requires recompute parameters")
|
|
}
|
|
|
|
pointer, err := fs.filer.FindEntry(ctx, util.NewFullPath(m.Directory, m.Name))
|
|
if err == filer_pb.ErrNotFound {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Capture the pre-update image so the metadata notification carries a correct
|
|
// diff; pointer.Extended is mutated in place below.
|
|
oldPointer := pointer.ShallowClone()
|
|
oldPointer.Extended = make(map[string][]byte, len(pointer.Extended))
|
|
for k, v := range pointer.Extended {
|
|
oldPointer.Extended[k] = v
|
|
}
|
|
|
|
if pointer.Extended == nil {
|
|
pointer.Extended = make(map[string][]byte)
|
|
}
|
|
|
|
// Remember the prior chosen child so it can be demoted once the pointer moves.
|
|
var priorName string
|
|
if rc.NameToKey != "" {
|
|
priorName = string(pointer.Extended[rc.NameToKey])
|
|
}
|
|
|
|
// The store streams entries ascending by name. For the lowest-name pick we
|
|
// only need the first entry, so cap the listing at one; for the highest-name
|
|
// pick we must scan all and keep the last (the store has no reverse order).
|
|
// With exclude_name set the first child may be the excluded one, so the cap
|
|
// is lifted to find the first non-excluded entry.
|
|
limit := int64(math.MaxInt32)
|
|
if !rc.Descending && rc.ExcludeName == "" {
|
|
limit = 1
|
|
}
|
|
var chosen *filer.Entry
|
|
_, listErr := fs.filer.StreamListDirectoryEntries(ctx, util.FullPath(rc.ScanDir), "", false, limit, "", "", "", func(entry *filer.Entry) (bool, error) {
|
|
if rc.ExcludeName != "" && entry.Name() == rc.ExcludeName {
|
|
return true, nil
|
|
}
|
|
chosen = entry
|
|
return rc.Descending, nil
|
|
})
|
|
if listErr != nil {
|
|
return listErr
|
|
}
|
|
|
|
cleared := []string{rc.NameToKey, rc.SizeToKey, rc.MtimeToKey}
|
|
if chosen == nil {
|
|
for pointerKey := range rc.CopyExtended {
|
|
delete(pointer.Extended, pointerKey)
|
|
}
|
|
for _, k := range cleared {
|
|
if k != "" {
|
|
delete(pointer.Extended, k)
|
|
}
|
|
}
|
|
} else {
|
|
for pointerKey, sourceKey := range rc.CopyExtended {
|
|
if v, ok := chosen.Extended[sourceKey]; ok {
|
|
pointer.Extended[pointerKey] = v
|
|
} else {
|
|
delete(pointer.Extended, pointerKey)
|
|
}
|
|
}
|
|
if rc.NameToKey != "" {
|
|
pointer.Extended[rc.NameToKey] = []byte(chosen.Name())
|
|
}
|
|
if rc.SizeToKey != "" {
|
|
pointer.Extended[rc.SizeToKey] = []byte(strconv.FormatUint(chosen.FileSize, 10))
|
|
}
|
|
if rc.MtimeToKey != "" {
|
|
pointer.Extended[rc.MtimeToKey] = []byte(strconv.FormatInt(chosen.Mtime.Unix(), 10))
|
|
}
|
|
}
|
|
|
|
if err := fs.filer.UpdateEntry(ctx, oldPointer, pointer); err != nil {
|
|
return err
|
|
}
|
|
// Replicate the recomputed pointer to peer filers and subscribers. Without
|
|
// this the latest-version pointer stays in this filer's store only, so other
|
|
// filers never learn the current version and ListObjects undercounts.
|
|
fs.filer.NotifyUpdateEvent(ctx, oldPointer, pointer, false, fromOtherCluster, signatures)
|
|
|
|
// Stamp the displaced prior child (e.g. NoncurrentSinceNs for lifecycle).
|
|
newName := ""
|
|
if chosen != nil {
|
|
newName = chosen.Name()
|
|
}
|
|
if rc.DemoteKey != "" && priorName != "" && priorName != newName {
|
|
priorEntry, perr := fs.filer.FindEntry(ctx, util.NewFullPath(rc.ScanDir, priorName))
|
|
if perr == filer_pb.ErrNotFound {
|
|
return nil
|
|
}
|
|
if perr != nil {
|
|
return perr
|
|
}
|
|
oldPrior := priorEntry.ShallowClone()
|
|
oldPrior.Extended = make(map[string][]byte, len(priorEntry.Extended))
|
|
for k, v := range priorEntry.Extended {
|
|
oldPrior.Extended[k] = v
|
|
}
|
|
if priorEntry.Extended == nil {
|
|
priorEntry.Extended = make(map[string][]byte)
|
|
}
|
|
priorEntry.Extended[rc.DemoteKey] = rc.DemoteValue
|
|
if err := fs.filer.UpdateEntry(ctx, oldPrior, priorEntry); err != nil {
|
|
return err
|
|
}
|
|
fs.filer.NotifyUpdateEvent(ctx, oldPrior, priorEntry, false, fromOtherCluster, signatures)
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (fs *FilerServer) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "UpdateEntry %v", req)
|
|
if len(req.Entry.HardLinkId) > 0 {
|
|
glog.V(4).InfofCtx(ctx, "UpdateEntry %s/%s with HardLinkId %x counter=%d", req.Directory, req.Entry.Name, req.Entry.HardLinkId, req.Entry.HardLinkCounter)
|
|
}
|
|
|
|
fullpath := util.Join(req.Directory, req.Entry.Name)
|
|
|
|
// A conditional or preconditioned update is a read-then-write that the
|
|
// per-path lock below only makes atomic on this filer. Route it to the
|
|
// entry's ring owner so one filer's lock arbitrates every writer
|
|
// cluster-wide; is_moved bounds this to one hop.
|
|
if !req.IsMoved && (conditionIsSet(req.Condition) || len(req.ExpectedExtended) > 0) {
|
|
var ownerResp *filer_pb.UpdateEntryResponse
|
|
handled, forwardErr := fs.forwardToWriteOwner(ctx, entryRouteKey(util.FullPath(fullpath)), func(owner pb.ServerAddress) error {
|
|
glog.V(2).InfofCtx(ctx, "UpdateEntry %s: forwarding to owner %s", fullpath, owner)
|
|
req.IsMoved = true
|
|
return pb.WithFilerClient(false, 0, owner, fs.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
forwarded, e := client.UpdateEntry(ctx, req)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
ownerResp = forwarded
|
|
return nil
|
|
})
|
|
})
|
|
if handled {
|
|
if forwardErr != nil {
|
|
return &filer_pb.UpdateEntryResponse{}, forwardErr
|
|
}
|
|
return ownerResp, nil
|
|
}
|
|
}
|
|
|
|
// Serialize concurrent mutations to the same path on this filer so the
|
|
// read (preconditions, garbage diff) and the write are atomic. Callers
|
|
// route a key's writes to this owner filer, making this local lock
|
|
// sufficient.
|
|
lockPath := util.FullPath(fullpath)
|
|
pathLock := fs.entryLockTable.AcquireLock("UpdateEntry", lockPath, util.ExclusiveLock)
|
|
defer fs.entryLockTable.ReleaseLock(lockPath, pathLock)
|
|
|
|
// Fence stamped under the lock: the no-change path below returns no event,
|
|
// but the acknowledged state still reflects everything at or below this.
|
|
logTsNs := time.Now().UnixNano()
|
|
|
|
entry, err := fs.filer.FindEntry(ctx, lockPath)
|
|
if err != nil {
|
|
return &filer_pb.UpdateEntryResponse{}, fmt.Errorf("not found %s: %v", fullpath, err)
|
|
}
|
|
if err := validateUpdateEntryPreconditions(entry, req.ExpectedExtended); err != nil {
|
|
return &filer_pb.UpdateEntryResponse{}, err
|
|
}
|
|
if conditionIsSet(req.Condition) && !writeConditionSatisfied(req.Condition, entry) {
|
|
glog.V(3).InfofCtx(ctx, "UpdateEntry %s: precondition failed: %v", fullpath, req.Condition)
|
|
return &filer_pb.UpdateEntryResponse{}, status.Errorf(codes.FailedPrecondition, "precondition failed: %s", fullpath)
|
|
}
|
|
|
|
chunks, garbage, err2 := fs.cleanupChunks(ctx, fullpath, entry, req.Entry)
|
|
if err2 != nil {
|
|
return &filer_pb.UpdateEntryResponse{}, fmt.Errorf("UpdateEntry cleanupChunks %s: %v", fullpath, err2)
|
|
}
|
|
|
|
newEntry := filer.FromPbEntry(req.Directory, req.Entry)
|
|
newEntry.Chunks = chunks
|
|
|
|
// Don't apply TTL to remote entries - they're managed by remote storage
|
|
if newEntry.Remote != nil {
|
|
newEntry.TtlSec = 0
|
|
}
|
|
|
|
if filer.EqualEntry(entry, newEntry) {
|
|
return &filer_pb.UpdateEntryResponse{LogTsNs: logTsNs, LogSignature: fs.filer.Signature}, err
|
|
}
|
|
|
|
ctx, eventSink := filer.WithMetadataEventSink(ctx)
|
|
resp := &filer_pb.UpdateEntryResponse{LogTsNs: logTsNs, LogSignature: fs.filer.Signature}
|
|
if err = fs.filer.UpdateEntry(ctx, entry, newEntry); err == nil {
|
|
fs.filer.DeleteChunksNotRecursive(garbage)
|
|
|
|
fs.filer.NotifyUpdateEvent(ctx, entry, newEntry, true, req.IsFromOtherCluster, req.Signatures)
|
|
resp.MetadataEvent = eventSink.Last()
|
|
|
|
} else {
|
|
glog.V(3).InfofCtx(ctx, "UpdateEntry %s: %v", filepath.Join(req.Directory, req.Entry.Name), err)
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
func validateUpdateEntryPreconditions(entry *filer.Entry, expectedExtended map[string][]byte) error {
|
|
if len(expectedExtended) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for key, expectedValue := range expectedExtended {
|
|
var actualValue []byte
|
|
var ok bool
|
|
if entry != nil {
|
|
actualValue, ok = entry.Extended[key]
|
|
}
|
|
if ok {
|
|
if !bytes.Equal(actualValue, expectedValue) {
|
|
return status.Errorf(codes.FailedPrecondition, "extended attribute %q changed", key)
|
|
}
|
|
continue
|
|
}
|
|
if len(expectedValue) > 0 {
|
|
return status.Errorf(codes.FailedPrecondition, "extended attribute %q changed", key)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (fs *FilerServer) cleanupChunks(ctx context.Context, fullpath string, existingEntry *filer.Entry, newEntry *filer_pb.Entry) (chunks, garbage []*filer_pb.FileChunk, err error) {
|
|
|
|
// remove old chunks if not included in the new ones
|
|
if existingEntry != nil {
|
|
garbage, err = filer.MinusChunks(ctx, fs.lookupFileId, existingEntry.GetChunks(), newEntry.GetChunks(), fs.filer.MasterClient)
|
|
if err != nil {
|
|
return newEntry.GetChunks(), nil, fmt.Errorf("MinusChunks: %w", err)
|
|
}
|
|
}
|
|
|
|
// files with manifest chunks are usually large and append only, skip calculating covered chunks
|
|
manifestChunks, nonManifestChunks := filer.SeparateManifestChunks(newEntry.GetChunks())
|
|
|
|
chunks, coveredChunks := filer.CompactFileChunks(ctx, fs.lookupFileId, nonManifestChunks)
|
|
garbage = append(garbage, coveredChunks...)
|
|
|
|
if newEntry.Attributes != nil {
|
|
so, _ := fs.detectStorageOption(ctx, fullpath,
|
|
"",
|
|
"",
|
|
newEntry.Attributes.TtlSec,
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
) // ignore readonly error for capacity needed to manifestize
|
|
// A failed fold leaves a flat list, which is still a correct entry --
|
|
// and the store rejects it by name if it cannot hold one this size.
|
|
folded, manifestErr := filer.MaybeManifestize(fs.saveAsChunk(ctx, so), fs.filer.DeleteChunksNotRecursive, chunks)
|
|
if manifestErr != nil {
|
|
glog.V(0).InfofCtx(ctx, "MaybeManifestize %s: %v", fullpath, manifestErr)
|
|
}
|
|
chunks = folded
|
|
}
|
|
|
|
chunks = append(manifestChunks, chunks...)
|
|
|
|
return
|
|
}
|
|
|
|
func (fs *FilerServer) AppendToEntry(ctx context.Context, req *filer_pb.AppendToEntryRequest) (*filer_pb.AppendToEntryResponse, error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "AppendToEntry %v", req)
|
|
fullpath := util.NewFullPath(req.Directory, req.EntryName)
|
|
|
|
lockClient := cluster.NewLockClient(fs.grpcDialOption, fs.option.Host)
|
|
lock := lockClient.NewShortLivedLock(string(fullpath), string(fs.option.Host))
|
|
if lock == nil {
|
|
return nil, fmt.Errorf("failed to acquire lock for %s", fullpath)
|
|
}
|
|
defer lock.StopShortLivedLock()
|
|
|
|
// The cluster lock serializes appenders across filers; the path lock makes
|
|
// this read-modify-write atomic against conditional updates and deletes on
|
|
// the owner filer.
|
|
pathLock := fs.entryLockTable.AcquireLock("AppendToEntry", fullpath, util.ExclusiveLock)
|
|
defer fs.entryLockTable.ReleaseLock(fullpath, pathLock)
|
|
|
|
var offset int64 = 0
|
|
entry, err := fs.filer.FindEntry(ctx, fullpath)
|
|
if err == filer_pb.ErrNotFound {
|
|
entry = &filer.Entry{
|
|
FullPath: fullpath,
|
|
Attr: filer.Attr{
|
|
Crtime: time.Now(),
|
|
Mtime: time.Now(),
|
|
Mode: os.FileMode(0644),
|
|
Uid: OS_UID,
|
|
Gid: OS_GID,
|
|
},
|
|
}
|
|
} else {
|
|
offset = int64(filer.TotalSize(entry.GetChunks()))
|
|
}
|
|
|
|
for _, chunk := range req.Chunks {
|
|
chunk.Offset = offset
|
|
offset += int64(chunk.Size)
|
|
}
|
|
|
|
entry.Chunks = append(entry.GetChunks(), req.Chunks...)
|
|
so, err := fs.applyStorageDefaultsToEntry(ctx, entry)
|
|
if err != nil {
|
|
glog.WarningfCtx(ctx, "applyStorageDefaultsToEntry: %v", err)
|
|
return &filer_pb.AppendToEntryResponse{}, err
|
|
}
|
|
entry.Chunks, err = filer.MaybeManifestize(fs.saveAsChunk(ctx, so), fs.filer.DeleteChunksNotRecursive, entry.GetChunks())
|
|
if err != nil {
|
|
// the append is still correct with the flat list
|
|
glog.V(0).InfofCtx(ctx, "MaybeManifestize %s: %v", fullpath, err)
|
|
}
|
|
|
|
err = fs.filer.CreateEntry(context.Background(), entry, nil, false, false, nil, false, fs.filer.MaxFilenameLength)
|
|
|
|
return &filer_pb.AppendToEntryResponse{}, err
|
|
}
|
|
|
|
func (fs *FilerServer) DeleteEntry(ctx context.Context, req *filer_pb.DeleteEntryRequest) (resp *filer_pb.DeleteEntryResponse, err error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "DeleteEntry %v", req)
|
|
|
|
// A delete queues the entry's chunks for deletion, so it must not
|
|
// interleave with a conditional update's check-then-write on the same
|
|
// path: the update would pass its precondition and then resurrect fids
|
|
// that are already on the deletion queue.
|
|
fullpath := util.JoinPath(req.Directory, req.Name)
|
|
pathLock := fs.entryLockTable.AcquireLock("DeleteEntry", fullpath, util.ExclusiveLock)
|
|
defer fs.entryLockTable.ReleaseLock(fullpath, pathLock)
|
|
|
|
ctx, eventSink := filer.WithMetadataEventSink(ctx)
|
|
err = fs.filer.DeleteEntryMetaAndData(ctx, fullpath, req.IsRecursive, req.IgnoreRecursiveError, req.IsDeleteData, req.IsFromOtherCluster, req.Signatures, req.IfNotModifiedAfter)
|
|
resp = &filer_pb.DeleteEntryResponse{}
|
|
if err != nil && err != filer_pb.ErrNotFound {
|
|
resp.Error = err.Error()
|
|
} else {
|
|
resp.MetadataEvent = eventSink.Last()
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (fs *FilerServer) AssignVolume(ctx context.Context, req *filer_pb.AssignVolumeRequest) (resp *filer_pb.AssignVolumeResponse, err error) {
|
|
|
|
so, err := fs.resolveAssignStorageOption(ctx, req)
|
|
if err != nil {
|
|
glog.V(3).InfofCtx(ctx, "AssignVolume: %v", err)
|
|
return &filer_pb.AssignVolumeResponse{Error: fmt.Sprintf("assign volume: %v", err)}, nil
|
|
}
|
|
|
|
assignRequest, altRequest := so.ToAssignRequests(int(req.Count))
|
|
assignRequest.ExpectedDataSize = req.ExpectedDataSize
|
|
if altRequest != nil {
|
|
altRequest.ExpectedDataSize = req.ExpectedDataSize
|
|
}
|
|
|
|
assignResult, err := operation.Assign(ctx, fs.filer.GetMaster, fs.grpcDialOption, assignRequest, altRequest)
|
|
if err != nil {
|
|
glog.V(3).InfofCtx(ctx, "AssignVolume: %v", err)
|
|
return &filer_pb.AssignVolumeResponse{Error: fmt.Sprintf("assign volume: %v", err)}, nil
|
|
}
|
|
if assignResult.Error != "" {
|
|
glog.V(3).InfofCtx(ctx, "AssignVolume error: %v", assignResult.Error)
|
|
return &filer_pb.AssignVolumeResponse{Error: fmt.Sprintf("assign volume result: %v", assignResult.Error)}, nil
|
|
}
|
|
|
|
resp = &filer_pb.AssignVolumeResponse{
|
|
FileId: assignResult.Fid,
|
|
Count: int32(assignResult.Count),
|
|
Location: &filer_pb.Location{
|
|
Url: assignResult.Url,
|
|
PublicUrl: assignResult.PublicUrl,
|
|
GrpcPort: uint32(assignResult.GrpcPort),
|
|
},
|
|
Auth: string(assignResult.Auth),
|
|
Collection: so.Collection,
|
|
Replication: so.Replication,
|
|
Fsync: so.Fsync,
|
|
}
|
|
// Forward the replica holders so a client can write all copies directly.
|
|
for _, replica := range assignResult.Replicas {
|
|
resp.Replicas = append(resp.Replicas, &filer_pb.Location{
|
|
Url: replica.Url,
|
|
PublicUrl: replica.PublicUrl,
|
|
DataCenter: replica.DataCenter,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (fs *FilerServer) resolveAssignStorageOption(ctx context.Context, req *filer_pb.AssignVolumeRequest) (*operation.StorageOption, error) {
|
|
so, err := fs.detectStorageOption(ctx, req.Path, req.Collection, req.Replication, req.TtlSec, req.DiskType, req.DataCenter, req.Rack, req.DataNode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Mirror the HTTP write path: only apply the filer's default disk when the
|
|
// matched locationPrefix rule did not already select one.
|
|
if so.DiskType == "" {
|
|
so.DiskType = fs.option.DiskType
|
|
}
|
|
|
|
return so, nil
|
|
}
|
|
|
|
func (fs *FilerServer) CollectionList(ctx context.Context, req *filer_pb.CollectionListRequest) (resp *filer_pb.CollectionListResponse, err error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "CollectionList %v", req)
|
|
resp = &filer_pb.CollectionListResponse{}
|
|
|
|
err = fs.filer.MasterClient.WithClient(ctx, false, func(client master_pb.SeaweedClient) error {
|
|
masterResp, err := client.CollectionList(ctx, &master_pb.CollectionListRequest{
|
|
IncludeNormalVolumes: req.IncludeNormalVolumes,
|
|
IncludeEcVolumes: req.IncludeEcVolumes,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, c := range masterResp.Collections {
|
|
resp.Collections = append(resp.Collections, &filer_pb.Collection{Name: c.Name})
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func (fs *FilerServer) DeleteCollection(ctx context.Context, req *filer_pb.DeleteCollectionRequest) (resp *filer_pb.DeleteCollectionResponse, err error) {
|
|
|
|
glog.V(4).InfofCtx(ctx, "DeleteCollection %v", req)
|
|
|
|
err = fs.filer.DoDeleteCollection(ctx, req.GetCollection())
|
|
|
|
return &filer_pb.DeleteCollectionResponse{}, err
|
|
}
|