mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-01 18:00:46 +01:00
* fix(s3tables): keep the data under a deleted namespace A rename leaves the source table directory holding data the renamed table still points at, and DeleteNamespace removed it recursively. The delete now keeps the directory, minus its catalog record, when files remain under it; CreateNamespace can take it back. * fix(s3tables): keep hidden-directory data, report name takeover as 409 - DeleteNamespace also looks for files under hidden directories, since a table location may sit under one. - CreateNamespace answers 409 when a file takes the name during the take-back of a kept directory. * s3tables: a vanished leftover holds nothing to keep A bare directory removed between the namespace listing and the walk made dirHoldsFiles answer its ListEntries NotFound, and the delete reported that miss as a 404 or 500 on a live namespace. Skip the gone directory instead: whatever it held is already gone, so it cannot keep the namespace. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3tables: a delete's gone entry is a conflict, not a success The filer answers a precondition delete on an entry that is already gone with FailedPrecondition carrying the not-found text, which DoRemoveRequest reads as a completed delete -- DeleteNamespace answered 200 for a delete that never ran. deleteDirectoryIfMatches now calls DeleteEntry directly, keeping the FailedPrecondition-to-conflict mapping and refusing a response-level error. keepAsLeftover likewise reports a directory removed since the scan as ErrConcurrentUpdate, so the handler answers 409 rather than 500. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
129 lines
5.0 KiB
Go
129 lines
5.0 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// deleteEntryStub answers DeleteEntry the way the filer does: a rejected delete
|
|
// comes back as a nil transport error with the reason in the response.
|
|
type deleteEntryStub struct {
|
|
filer_pb.SeaweedFilerClient
|
|
resp *filer_pb.DeleteEntryResponse
|
|
err error
|
|
req *filer_pb.DeleteEntryRequest
|
|
}
|
|
|
|
func (s *deleteEntryStub) DeleteEntry(_ context.Context, req *filer_pb.DeleteEntryRequest, _ ...grpc.CallOption) (*filer_pb.DeleteEntryResponse, error) {
|
|
s.req = req
|
|
return s.resp, s.err
|
|
}
|
|
|
|
func TestDeleteDirectoryReportsRejectedDelete(t *testing.T) {
|
|
stub := &deleteEntryStub{resp: &filer_pb.DeleteEntryResponse{Error: "fail to delete non-empty folder"}}
|
|
|
|
err := (&S3TablesHandler{}).deleteDirectory(context.Background(), stub, "/buckets/b/ns/t")
|
|
|
|
require.Error(t, err, "a delete the filer rejected must not be reported as success")
|
|
assert.Contains(t, err.Error(), "fail to delete non-empty folder")
|
|
}
|
|
|
|
func TestDeleteDirectoryToleratesMissingEntry(t *testing.T) {
|
|
t.Run("reported in the response", func(t *testing.T) {
|
|
stub := &deleteEntryStub{resp: &filer_pb.DeleteEntryResponse{Error: filer_pb.ErrNotFound.Error()}}
|
|
assert.NoError(t, (&S3TablesHandler{}).deleteDirectory(context.Background(), stub, "/buckets/b/ns/t"))
|
|
})
|
|
t.Run("reported as a transport error", func(t *testing.T) {
|
|
stub := &deleteEntryStub{err: status.Error(codes.NotFound, filer_pb.ErrNotFound.Error())}
|
|
assert.NoError(t, (&S3TablesHandler{}).deleteDirectory(context.Background(), stub, "/buckets/b/ns/t"))
|
|
})
|
|
}
|
|
|
|
func TestDeleteDirectoryPropagatesTransportError(t *testing.T) {
|
|
stub := &deleteEntryStub{err: errors.New("filer unreachable")}
|
|
|
|
err := (&S3TablesHandler{}).deleteDirectory(context.Background(), stub, "/buckets/b/ns/t")
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "filer unreachable")
|
|
}
|
|
|
|
// deleteDirectory drops a whole subtree, so the delete has to stay recursive and
|
|
// take the data with it.
|
|
func TestDeleteDirectoryDeletesTheSubtree(t *testing.T) {
|
|
stub := &deleteEntryStub{resp: &filer_pb.DeleteEntryResponse{}}
|
|
|
|
require.NoError(t, (&S3TablesHandler{}).deleteDirectory(context.Background(), stub, "/buckets/b/ns/t"))
|
|
|
|
require.NotNil(t, stub.req)
|
|
assert.Equal(t, "/buckets/b/ns", stub.req.Directory)
|
|
assert.Equal(t, "t", stub.req.Name)
|
|
assert.True(t, stub.req.IsDeleteData)
|
|
assert.True(t, stub.req.IsRecursive)
|
|
assert.True(t, stub.req.IgnoreRecursiveError)
|
|
}
|
|
|
|
// The filer answers a precondition delete on an entry that is already gone
|
|
// with FailedPrecondition carrying the not-found text. DoRemoveRequest reads
|
|
// any error containing that text as a completed delete, so the conditional
|
|
// delete must keep the conflict itself: a name that changed under the delete
|
|
// is a concurrent update, not a success.
|
|
func TestDeleteDirectoryIfMatchesReportsAVanishedEntry(t *testing.T) {
|
|
stub := &deleteEntryStub{err: status.Error(codes.FailedPrecondition,
|
|
"extended attribute precondition /buckets/b/ns: "+filer_pb.ErrNotFound.Error())}
|
|
|
|
err := (&S3TablesHandler{}).deleteDirectoryIfMatches(context.Background(), stub, "/buckets/b/ns",
|
|
&filer_pb.Entry{Extended: map[string][]byte{ExtendedKeyMetadata: []byte("{}")}})
|
|
|
|
assert.ErrorIs(t, err, ErrConcurrentUpdate)
|
|
}
|
|
|
|
// A precondition delete the filer refuses inside the response is not a
|
|
// success either.
|
|
func TestDeleteDirectoryIfMatchesReportsResponseError(t *testing.T) {
|
|
stub := &deleteEntryStub{resp: &filer_pb.DeleteEntryResponse{Error: "fail to delete non-empty folder"}}
|
|
|
|
err := (&S3TablesHandler{}).deleteDirectoryIfMatches(context.Background(), stub, "/buckets/b/ns", &filer_pb.Entry{})
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "fail to delete non-empty folder")
|
|
}
|
|
|
|
// The bucket is the directory, so DeleteTableBucket must not answer success
|
|
// when the filer refused to remove it.
|
|
func TestDeleteTableBucketReportsRejectedDirectoryDelete(t *testing.T) {
|
|
fs := s3tablestest.Start(t)
|
|
bucketMeta, err := json.Marshal(tableBucketMetadata{Name: renameTestBucket, OwnerAccountID: DefaultAccountID})
|
|
require.NoError(t, err)
|
|
fs.Put(TablesPath, renameTestBucket, map[string][]byte{
|
|
ExtendedKeyTableBucket: []byte("{}"),
|
|
ExtendedKeyMetadata: bucketMeta,
|
|
})
|
|
|
|
bucketPath := GetTableBucketPath(renameTestBucket)
|
|
fs.RejectDelete = func(dir, name string) string {
|
|
if dir+"/"+name == bucketPath {
|
|
return "fail to delete non-empty folder"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
m := NewManager()
|
|
m.SetTrusted(true)
|
|
err = m.Execute(context.Background(), NewManagerClient(fs.Client), "DeleteTableBucket",
|
|
&DeleteTableBucketRequest{TableBucketARN: mustBucketARN(t)}, nil, "")
|
|
|
|
require.Error(t, err, "a bucket the filer refused to delete must not be reported as deleted")
|
|
assert.NotNil(t, fs.Get(TablesPath, renameTestBucket), "the bucket is still there")
|
|
}
|