mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 08:50:48 +01:00
* iceberg: serve a registered table's metadata on load and commit RegisterTable saved only the metadata location, and loads and commits never read the file there. They served a placeholder with no schema, no snapshots and a new random table UUID on every load, so a registered table read as empty, an INSERT failed against the empty schema, and every other commit failed its assert-table-uuid requirement. Metadata uploaded through the S3 gateway, as a migrated table's is, is stored in chunks that registration did not read, so it registered an empty table. Read the file following its chunks, reject one that is not Iceberg metadata with a 400, and keep its content and table UUID in the record the way CreateTable does. Records registered before this change hold only the location. Loads and commits now read the file there, and the first commit stores it in the record under the file's table UUID. * iceberg: transactions read a location-only registration's metadata file * iceberg: cover transactions on a location-only registration * iceberg: refresh the default name mapping on transaction commits * iceberg: cover name-mapping refresh in transactions * iceberg: roll a transaction back to a registration's metadata file A transaction that failed after flipping a table registered before the record kept its metadata could not undo the flip. The rollback restored the record's prior metadata, which for such a table is only a location, and UpdateTable keeps the full metadata a flip stored when the restore sends none. Loads went on serving the change the client was told had failed, and a client retrying the transaction could apply it twice. Restore the metadata loads served from the file instead, under the file's table UUID. * iceberg: register oversized metadata by its location Registration sends the metadata file's content to the S3 Tables manager, which refuses a request over its 10MB body limit, so a table whose metadata is larger could no longer be registered at all. Register such a file by its location and table UUID alone; loads read it from that location, as they do for a record that never kept its metadata. Export the limit as s3tables.MaxRequestBodySize so the catalog can check against it. A commit to such a table still sends the whole metadata to UpdateTable and stays over the limit. * iceberg: commit oversized metadata by its location * iceberg: let maintenance follow a location-only table record * admin: read a table's metadata file when the record keeps none * iceberg: bound the metadata file a location-only record reads * iceberg: bound only chunked metadata reads in the worker Inline content is what the catalog itself wrote and the REST load path accepts it at any size, so rejecting it in loadMetadata stranded tables over 64 MiB from every maintenance scan. Cap only the chunked read, where the recorded location can name any object in the bucket. --------- Co-authored-by: chrislusf <chrislusf@users.noreply.github.com>
201 lines
6.9 KiB
Go
201 lines
6.9 KiB
Go
package iceberg
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
)
|
|
|
|
// metadataDirPath returns the filer directory holding a table's metadata files.
|
|
func metadataDirPath(bucketName, tablePath string) string {
|
|
dir := path.Join(s3tables.TablesPath, bucketName)
|
|
if tablePath != "" {
|
|
dir = path.Join(dir, tablePath)
|
|
}
|
|
return path.Join(dir, "metadata")
|
|
}
|
|
|
|
// saveMetadataFile saves the Iceberg metadata JSON file to the filer.
|
|
// It constructs the filer path from the S3 location components. With exclusive
|
|
// set the write fails with filer_pb.ErrEntryAlreadyExists rather than replacing
|
|
// a file that is already there: two commits racing off the same base pick the
|
|
// same v{N} name, and letting the second overwrite the first would leave the
|
|
// winner's catalog pointer aimed at the loser's metadata.
|
|
func (s *Server) saveMetadataFile(ctx context.Context, bucketName, tablePath, metadataFileName string, content []byte, exclusive bool) error {
|
|
return s.saveMetadataBlob(ctx, bucketName, tablePath, metadataFileName, content, "application/json", exclusive)
|
|
}
|
|
|
|
// saveMetadataBlob saves a file into the table's metadata directory.
|
|
func (s *Server) saveMetadataBlob(ctx context.Context, bucketName, tablePath, metadataFileName string, content []byte, mimeType string, exclusive bool) error {
|
|
|
|
// Create context with timeout for file operations
|
|
opCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
return s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
bucketsPath := s3tables.TablesPath
|
|
|
|
ensureDir := func(parent, name, errorContext string) error {
|
|
_, err := filer_pb.LookupEntry(opCtx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: parent,
|
|
Name: name,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if err != filer_pb.ErrNotFound {
|
|
return fmt.Errorf("lookup %s failed: %w", errorContext, err)
|
|
}
|
|
|
|
// If lookup fails with ErrNotFound, try to create the directory.
|
|
resp, createErr := client.CreateEntry(opCtx, &filer_pb.CreateEntryRequest{
|
|
Directory: parent,
|
|
Entry: &filer_pb.Entry{
|
|
Name: name,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: time.Now().Unix(),
|
|
Crtime: time.Now().Unix(),
|
|
FileMode: uint32(0755 | os.ModeDir),
|
|
},
|
|
},
|
|
})
|
|
if createErr != nil {
|
|
return fmt.Errorf("failed to create %s: %w", errorContext, createErr)
|
|
}
|
|
if resp.ErrorCode != filer_pb.FilerError_OK && resp.ErrorCode != filer_pb.FilerError_ENTRY_ALREADY_EXISTS {
|
|
return fmt.Errorf("failed to create %s: %s", errorContext, resp.Error)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
bucketDir := path.Join(bucketsPath, bucketName)
|
|
// 1. Ensure bucket directory exists: <bucketsPath>/<bucket>
|
|
if err := ensureDir(bucketsPath, bucketName, "bucket directory"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Ensure table path exists under the bucket directory
|
|
tableDir := bucketDir
|
|
if tablePath != "" {
|
|
segments := strings.Split(tablePath, "/")
|
|
for _, segment := range segments {
|
|
if segment == "" {
|
|
continue
|
|
}
|
|
if err := ensureDir(tableDir, segment, "table directory"); err != nil {
|
|
return err
|
|
}
|
|
tableDir = path.Join(tableDir, segment)
|
|
}
|
|
}
|
|
|
|
// 3. Ensure metadata directory exists: <bucketsPath>/<bucket>/<tablePath>/metadata
|
|
metadataDir := path.Join(tableDir, "metadata")
|
|
if err := ensureDir(tableDir, "metadata", "metadata directory"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 4. Write the file
|
|
resp, err := client.CreateEntry(opCtx, &filer_pb.CreateEntryRequest{
|
|
Directory: metadataDir,
|
|
OExcl: exclusive,
|
|
Entry: &filer_pb.Entry{
|
|
Name: metadataFileName,
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: time.Now().Unix(),
|
|
Crtime: time.Now().Unix(),
|
|
FileMode: uint32(0644),
|
|
FileSize: uint64(len(content)),
|
|
},
|
|
Content: content,
|
|
Extended: map[string][]byte{
|
|
"Mime-Type": []byte(mimeType),
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write metadata file: %w", err)
|
|
}
|
|
if resp.ErrorCode != filer_pb.FilerError_OK {
|
|
if sentinel := filer_pb.FilerErrorToSentinel(resp.ErrorCode); sentinel != nil {
|
|
return fmt.Errorf("failed to write metadata file: %w", sentinel)
|
|
}
|
|
return fmt.Errorf("failed to write metadata file: code=%v %s", resp.ErrorCode, resp.Error)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *Server) deleteMetadataFile(ctx context.Context, bucketName, tablePath, metadataFileName string) error {
|
|
opCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
metadataDir := metadataDirPath(bucketName, tablePath)
|
|
return s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
return filer_pb.DoRemove(opCtx, client, metadataDir, metadataFileName, true, false, true, false, nil)
|
|
})
|
|
}
|
|
|
|
func (s *Server) loadMetadataFile(ctx context.Context, bucketName, tablePath, metadataFileName string) ([]byte, error) {
|
|
opCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
metadataDir := metadataDirPath(bucketName, tablePath)
|
|
|
|
var content []byte
|
|
err := s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
resp, err := filer_pb.LookupEntry(opCtx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: metadataDir,
|
|
Name: metadataFileName,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp == nil || resp.Entry == nil {
|
|
return fmt.Errorf("lookup returned nil entry for %s/%s", metadataDir, metadataFileName)
|
|
}
|
|
content = append([]byte(nil), resp.Entry.Content...)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
// currentMetadataBytes returns the Iceberg metadata a table serves: the copy
|
|
// kept in its catalog record or, for a record that holds only a metadata
|
|
// location, the file at that location. Tables registered before the record
|
|
// kept their metadata look like that, and serving an empty placeholder for
|
|
// them hides every snapshot and fails every commit on a table UUID mismatch.
|
|
// It returns nil, for the caller's placeholder, when the record has neither
|
|
// or the file is gone.
|
|
func (s *Server) currentMetadataBytes(ctx context.Context, getResp s3tables.GetTableResponse, bucketName string) ([]byte, error) {
|
|
if getResp.Metadata != nil && len(getResp.Metadata.FullMetadata) > 0 {
|
|
return getResp.Metadata.FullMetadata, nil
|
|
}
|
|
if getResp.MetadataLocation == "" {
|
|
return nil, nil
|
|
}
|
|
metadataBucket, tablePath, err := parseS3Location(tableLocationFromMetadataLocation(getResp.MetadataLocation))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := confineMetadataLocation(metadataBucket, tablePath, bucketName); err != nil {
|
|
return nil, err
|
|
}
|
|
content, err := s.loadTableMetadataBlob(ctx, metadataBucket, tablePath, path.Base(getResp.MetadataLocation))
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
return nil, nil
|
|
}
|
|
return content, err
|
|
}
|