mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-22 05:54:24 +02:00
* filer_pb: add MkdirExclusive A taken name leaves the callback as a result, not an error, so filer failover does not count a lost race as a filer failure. * s3tables: create the entry with its attributes in one exclusive create CreateTableBucket and CreateNamespace checked that the name was free and then wrote the entry in several calls, so a second create in the window replaced the first caller's entry including its owner. The create now carries the marker, metadata and tags in one O_EXCL CreateEntry that answers ErrEntryAlreadyExists with the same 409. * s3: PutBucket uses an exclusive mkdir The same window let a plain PutBucket replace a table bucket created concurrently. The already-exists branch now answers from the entry itself, since the registry just cached the name as absent, and the admin auto-create path is exclusive too. * s3: read the winning entry when a create loses the race The concurrent-create branch called existingBucketError on a name the registry had just cached as absent, answering NoSuchBucket, and the auto-create fallback could assign an S3 owner to a table bucket it met there. The negative cache is cleared before the existing-bucket error is built, and a table bucket keeps its owner-less shape. * s3: fail an auto-create that met a table bucket A table bucket carries no S3 owner and must not gain one, and the upload that auto-create ran for must not continue into it either. * s3: fail an auto-create when the winning entry cannot be read An unreadable existing entry fell through to success, so the write continued into an unvalidated entry, possibly a table bucket.
326 lines
11 KiB
Go
326 lines
11 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
var (
|
|
ErrAttributeNotFound = errors.New("attribute not found")
|
|
ErrConcurrentUpdate = errors.New("attribute changed concurrently")
|
|
)
|
|
|
|
// extendedAttributeUpdateAttempts bounds the read-modify-write retry loop in
|
|
// updateExtendedAttribute.
|
|
const extendedAttributeUpdateAttempts = 5
|
|
|
|
// Filer operations - Common functions for interacting with the filer
|
|
|
|
// createDirectory creates a new directory at the specified path
|
|
func (h *S3TablesHandler) createDirectory(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) error {
|
|
return h.createDirectoryEntry(ctx, client, path, false, nil)
|
|
}
|
|
|
|
// createDirectoryExclusive creates a directory entry together with its
|
|
// extended attributes in one CreateEntry that fails with
|
|
// filer_pb.ErrEntryAlreadyExists when an entry is already there: nothing is
|
|
// replaced and no reader sees the directory without its attributes.
|
|
func (h *S3TablesHandler) createDirectoryExclusive(ctx context.Context, client filer_pb.SeaweedFilerClient, path string, extended map[string][]byte) error {
|
|
return h.createDirectoryEntry(ctx, client, path, true, extended)
|
|
}
|
|
|
|
func (h *S3TablesHandler) createDirectoryEntry(ctx context.Context, client filer_pb.SeaweedFilerClient, path string, exclusive bool, extended map[string][]byte) error {
|
|
dir, name := splitPath(path)
|
|
now := time.Now().Unix()
|
|
return filer_pb.CreateEntry(ctx, client, &filer_pb.CreateEntryRequest{
|
|
Directory: dir,
|
|
OExcl: exclusive,
|
|
Entry: &filer_pb.Entry{
|
|
Name: name,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: now,
|
|
Crtime: now,
|
|
FileMode: uint32(0755 | os.ModeDir),
|
|
},
|
|
Extended: extended,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ensureDirectory ensures a directory exists at the specified path
|
|
func (h *S3TablesHandler) ensureDirectory(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) error {
|
|
dir, name := splitPath(path)
|
|
_, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: dir,
|
|
Name: name,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
return h.createDirectory(ctx, client, path)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// deleteEntryIfExists removes an entry if it exists, ignoring missing errors
|
|
func (h *S3TablesHandler) deleteEntryIfExists(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) error {
|
|
dir, name := splitPath(path)
|
|
return filer_pb.DoRemove(ctx, client, dir, name, true, false, true, false, nil)
|
|
}
|
|
|
|
// mutateEntryExtended applies mutate to an entry's extended attributes and
|
|
// writes the entry back, asserting that nothing on it changed in between.
|
|
// UpdateEntry rewrites the whole entry from the snapshot read here, so an
|
|
// unconditional write would delete an attribute a concurrent writer created.
|
|
func (h *S3TablesHandler) mutateEntryExtended(
|
|
ctx context.Context,
|
|
client filer_pb.SeaweedFilerClient,
|
|
path string,
|
|
mutate func(extended map[string][]byte) error,
|
|
) error {
|
|
dir, name := splitPath(path)
|
|
|
|
for attempt := 0; attempt < extendedAttributeUpdateAttempts; attempt++ {
|
|
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: dir,
|
|
Name: name,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
entry := resp.Entry
|
|
expected := snapshotExtended(entry.Extended)
|
|
if entry.Extended == nil {
|
|
entry.Extended = make(map[string][]byte)
|
|
}
|
|
if err := mutate(entry.Extended); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{
|
|
Directory: dir,
|
|
Entry: entry,
|
|
ExpectedExtended: expected,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if status.Code(err) != codes.FailedPrecondition {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("%w: %s", ErrConcurrentUpdate, path)
|
|
}
|
|
|
|
// setExtendedAttribute sets an extended attribute on an existing entry
|
|
func (h *S3TablesHandler) setExtendedAttribute(ctx context.Context, client filer_pb.SeaweedFilerClient, path, key string, data []byte) error {
|
|
return h.mutateEntryExtended(ctx, client, path, func(extended map[string][]byte) error {
|
|
extended[key] = data
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// setExtendedAttributes sets multiple extended attributes on an existing entry
|
|
// in a single UpdateEntry, so callers don't leave the entry partially tagged.
|
|
func (h *S3TablesHandler) setExtendedAttributes(ctx context.Context, client filer_pb.SeaweedFilerClient, path string, attrs map[string][]byte) error {
|
|
return h.mutateEntryExtended(ctx, client, path, func(extended map[string][]byte) error {
|
|
for key, data := range attrs {
|
|
extended[key] = data
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// removeExtendedAttributes deletes the given extended attributes from an entry,
|
|
// leaving the directory and its children intact.
|
|
func (h *S3TablesHandler) removeExtendedAttributes(ctx context.Context, client filer_pb.SeaweedFilerClient, path string, keys ...string) error {
|
|
return h.mutateEntryExtended(ctx, client, path, func(extended map[string][]byte) error {
|
|
for _, key := range keys {
|
|
delete(extended, key)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// removeExtendedAttributesIf drops the given attributes only while they still
|
|
// hold the values the caller captured. A rename copies attributes to the
|
|
// destination and then clears the source; without this, a write that landed in
|
|
// between is deleted here and never reaches the new name.
|
|
func (h *S3TablesHandler) removeExtendedAttributesIf(
|
|
ctx context.Context,
|
|
client filer_pb.SeaweedFilerClient,
|
|
path string,
|
|
expected map[string][]byte,
|
|
keys ...string,
|
|
) error {
|
|
return h.mutateEntryExtended(ctx, client, path, func(extended map[string][]byte) error {
|
|
for key, want := range expected {
|
|
if !bytes.Equal(extended[key], want) {
|
|
return fmt.Errorf("%w: %s changed on %s", ErrConcurrentUpdate, key, path)
|
|
}
|
|
}
|
|
for _, key := range keys {
|
|
delete(extended, key)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// updateExtendedAttribute applies mutate to the current value of key and writes
|
|
// the result back. mutate runs again on each retry, so it always sees the
|
|
// current value.
|
|
func (h *S3TablesHandler) updateExtendedAttribute(
|
|
ctx context.Context,
|
|
client filer_pb.SeaweedFilerClient,
|
|
path, key string,
|
|
mutate func(current []byte) ([]byte, error),
|
|
) error {
|
|
return h.mutateEntryExtended(ctx, client, path, func(extended map[string][]byte) error {
|
|
updated, err := mutate(extended[key])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extended[key] = updated
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// s3tablesExtendedKeys is every attribute this package stores on a bucket or
|
|
// table entry. A whole-entry write has to assert the absent ones too: a key
|
|
// missing from the precondition is a key a concurrent writer can create and
|
|
// this write will then delete.
|
|
var s3tablesExtendedKeys = []string{
|
|
ExtendedKeyTableBucket,
|
|
ExtendedKeyMetadata,
|
|
ExtendedKeyMetadataVersion,
|
|
ExtendedKeyPolicy,
|
|
ExtendedKeyTags,
|
|
ExtendedKeyMaintenance,
|
|
ExtendedKeyMaintenanceStatus,
|
|
ExtendedKeyEntryType,
|
|
}
|
|
|
|
// SnapshotExtended captures an entry's attributes as an UpdateEntry
|
|
// precondition, so a whole-entry write cannot silently revert or delete an
|
|
// attribute a concurrent writer touched. Attributes absent at read time are
|
|
// asserted absent, which fails the precondition if someone creates one first.
|
|
func SnapshotExtended(extended map[string][]byte, keys ...string) map[string][]byte {
|
|
return snapshotExtended(extended, keys...)
|
|
}
|
|
|
|
func snapshotExtended(extended map[string][]byte, keys ...string) map[string][]byte {
|
|
expected := make(map[string][]byte, len(extended)+len(s3tablesExtendedKeys))
|
|
for k, v := range extended {
|
|
expected[k] = v
|
|
}
|
|
for _, k := range s3tablesExtendedKeys {
|
|
if _, ok := expected[k]; !ok {
|
|
expected[k] = nil
|
|
}
|
|
}
|
|
for _, k := range keys {
|
|
if _, ok := expected[k]; !ok {
|
|
expected[k] = nil
|
|
}
|
|
}
|
|
return expected
|
|
}
|
|
|
|
// getExtendedAttribute gets an extended attribute from an entry
|
|
func (h *S3TablesHandler) getExtendedAttribute(ctx context.Context, client filer_pb.SeaweedFilerClient, path, key string) ([]byte, error) {
|
|
dir, name := splitPath(path)
|
|
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: dir,
|
|
Name: name,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.Entry.Extended == nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrAttributeNotFound, key)
|
|
}
|
|
|
|
data, ok := resp.Entry.Extended[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s", ErrAttributeNotFound, key)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// loadNamespaceMetadata resolves a namespace to its metadata. A directory that
|
|
// carries no namespace metadata is not a namespace, so a missing attribute
|
|
// reports the same absence as a missing entry and every caller tests one
|
|
// condition instead of forgetting the second.
|
|
func (h *S3TablesHandler) loadNamespaceMetadata(ctx context.Context, filerClient FilerClient, bucketName, namespaceName string) (*namespaceMetadata, error) {
|
|
var metadata namespaceMetadata
|
|
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
data, err := h.getExtendedAttribute(ctx, client, GetNamespacePath(bucketName, namespaceName), ExtendedKeyMetadata)
|
|
if err != nil {
|
|
if errors.Is(err, ErrAttributeNotFound) {
|
|
return filer_pb.ErrNotFound
|
|
}
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
return fmt.Errorf("failed to unmarshal namespace metadata: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &metadata, nil
|
|
}
|
|
|
|
// lookupEntry returns the filer entry at the given path.
|
|
func (h *S3TablesHandler) lookupEntry(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) (*filer_pb.Entry, error) {
|
|
dir, name := splitPath(path)
|
|
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: dir,
|
|
Name: name,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Entry, nil
|
|
}
|
|
|
|
// deleteExtendedAttribute deletes an extended attribute from an entry
|
|
func (h *S3TablesHandler) deleteExtendedAttribute(ctx context.Context, client filer_pb.SeaweedFilerClient, path, key string) error {
|
|
return h.mutateEntryExtended(ctx, client, path, func(extended map[string][]byte) error {
|
|
delete(extended, key)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// deleteDirectory deletes a directory and all its contents
|
|
func (h *S3TablesHandler) deleteDirectory(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) error {
|
|
dir, name := splitPath(path)
|
|
return filer_pb.DoRemove(ctx, client, dir, name, true, true, true, false, nil)
|
|
}
|
|
|
|
// entryExists checks if an entry exists at the given path
|
|
func (h *S3TablesHandler) entryExists(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) bool {
|
|
dir, name := splitPath(path)
|
|
_, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: dir,
|
|
Name: name,
|
|
})
|
|
return err == nil
|
|
}
|