mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 00:09:00 +01:00
* filer_pb: map PRECONDITION_FAILED to a sentinel error A conditional CreateEntry that the owner's path lock refuses comes back as FilerError_PRECONDITION_FAILED; mapping it to ErrPreconditionFailed lets the S3 gateway tell a rejected precondition apart from a transport failure and report 412 instead of retrying the write. * s3api: make the ambiguous-PUT recovery create conditional under the lock createAfterAmbiguousRoute verified state and then issued an unconditional create, so a write landing between the lookup and the create was still clobbered by the stale re-commit (issue #11631). The recovery create now carries a WriteCondition — IF_NOT_EXISTS for a proven-absent entry, IF_ENTRY_EQUAL for a directory it may need to write onto — which the owner filer evaluates under its per-path lock, making the check and the write atomic. A refused precondition loops back to re-read the state instead of overwriting whatever won the race. * s3api: do not fail over a routed transaction past a possibly-sent request withFilerClientFailover retried the callback on the next filer on any transport error, so an ObjectTransaction whose response was lost could be re-applied over a write that committed in between (issue #11631). A gRPC error cannot distinguish "never sent" from "committed", but channel readiness can: a non-replayable callback now runs only on a connection that just reached READY, and failover continues only over channels that never came up — the one failure mode that provably sent nothing. * s3api: run ambiguous-route recovery for empty PUTs too The recovery was gated on a nonempty chunk list, so a zero-byte PUT whose routed transaction lost its response still fell back to an unconditional create that could overwrite a newer write and delete its chunks. Chunk-count mismatch on an empty upload list is enough to distinguish a superseding entry from this PUT's — there is nothing to save by skipping the check. * s3api: keep conditional recovery creates off the replay path The conditional create in the ambiguous-PUT fallback rode the replayable WithFilerClient failover, so a committed create whose response was lost could be re-sent to another filer — where a delete in between would let IF_NOT_EXISTS pass again and resurrect an entry pointing at reclaimed chunks. Conditional creates now run through the non-replayable failover; an ambiguous result falls through to confirmCreateLanded, which resolves the committed entry rather than resending. * s3api: do not mark a filer unhealthy for an answered request A reply that reached the callback — including an application-level failure like a refused write precondition — proves the filer is alive. Checking received before the health bookkeeping keeps definitive application errors from tripping the failure circuit or flagging a live routed owner as unreachable. * s3api: do not claim an empty stored entry as an ambiguous PUT's own Two chunkless entries compare equal under sameFileChunks, so an older empty object satisfies "the route landed" even though it predates this write — recovery would return success while leaving the request's Content-Type and metadata unapplied. A landed PUT stores the entry exactly as built, so require mime/size/md5/content and the extended attributes to match before claiming it; anything else stays uncertain and the client retries. * s3api: prove ambiguous empty PUTs by write id and pin recovery to the owner - Conditional recovery creates are sent to the routed owner alone instead of failing over: the condition only serializes with routed writes under the owner's per-path lock, and another filer's forwarding is only as correct as its ring view. An unreachable owner now yields an uncertain result rather than a create under a different serialization boundary. - A proven-absent path no longer satisfies an empty, chunkless PUT: the routed commit may have landed and been deleted, and there are no chunks to verify, so IF_NOT_EXISTS could resurrect a removed object. Recovery returns uncertain for the client to retry. - Chunkless identity moves from metadata equality to a per-write id (x-seaweedfs-write-id) stamped into Extended: an identical older object can match mime/md5/content but not the id. Entries without this write's id are not claimed, and confirmCreateLanded can now positively confirm a committed empty object instead of always reporting failure.
419 lines
13 KiB
Go
419 lines
13 KiB
Go
package filer_pb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/viant/ptrie"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
const cutoffTimeNewEmptyDir = 3
|
|
|
|
func (entry *Entry) IsInRemoteOnly() bool {
|
|
return len(entry.GetChunks()) == 0 && entry.RemoteEntry != nil && entry.RemoteEntry.RemoteSize > 0
|
|
}
|
|
|
|
func (entry *Entry) IsDirectoryKeyObject() bool {
|
|
// Also true for a file promoted to a directory by a child write, which keeps its
|
|
// chunks/content, or its remote entry when the file was tiered to remote storage.
|
|
return entry.IsDirectory &&
|
|
((entry.Attributes != nil && entry.Attributes.Mime != "") || len(entry.GetChunks()) > 0 || len(entry.GetContent()) > 0 || entry.IsInRemoteOnly() || entry.IsPrefixObject())
|
|
}
|
|
|
|
// IsPrefixObject reports whether the directory entry also holds the object named by its
|
|
// path without a trailing slash, which an empty object leaves no other trace of.
|
|
func (entry *Entry) IsPrefixObject() bool {
|
|
if entry == nil || !entry.IsDirectory {
|
|
return false
|
|
}
|
|
_, marked := entry.Extended[s3_constants.SeaweedFSPrefixObject]
|
|
return marked
|
|
}
|
|
|
|
// MarkPrefixObject turns a file entry into the directory entry that stores it, so keys
|
|
// nested under its key can be created beside it.
|
|
func (entry *Entry) MarkPrefixObject() {
|
|
entry.IsDirectory = true
|
|
if entry.Attributes != nil {
|
|
entry.Attributes.FileMode |= uint32(os.ModeDir) | 0111
|
|
}
|
|
if entry.Extended == nil {
|
|
entry.Extended = make(map[string][]byte)
|
|
}
|
|
entry.Extended[s3_constants.SeaweedFSPrefixObject] = []byte("true")
|
|
}
|
|
|
|
func (entry *Entry) GetExpiryTime() (expiryTime int64) {
|
|
// For S3 objects with lifecycle expiration, use Mtime (modification time)
|
|
// For regular TTL entries, use Crtime (creation time) for backward compatibility
|
|
if entry.Extended != nil {
|
|
if _, hasS3Expiry := entry.Extended[s3_constants.SeaweedFSExpiresS3]; hasS3Expiry {
|
|
// S3 lifecycle expiration: base TTL on modification time
|
|
expiryTime = entry.Attributes.Mtime
|
|
if expiryTime == 0 {
|
|
expiryTime = entry.Attributes.Crtime
|
|
}
|
|
expiryTime += int64(entry.Attributes.TtlSec)
|
|
return expiryTime
|
|
}
|
|
}
|
|
|
|
// Regular TTL expiration: base on creation time only
|
|
expiryTime = entry.Attributes.Crtime + int64(entry.Attributes.TtlSec)
|
|
return expiryTime
|
|
}
|
|
|
|
func (entry *Entry) IsExpired() bool {
|
|
return entry != nil && entry.Attributes != nil && entry.Attributes.TtlSec > 0 &&
|
|
time.Now().Unix() >= entry.GetExpiryTime()
|
|
}
|
|
|
|
func (entry *Entry) FileMode() (fileMode os.FileMode) {
|
|
if entry != nil && entry.Attributes != nil {
|
|
fileMode = os.FileMode(entry.Attributes.FileMode)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (entry *Entry) IsOlderDir() bool {
|
|
return entry.IsDirectory && entry.Attributes != nil && entry.Attributes.Mime == "" && entry.Attributes.GetCrtime() <= time.Now().Unix()-cutoffTimeNewEmptyDir
|
|
}
|
|
|
|
func ToFileIdObject(fileIdStr string) (*FileId, error) {
|
|
t, err := needle.ParseFileIdFromString(fileIdStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &FileId{
|
|
VolumeId: uint32(t.VolumeId),
|
|
Cookie: uint32(t.Cookie),
|
|
FileKey: uint64(t.Key),
|
|
}, nil
|
|
|
|
}
|
|
|
|
func (fid *FileId) toFileIdString() string {
|
|
return needle.NewFileId(needle.VolumeId(fid.VolumeId), fid.FileKey, fid.Cookie).String()
|
|
}
|
|
|
|
func (c *FileChunk) GetFileIdString() string {
|
|
if c.FileId != "" {
|
|
return c.FileId
|
|
}
|
|
if c.Fid != nil {
|
|
c.FileId = c.Fid.toFileIdString()
|
|
return c.FileId
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func BeforeEntrySerialization(chunks []*FileChunk) {
|
|
|
|
for _, chunk := range chunks {
|
|
|
|
if chunk.FileId != "" {
|
|
if fid, err := ToFileIdObject(chunk.FileId); err == nil {
|
|
chunk.Fid = fid
|
|
chunk.FileId = ""
|
|
}
|
|
}
|
|
|
|
if chunk.SourceFileId != "" {
|
|
if fid, err := ToFileIdObject(chunk.SourceFileId); err == nil {
|
|
chunk.SourceFid = fid
|
|
chunk.SourceFileId = ""
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func AfterEntryDeserialization(chunks []*FileChunk) {
|
|
|
|
for _, chunk := range chunks {
|
|
|
|
if chunk.Fid != nil && chunk.FileId == "" {
|
|
chunk.FileId = chunk.Fid.toFileIdString()
|
|
}
|
|
|
|
if chunk.SourceFid != nil && chunk.SourceFileId == "" {
|
|
chunk.SourceFileId = chunk.SourceFid.toFileIdString()
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func CreateEntry(ctx context.Context, client SeaweedFilerClient, request *CreateEntryRequest) error {
|
|
_, err := CreateEntryWithResponse(ctx, client, request)
|
|
return err
|
|
}
|
|
|
|
func CreateEntryWithResponse(ctx context.Context, client SeaweedFilerClient, request *CreateEntryRequest) (*CreateEntryResponse, error) {
|
|
resp, err := client.CreateEntry(ctx, request)
|
|
if err != nil {
|
|
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, err)
|
|
return nil, fmt.Errorf("CreateEntry: %w", err)
|
|
}
|
|
if resp.ErrorCode != FilerError_OK {
|
|
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v (code %v)", request.Directory, request.Entry.Name, request.OExcl, resp.Error, resp.ErrorCode)
|
|
if sentinel := FilerErrorToSentinel(resp.ErrorCode); sentinel != nil {
|
|
return nil, fmt.Errorf("CreateEntry %s/%s: %w", request.Directory, request.Entry.Name, sentinel)
|
|
}
|
|
return nil, fmt.Errorf("CreateEntry: %w", errors.New(resp.Error))
|
|
}
|
|
if resp.Error != "" {
|
|
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, resp.Error)
|
|
return nil, fmt.Errorf("CreateEntry: %w", errors.New(resp.Error))
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// SnapshotExtended clones extended attributes for use as an UpdateEntry
|
|
// ExpectedExtended precondition. Listed keys are asserted even when absent,
|
|
// so a concurrent add still fails the precondition.
|
|
func SnapshotExtended(extended map[string][]byte, keys ...string) map[string][]byte {
|
|
expected := make(map[string][]byte, len(extended)+len(keys))
|
|
for k, v := range extended {
|
|
expected[k] = v
|
|
}
|
|
for _, k := range keys {
|
|
if _, ok := expected[k]; !ok {
|
|
expected[k] = nil
|
|
}
|
|
}
|
|
return expected
|
|
}
|
|
|
|
func UpdateEntry(ctx context.Context, client SeaweedFilerClient, request *UpdateEntryRequest) error {
|
|
_, err := UpdateEntryWithResponse(ctx, client, request)
|
|
return err
|
|
}
|
|
|
|
func UpdateEntryWithResponse(ctx context.Context, client SeaweedFilerClient, request *UpdateEntryRequest) (*UpdateEntryResponse, error) {
|
|
resp, err := client.UpdateEntry(ctx, request)
|
|
if err != nil {
|
|
glog.V(1).InfofCtx(ctx, "update entry %s/%s :%v", request.Directory, request.Entry.Name, err)
|
|
return nil, fmt.Errorf("UpdateEntry: %w", err)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func LookupEntry(ctx context.Context, client SeaweedFilerClient, request *LookupDirectoryEntryRequest) (*LookupDirectoryEntryResponse, error) {
|
|
resp, err := client.LookupDirectoryEntry(ctx, request)
|
|
if err != nil {
|
|
if err == ErrNotFound || strings.Contains(err.Error(), ErrNotFound.Error()) {
|
|
return nil, ErrNotFound
|
|
}
|
|
glog.V(3).InfofCtx(ctx, "read %s/%v: %v", request.Directory, request.Name, err)
|
|
return nil, fmt.Errorf("LookupEntry1: %w", err)
|
|
}
|
|
if resp.Entry == nil {
|
|
return nil, ErrNotFound
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
var ErrNotFound = errors.New("filer: no entry is found in filer store")
|
|
|
|
// Sentinel errors for filer entry operations.
|
|
// These are set by the filer and reconstructed from FilerError codes after
|
|
// crossing the gRPC boundary, so consumers can use errors.Is() instead of
|
|
// parsing error strings.
|
|
var (
|
|
ErrEntryNameTooLong = errors.New("entry name too long")
|
|
ErrParentIsFile = errors.New("parent path is a file")
|
|
ErrExistingIsDirectory = errors.New("existing entry is a directory")
|
|
ErrExistingIsFile = errors.New("existing entry is a file")
|
|
ErrEntryAlreadyExists = errors.New("entry already exists")
|
|
ErrReadOnly = errors.New("read only")
|
|
ErrPreconditionFailed = errors.New("precondition failed")
|
|
)
|
|
|
|
// FilerErrorToSentinel maps a proto FilerError code to its sentinel error.
|
|
// Returns nil for OK or unknown codes.
|
|
func FilerErrorToSentinel(code FilerError) error {
|
|
switch code {
|
|
case FilerError_ENTRY_NAME_TOO_LONG:
|
|
return ErrEntryNameTooLong
|
|
case FilerError_PARENT_IS_FILE:
|
|
return ErrParentIsFile
|
|
case FilerError_EXISTING_IS_DIRECTORY:
|
|
return ErrExistingIsDirectory
|
|
case FilerError_EXISTING_IS_FILE:
|
|
return ErrExistingIsFile
|
|
case FilerError_ENTRY_ALREADY_EXISTS:
|
|
return ErrEntryAlreadyExists
|
|
case FilerError_READ_ONLY:
|
|
return ErrReadOnly
|
|
case FilerError_PRECONDITION_FAILED:
|
|
return ErrPreconditionFailed
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// AssignVolumeResponseError returns the failure resp reports, or nil. The error
|
|
// keeps the filer's text and, when error_code is set, unwraps to its sentinel so
|
|
// callers can match it with errors.Is.
|
|
func AssignVolumeResponseError(resp *AssignVolumeResponse) error {
|
|
if resp.Error == "" && resp.ErrorCode == FilerError_OK {
|
|
return nil
|
|
}
|
|
return &codedError{msg: resp.Error, sentinel: FilerErrorToSentinel(resp.ErrorCode)}
|
|
}
|
|
|
|
// codedError is a filer error message paired with the sentinel of the code
|
|
// sent alongside it (nil for OK or an unknown code).
|
|
type codedError struct {
|
|
msg string
|
|
sentinel error
|
|
}
|
|
|
|
func (e *codedError) Error() string { return e.msg }
|
|
func (e *codedError) Unwrap() error { return e.sentinel }
|
|
|
|
func IsEmpty(event *SubscribeMetadataResponse) bool {
|
|
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry == nil
|
|
}
|
|
|
|
func IsCreate(event *SubscribeMetadataResponse) bool {
|
|
return event.EventNotification.NewEntry != nil && event.EventNotification.OldEntry == nil
|
|
}
|
|
|
|
func IsUpdate(event *SubscribeMetadataResponse) bool {
|
|
return event.EventNotification.NewEntry != nil &&
|
|
event.EventNotification.OldEntry != nil &&
|
|
event.Directory == event.EventNotification.NewParentPath &&
|
|
event.EventNotification.NewEntry.Name == event.EventNotification.OldEntry.Name
|
|
}
|
|
|
|
func IsDelete(event *SubscribeMetadataResponse) bool {
|
|
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry != nil
|
|
}
|
|
|
|
func IsRename(event *SubscribeMetadataResponse) bool {
|
|
return event.EventNotification.NewEntry != nil &&
|
|
event.EventNotification.OldEntry != nil &&
|
|
(event.Directory != event.EventNotification.NewParentPath ||
|
|
event.EventNotification.NewEntry.Name != event.EventNotification.OldEntry.Name)
|
|
}
|
|
|
|
func MetadataEventSourceDirectory(event *SubscribeMetadataResponse) string {
|
|
if event == nil {
|
|
return ""
|
|
}
|
|
return event.Directory
|
|
}
|
|
|
|
func MetadataEventTargetDirectory(event *SubscribeMetadataResponse) string {
|
|
if event == nil {
|
|
return ""
|
|
}
|
|
if event.EventNotification != nil && event.EventNotification.NewParentPath != "" {
|
|
return event.EventNotification.NewParentPath
|
|
}
|
|
return event.Directory
|
|
}
|
|
|
|
func metadataEventSourceEntryName(event *SubscribeMetadataResponse) string {
|
|
if event == nil || event.EventNotification == nil {
|
|
return ""
|
|
}
|
|
if event.EventNotification.OldEntry != nil {
|
|
return event.EventNotification.OldEntry.Name
|
|
}
|
|
if event.EventNotification.NewEntry != nil {
|
|
return event.EventNotification.NewEntry.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func metadataEventTargetEntryName(event *SubscribeMetadataResponse) string {
|
|
if event == nil || event.EventNotification == nil {
|
|
return ""
|
|
}
|
|
if event.EventNotification.NewEntry != nil {
|
|
return event.EventNotification.NewEntry.Name
|
|
}
|
|
if event.EventNotification.OldEntry != nil {
|
|
return event.EventNotification.OldEntry.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func MetadataEventSourceFullPath(event *SubscribeMetadataResponse) string {
|
|
return util.Join(MetadataEventSourceDirectory(event), metadataEventSourceEntryName(event))
|
|
}
|
|
|
|
func MetadataEventTargetFullPath(event *SubscribeMetadataResponse) string {
|
|
return util.Join(MetadataEventTargetDirectory(event), metadataEventTargetEntryName(event))
|
|
}
|
|
|
|
func MetadataEventTouchesDirectory(event *SubscribeMetadataResponse, dir string) bool {
|
|
if MetadataEventSourceDirectory(event) == dir {
|
|
return true
|
|
}
|
|
return event != nil &&
|
|
event.EventNotification != nil &&
|
|
event.EventNotification.NewEntry != nil &&
|
|
MetadataEventTargetDirectory(event) == dir
|
|
}
|
|
|
|
func MetadataEventMatchesSubscription(event *SubscribeMetadataResponse, pathPrefix string, pathPrefixes []string, directories []string) bool {
|
|
if event == nil {
|
|
return false
|
|
}
|
|
|
|
if metadataEventMatchesPath(MetadataEventSourceFullPath(event), MetadataEventSourceDirectory(event), pathPrefix, pathPrefixes, directories) {
|
|
return true
|
|
}
|
|
|
|
return event.EventNotification != nil &&
|
|
event.EventNotification.NewEntry != nil &&
|
|
metadataEventMatchesPath(MetadataEventTargetFullPath(event), MetadataEventTargetDirectory(event), pathPrefix, pathPrefixes, directories)
|
|
}
|
|
|
|
func metadataEventMatchesPath(fullPath, dirPath, pathPrefix string, pathPrefixes []string, directories []string) bool {
|
|
if hasPrefixIn(fullPath, pathPrefixes) {
|
|
return true
|
|
}
|
|
if matchByDirectory(dirPath, directories) {
|
|
return true
|
|
}
|
|
return strings.HasPrefix(fullPath, pathPrefix)
|
|
}
|
|
|
|
func hasPrefixIn(text string, prefixes []string) bool {
|
|
for _, p := range prefixes {
|
|
if strings.HasPrefix(text, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchByDirectory(dirPath string, directories []string) bool {
|
|
for _, dir := range directories {
|
|
if dirPath == dir {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var _ = ptrie.KeyProvider(&FilerConf_PathConf{})
|
|
|
|
func (fp *FilerConf_PathConf) Key() interface{} {
|
|
key, _ := proto.Marshal(fp)
|
|
return string(key)
|
|
}
|