feat: drop table location mapping support (#8458)

* feat: drop table location mapping support

Disable external metadata locations for S3 Tables and remove the table location
mapping index entirely. Table metadata must live under the table bucket paths,
so lookups no longer use mapping directories.

Changes:
- Remove mapping lookup and cache from bucket path resolution
- Reject metadataLocation in CreateTable and UpdateTable
- Remove mapping helpers and tests

* compile

* refactor

* fix: accept metadataLocation in S3 Tables API requests

We removed the external table location mapping feature, but still need to
accept and store metadataLocation values from clients like Trino. The mapping
feature was an internal implementation detail that mapped external buckets to
internal table paths. The metadataLocation field itself is part of the S3 Tables
API and should be preserved.

* fmt

* fix: handle MetadataLocation in UpdateTable requests

Mirror handleCreateTable behavior by updating metadata.MetadataLocation
when req.MetadataLocation is provided in UpdateTable requests. This ensures
table metadata location can be updated, not just set during creation.
This commit is contained in:
Chris Lu
2026-02-26 16:36:24 -08:00
committed by GitHub
parent 641351da78
commit 8eba7ba5b2
8 changed files with 50 additions and 483 deletions

View File

@@ -1,9 +1,7 @@
package s3api
import (
"context"
"errors"
"io"
"path"
"strings"
@@ -43,136 +41,12 @@ func (s3a *S3ApiServer) isTableBucket(bucket string) bool {
return false
}
func (s3a *S3ApiServer) tableLocationDir(bucket string) (string, bool) {
if bucket == "" {
return "", false
}
// Check cache first
if s3a.bucketRegistry != nil {
s3a.bucketRegistry.tableLocationLock.RLock()
if tablePath, ok := s3a.bucketRegistry.tableLocationCache[bucket]; ok {
s3a.bucketRegistry.tableLocationLock.RUnlock()
return tablePath, tablePath != ""
}
s3a.bucketRegistry.tableLocationLock.RUnlock()
}
tablePath, err := s3a.lookupTableLocationMapping(bucket, s3tables.GetTableLocationMappingDir())
// Only cache definitive results: successful lookup (tablePath set) or definitive not-found (ErrNotFound)
// Don't cache transient errors to avoid treating temporary failures as permanent misses
if err == nil || errors.Is(err, filer_pb.ErrNotFound) {
if s3a.bucketRegistry != nil {
s3a.bucketRegistry.tableLocationLock.Lock()
s3a.bucketRegistry.tableLocationCache[bucket] = tablePath
s3a.bucketRegistry.tableLocationLock.Unlock()
}
}
if tablePath == "" {
if err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
glog.V(1).Infof("table location mapping lookup failed for %s: %v", bucket, err)
}
return "", false
}
return tablePath, true
}
func (s3a *S3ApiServer) readTableLocationMappingFromDirectory(mappingDir string) (string, error) {
var mappedPath string
conflict := false
err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
Directory: mappingDir,
Limit: 4294967295, // math.MaxUint32
})
if err != nil {
return err
}
for {
resp, recvErr := stream.Recv()
if recvErr == io.EOF {
return nil
}
if recvErr != nil {
return recvErr
}
if resp == nil || resp.Entry == nil || resp.Entry.IsDirectory || len(resp.Entry.Content) == 0 {
continue
}
candidate := normalizeTableLocationMappingPath(string(resp.Entry.Content))
if candidate == "" {
continue
}
if mappedPath == "" {
mappedPath = candidate
continue
}
if mappedPath != candidate {
conflict = true
return nil
}
}
})
if err != nil {
return "", err
}
if conflict {
glog.V(1).Infof("table location mapping conflict under %s: multiple mapped roots found", mappingDir)
return "", nil
}
return mappedPath, nil
}
func (s3a *S3ApiServer) lookupTableLocationMapping(bucket, mappingDir string) (string, error) {
entry, err := s3a.getEntry(mappingDir, bucket)
if err != nil || entry == nil {
return "", err
}
if entry.IsDirectory {
return s3a.readTableLocationMappingFromDirectory(path.Join(mappingDir, bucket))
}
if len(entry.Content) == 0 {
return "", nil
}
return normalizeTableLocationMappingPath(string(entry.Content)), nil
}
func normalizeTableLocationMappingPath(rawPath string) string {
rawPath = strings.TrimSpace(rawPath)
if rawPath == "" {
return ""
}
normalized := path.Clean("/" + strings.TrimPrefix(rawPath, "/"))
tablesPrefix := strings.TrimSuffix(s3tables.TablesPath, "/") + "/"
if !strings.HasPrefix(normalized, tablesPrefix) {
return normalized
}
remaining := strings.TrimPrefix(normalized, tablesPrefix)
bucketName, _, _ := strings.Cut(remaining, "/")
if bucketName == "" {
return ""
}
return path.Join(s3tables.TablesPath, bucketName)
}
func (s3a *S3ApiServer) bucketRoot(bucket string) string {
// Returns the unified buckets root path for all bucket types
return s3a.option.BucketsPath
}
func (s3a *S3ApiServer) bucketDir(bucket string) string {
if tablePath, ok := s3a.tableLocationDir(bucket); ok {
return tablePath
}
return path.Join(s3a.bucketRoot(bucket), bucket)
}
@@ -221,8 +95,5 @@ func (s3a *S3ApiServer) bucketExists(bucket string) (bool, error) {
}
func (s3a *S3ApiServer) getBucketEntry(bucket string) (*filer_pb.Entry, error) {
if tablePath, ok := s3a.tableLocationDir(bucket); ok {
return s3a.getEntry(path.Dir(tablePath), path.Base(tablePath))
}
return s3a.getEntry(s3a.option.BucketsPath, bucket)
}