mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 06:54:24 +00:00
When unlinking a hard-linked file, DeleteOneEntry and DeleteEntry both called DeleteHardLink before removing the directory entry from the store. If DeleteHardLink returned an error (e.g. KV storage issue, decode failure), the function returned early without deleting the directory entry itself. This left a stale entry in the filer store, causing subsequent rmdir to fail with ENOTEMPTY. Change both functions to log the hard link cleanup error and continue to delete the directory entry regardless. This ensures the parent directory can always be removed after all its children are unlinked. Remove tests/unlink/14.t from the pjdfstest known failures list since this fix addresses the root cause.
470 lines
15 KiB
Go
470 lines
15 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/viant/ptrie"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
var (
|
|
_ = VirtualFilerStore(&FilerStoreWrapper{})
|
|
_ = Debuggable(&FilerStoreWrapper{})
|
|
)
|
|
|
|
type VirtualFilerStore interface {
|
|
FilerStore
|
|
DeleteHardLink(ctx context.Context, hardLinkId HardLinkId) error
|
|
DeleteOneEntry(ctx context.Context, entry *Entry) error
|
|
InsertEntryKnownAbsent(ctx context.Context, entry *Entry) error
|
|
AddPathSpecificStore(path string, storeId string, store FilerStore)
|
|
OnBucketCreation(bucket string)
|
|
OnBucketDeletion(bucket string)
|
|
CanDropWholeBucket() bool
|
|
SameActualStore(a, b util.FullPath) bool
|
|
}
|
|
|
|
type FilerStoreWrapper struct {
|
|
defaultStore FilerStore
|
|
pathToStore ptrie.Trie[string]
|
|
storeIdToStore map[string]FilerStore
|
|
hasPathSpecificStore bool // fast check to skip MatchPrefix when no path-specific stores
|
|
}
|
|
|
|
func NewFilerStoreWrapper(store FilerStore) *FilerStoreWrapper {
|
|
if innerStore, ok := store.(*FilerStoreWrapper); ok {
|
|
return innerStore
|
|
}
|
|
return &FilerStoreWrapper{
|
|
defaultStore: store,
|
|
pathToStore: ptrie.New[string](),
|
|
storeIdToStore: make(map[string]FilerStore),
|
|
}
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) CanDropWholeBucket() bool {
|
|
if ba, ok := fsw.defaultStore.(BucketAware); ok {
|
|
return ba.CanDropWholeBucket()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) OnBucketCreation(bucket string) {
|
|
for _, store := range fsw.storeIdToStore {
|
|
if ba, ok := store.(BucketAware); ok {
|
|
ba.OnBucketCreation(bucket)
|
|
}
|
|
}
|
|
if ba, ok := fsw.defaultStore.(BucketAware); ok {
|
|
ba.OnBucketCreation(bucket)
|
|
}
|
|
}
|
|
func (fsw *FilerStoreWrapper) OnBucketDeletion(bucket string) {
|
|
for _, store := range fsw.storeIdToStore {
|
|
if ba, ok := store.(BucketAware); ok {
|
|
ba.OnBucketDeletion(bucket)
|
|
}
|
|
}
|
|
if ba, ok := fsw.defaultStore.(BucketAware); ok {
|
|
ba.OnBucketDeletion(bucket)
|
|
}
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) AddPathSpecificStore(path string, storeId string, store FilerStore) {
|
|
fsw.storeIdToStore[storeId] = NewFilerStorePathTranslator(path, store)
|
|
err := fsw.pathToStore.Put([]byte(path), storeId)
|
|
if err != nil {
|
|
glog.Fatalf("put path specific store: %v", err)
|
|
}
|
|
fsw.hasPathSpecificStore = true
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) getActualStore(path util.FullPath) (store FilerStore) {
|
|
store = fsw.defaultStore
|
|
// Fast path: skip MatchPrefix if no path-specific stores are configured (common case)
|
|
if !fsw.hasPathSpecificStore {
|
|
return
|
|
}
|
|
if path == "/" || path == "//" {
|
|
return
|
|
}
|
|
var storeId string
|
|
fsw.pathToStore.MatchPrefix([]byte(path), func(key []byte, value string) bool {
|
|
storeId = value
|
|
return false
|
|
})
|
|
if storeId != "" {
|
|
store = fsw.storeIdToStore[storeId]
|
|
}
|
|
return
|
|
}
|
|
|
|
// SameActualStore reports whether two paths resolve to the same underlying
|
|
// store. When path-specific stores are configured, different subtrees may
|
|
// be served by different backends.
|
|
func (fsw *FilerStoreWrapper) SameActualStore(a, b util.FullPath) bool {
|
|
return fsw.getActualStore(a) == fsw.getActualStore(b)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) getDefaultStore() (store FilerStore) {
|
|
return fsw.defaultStore
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) GetName() string {
|
|
return fsw.getDefaultStore().GetName()
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) Initialize(configuration util.Configuration, prefix string) error {
|
|
return fsw.getDefaultStore().Initialize(configuration, prefix)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) InsertEntry(ctx context.Context, entry *Entry) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(entry.FullPath)
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "insert").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "insert").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
|
normalizeEntryMimeForStore(entry)
|
|
|
|
if len(entry.HardLinkId) > 0 {
|
|
glog.V(4).InfofCtx(ctx, "InsertEntry %s has HardLinkId %x counter=%d",
|
|
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter)
|
|
}
|
|
|
|
if err := fsw.handleUpdateToHardLinks(ctx, entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
// glog.V(4).Infof("InsertEntry %s", entry.FullPath)
|
|
return actualStore.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
// InsertEntryKnownAbsent skips the pre-insert FindEntry path when the caller has
|
|
// already established that the target path does not exist.
|
|
func (fsw *FilerStoreWrapper) InsertEntryKnownAbsent(ctx context.Context, entry *Entry) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(entry.FullPath)
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "insert").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "insert").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
|
normalizeEntryMimeForStore(entry)
|
|
|
|
if len(entry.HardLinkId) > 0 {
|
|
glog.V(4).InfofCtx(ctx, "InsertEntryKnownAbsent %s has HardLinkId %x counter=%d",
|
|
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter)
|
|
if err := fsw.setHardLink(ctx, entry); err != nil {
|
|
return fmt.Errorf("setHardLink %x: %v", entry.HardLinkId, err)
|
|
}
|
|
}
|
|
|
|
return actualStore.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) UpdateEntry(ctx context.Context, entry *Entry) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(entry.FullPath)
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "update").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "update").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
filer_pb.BeforeEntrySerialization(entry.GetChunks())
|
|
normalizeEntryMimeForStore(entry)
|
|
|
|
if len(entry.HardLinkId) > 0 {
|
|
glog.V(4).InfofCtx(ctx, "UpdateEntry %s has HardLinkId %x counter=%d",
|
|
entry.FullPath, entry.HardLinkId, entry.HardLinkCounter)
|
|
}
|
|
|
|
if err := fsw.handleUpdateToHardLinks(ctx, entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
// glog.V(4).Infof("UpdateEntry %s", entry.FullPath)
|
|
return actualStore.UpdateEntry(ctx, entry)
|
|
}
|
|
|
|
func normalizeEntryMimeForStore(entry *Entry) {
|
|
if entry == nil {
|
|
return
|
|
}
|
|
if !entry.IsDirectory() && entry.Mime == "application/octet-stream" {
|
|
entry.Mime = ""
|
|
}
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) FindEntry(ctx context.Context, fp util.FullPath) (entry *Entry, err error) {
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(fp)
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "find").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "find").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
entry, err = actualStore.FindEntry(ctx, fp)
|
|
// glog.V(4).Infof("FindEntry %s: %v", fp, err)
|
|
if err != nil {
|
|
if fsw.CanDropWholeBucket() && strings.Contains(err.Error(), "Table") && strings.Contains(err.Error(), "doesn't exist") {
|
|
err = filer_pb.ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
fsw.maybeReadHardLink(ctx, entry)
|
|
|
|
filer_pb.AfterEntryDeserialization(entry.GetChunks())
|
|
return
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) DeleteEntry(ctx context.Context, fp util.FullPath) (err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(fp)
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "delete").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "delete").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
existingEntry, findErr := fsw.FindEntry(ctx, fp)
|
|
if findErr == filer_pb.ErrNotFound || existingEntry == nil {
|
|
return nil
|
|
}
|
|
if len(existingEntry.HardLinkId) != 0 {
|
|
// remove hard link
|
|
op := ctx.Value("OP")
|
|
if op != "MV" {
|
|
glog.V(4).InfofCtx(ctx, "DeleteHardLink %s", existingEntry.FullPath)
|
|
if hlErr := fsw.DeleteHardLink(ctx, existingEntry.HardLinkId); hlErr != nil {
|
|
// Log but continue: the directory entry must be removed
|
|
// even if hard link counter cleanup fails, otherwise the
|
|
// parent directory cannot be removed (rmdir ENOTEMPTY).
|
|
glog.Warningf("DeleteHardLink %s (id %x): %v", existingEntry.FullPath, existingEntry.HardLinkId, hlErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// glog.V(4).Infof("DeleteEntry %s", fp)
|
|
return actualStore.DeleteEntry(ctx, fp)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) DeleteOneEntry(ctx context.Context, existingEntry *Entry) (err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(existingEntry.FullPath)
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "delete").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "delete").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
if len(existingEntry.HardLinkId) != 0 {
|
|
// remove hard link
|
|
op := ctx.Value("OP")
|
|
if op != "MV" {
|
|
glog.V(4).InfofCtx(ctx, "DeleteHardLink %s", existingEntry.FullPath)
|
|
if hlErr := fsw.DeleteHardLink(ctx, existingEntry.HardLinkId); hlErr != nil {
|
|
// Log the hard link cleanup error but continue to delete
|
|
// the directory entry. If we return early here, the entry
|
|
// remains in the store and the parent directory cannot be
|
|
// removed (rmdir returns ENOTEMPTY).
|
|
glog.Warningf("DeleteHardLink %s (id %x): %v", existingEntry.FullPath, existingEntry.HardLinkId, hlErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// glog.V(4).Infof("DeleteOneEntry %s", existingEntry.FullPath)
|
|
return actualStore.DeleteEntry(ctx, existingEntry.FullPath)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) DeleteFolderChildren(ctx context.Context, fp util.FullPath) (err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(fp + "/")
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "deleteFolderChildren").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "deleteFolderChildren").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
// glog.V(4).Infof("DeleteFolderChildren %s", fp)
|
|
return actualStore.DeleteFolderChildren(ctx, fp)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) ListDirectoryEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc ListEachEntryFunc) (string, error) {
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(dirPath + "/")
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "list").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "list").Observe(time.Since(start).Seconds())
|
|
}()
|
|
|
|
// glog.V(4).Infof("ListDirectoryEntries %s from %s limit %d", dirPath, startFileName, limit)
|
|
return actualStore.ListDirectoryEntries(ctx, dirPath, startFileName, includeStartFile, limit, func(entry *Entry) (bool, error) {
|
|
fsw.maybeReadHardLink(ctx, entry)
|
|
filer_pb.AfterEntryDeserialization(entry.GetChunks())
|
|
return eachEntryFunc(entry)
|
|
})
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) ListDirectoryPrefixedEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc ListEachEntryFunc) (lastFileName string, err error) {
|
|
ctx = context.WithoutCancel(ctx)
|
|
actualStore := fsw.getActualStore(dirPath + "/")
|
|
stats.FilerStoreCounter.WithLabelValues(actualStore.GetName(), "prefixList").Inc()
|
|
start := time.Now()
|
|
defer func() {
|
|
stats.FilerStoreHistogram.WithLabelValues(actualStore.GetName(), "prefixList").Observe(time.Since(start).Seconds())
|
|
}()
|
|
if limit > math.MaxInt32-1 {
|
|
limit = math.MaxInt32 - 1
|
|
}
|
|
// glog.V(4).Infof("ListDirectoryPrefixedEntries %s from %s prefix %s limit %d", dirPath, startFileName, prefix, limit)
|
|
adjustedEntryFunc := func(entry *Entry) (bool, error) {
|
|
fsw.maybeReadHardLink(ctx, entry)
|
|
filer_pb.AfterEntryDeserialization(entry.GetChunks())
|
|
return eachEntryFunc(entry)
|
|
}
|
|
lastFileName, err = actualStore.ListDirectoryPrefixedEntries(ctx, dirPath, startFileName, includeStartFile, limit, prefix, adjustedEntryFunc)
|
|
if err == ErrUnsupportedListDirectoryPrefixed {
|
|
lastFileName, err = fsw.prefixFilterEntries(ctx, dirPath, startFileName, includeStartFile, limit, prefix, adjustedEntryFunc)
|
|
}
|
|
return lastFileName, err
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) prefixFilterEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc ListEachEntryFunc) (lastFileName string, err error) {
|
|
actualStore := fsw.getActualStore(dirPath + "/")
|
|
|
|
if prefix == "" {
|
|
return actualStore.ListDirectoryEntries(ctx, dirPath, startFileName, includeStartFile, limit, eachEntryFunc)
|
|
}
|
|
|
|
var notPrefixed []*Entry
|
|
lastFileName, err = actualStore.ListDirectoryEntries(ctx, dirPath, startFileName, includeStartFile, limit, func(entry *Entry) (bool, error) {
|
|
notPrefixed = append(notPrefixed, entry)
|
|
return true, nil
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
count := int64(0)
|
|
for count < limit && len(notPrefixed) > 0 {
|
|
for _, entry := range notPrefixed {
|
|
if strings.HasPrefix(entry.Name(), prefix) {
|
|
count++
|
|
res, resErr := eachEntryFunc(entry)
|
|
|
|
if resErr != nil {
|
|
err = fmt.Errorf("failed to process eachEntryFunc for entry %q: %w", entry.Name(), resErr)
|
|
return
|
|
}
|
|
|
|
if !res {
|
|
return
|
|
}
|
|
if count >= limit {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if count < limit && lastFileName < prefix {
|
|
notPrefixed = notPrefixed[:0]
|
|
lastFileName, err = actualStore.ListDirectoryEntries(ctx, dirPath, lastFileName, false, limit, func(entry *Entry) (bool, error) {
|
|
notPrefixed = append(notPrefixed, entry)
|
|
return true, nil
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) BeginTransaction(ctx context.Context) (context.Context, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
return fsw.getDefaultStore().BeginTransaction(ctx)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) CommitTransaction(ctx context.Context) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
return fsw.getDefaultStore().CommitTransaction(ctx)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) RollbackTransaction(ctx context.Context) error {
|
|
ctx = context.WithoutCancel(ctx)
|
|
return fsw.getDefaultStore().RollbackTransaction(ctx)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) Shutdown() {
|
|
fsw.getDefaultStore().Shutdown()
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) KvPut(ctx context.Context, key []byte, value []byte) (err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
return fsw.getDefaultStore().KvPut(ctx, key, value)
|
|
}
|
|
func (fsw *FilerStoreWrapper) KvGet(ctx context.Context, key []byte) (value []byte, err error) {
|
|
ctx = context.WithoutCancel(ctx)
|
|
return fsw.getDefaultStore().KvGet(ctx, key)
|
|
}
|
|
func (fsw *FilerStoreWrapper) KvDelete(ctx context.Context, key []byte) (err error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
ctx = context.WithoutCancel(ctx)
|
|
return fsw.getDefaultStore().KvDelete(ctx, key)
|
|
}
|
|
|
|
func (fsw *FilerStoreWrapper) Debug(writer io.Writer) {
|
|
if debuggable, ok := fsw.getDefaultStore().(Debuggable); ok {
|
|
debuggable.Debug(writer)
|
|
}
|
|
}
|