mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 20:47:46 +01:00
The worker assumed every file of a table sits under its catalog path, so loadFileByIcebergPath stripped the scheme off a recorded location and joined the remainder onto /buckets/<bucket>/<ns>/<table>. A table the REST catalog placed elsewhere in the bucket — which is what a client gets whenever the catalog path is already occupied — then resolves to a doubled path: lookup /buckets/lake/source/t/lake/source/t-0cd81bca-.../metadata/snap-.avro so the very first manifest list read fails and the job fails again on every scan interval, indefinitely. Resolve absolute references (s3:// URIs and /buckets paths) from the bucket root and keep relative ones under the table's own directory; the bucket-relative form is now the canonical key everywhere references are compared. That directory comes from the metadata location the catalog stores, so reads, writes and deletes all land where the table's other files are instead of splitting it across two trees. References outside the table's bucket are rejected rather than silently misresolved. Rewritten position-delete files now name their data file by absolute URI, the way the table itself names it, instead of a path relative to the table.
281 lines
8.7 KiB
Go
281 lines
8.7 KiB
Go
package iceberg
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/apache/iceberg-go"
|
|
"github.com/apache/iceberg-go/table"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
type planningIndex struct {
|
|
SnapshotID int64 `json:"snapshotId"`
|
|
ManifestList string `json:"manifestList,omitempty"`
|
|
UpdatedAtMs int64 `json:"updatedAtMs"`
|
|
DataManifestCount int64 `json:"dataManifestCount,omitempty"`
|
|
Compaction *planningIndexCompaction `json:"compaction,omitempty"`
|
|
RewriteManifests *planningIndexRewriteManifests `json:"rewriteManifests,omitempty"`
|
|
}
|
|
|
|
type planningIndexCompaction struct {
|
|
ConfigHash string `json:"configHash"`
|
|
Eligible bool `json:"eligible"`
|
|
}
|
|
|
|
type planningIndexRewriteManifests struct {
|
|
Threshold int64 `json:"threshold"`
|
|
Eligible bool `json:"eligible"`
|
|
}
|
|
|
|
type tableMetadataEnvelope struct {
|
|
MetadataVersion int `json:"metadataVersion"`
|
|
MetadataLocation string `json:"metadataLocation,omitempty"`
|
|
Metadata *struct {
|
|
FullMetadata json.RawMessage `json:"fullMetadata,omitempty"`
|
|
} `json:"metadata,omitempty"`
|
|
PlanningIndex json.RawMessage `json:"planningIndex,omitempty"`
|
|
}
|
|
|
|
// tableState is the catalog's view of one table: its Iceberg metadata, the
|
|
// metadata file backing it, where its files live, and the cached planning index.
|
|
type tableState struct {
|
|
Metadata table.Metadata
|
|
MetadataFileName string
|
|
// DataPath is the bucket-relative directory holding metadata/ and data/.
|
|
DataPath string
|
|
PlanningIndex *planningIndex
|
|
}
|
|
|
|
func parseTableMetadataEnvelope(metadataBytes []byte, bucketName, tablePath string) (*tableState, error) {
|
|
var envelope tableMetadataEnvelope
|
|
if err := json.Unmarshal(metadataBytes, &envelope); err != nil {
|
|
return nil, fmt.Errorf("parse metadata xattr: %w", err)
|
|
}
|
|
if envelope.Metadata == nil || len(envelope.Metadata.FullMetadata) == 0 {
|
|
return nil, fmt.Errorf("no fullMetadata in table xattr")
|
|
}
|
|
|
|
meta, err := table.ParseMetadataBytes(envelope.Metadata.FullMetadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse iceberg metadata: %w", err)
|
|
}
|
|
|
|
var index *planningIndex
|
|
if len(envelope.PlanningIndex) > 0 {
|
|
if err := json.Unmarshal(envelope.PlanningIndex, &index); err != nil {
|
|
glog.V(2).Infof("iceberg maintenance: ignoring invalid planning index cache: %v", err)
|
|
index = nil
|
|
}
|
|
}
|
|
|
|
metadataFileName := metadataFileNameFromLocation(envelope.MetadataLocation)
|
|
if metadataFileName == "" {
|
|
metadataFileName = fmt.Sprintf("v%d.metadata.json", envelope.MetadataVersion)
|
|
}
|
|
return &tableState{
|
|
Metadata: meta,
|
|
MetadataFileName: metadataFileName,
|
|
DataPath: tableDataPath(bucketName, tablePath, envelope.MetadataLocation),
|
|
PlanningIndex: index,
|
|
}, nil
|
|
}
|
|
|
|
// tableDataPath resolves the bucket-relative directory holding a table's files.
|
|
// The catalog records the table's real location in the metadata location, which
|
|
// is the catalog path only for tables the catalog placed there itself: a client
|
|
// creating a table through the REST catalog can be handed a location elsewhere
|
|
// in the bucket, e.g. "ns/table-<uuid>" when the catalog path was occupied.
|
|
// Locations outside the table's own bucket fall back to the catalog path.
|
|
func tableDataPath(bucketName, tablePath, metadataLocation string) string {
|
|
dataDir := s3tables.TableDataDirFromMetadataLocation(metadataLocation)
|
|
if rel, ok := cutPathPrefix(dataDir, path.Join(s3tables.TablesPath, bucketName)); ok && rel != "" {
|
|
return rel
|
|
}
|
|
return tablePath
|
|
}
|
|
|
|
func (idx *planningIndex) matchesSnapshot(meta table.Metadata) bool {
|
|
if idx == nil {
|
|
return false
|
|
}
|
|
currentSnap := meta.CurrentSnapshot()
|
|
if currentSnap == nil || currentSnap.ManifestList == "" {
|
|
return false
|
|
}
|
|
return idx.SnapshotID == currentSnap.SnapshotID && idx.ManifestList == currentSnap.ManifestList
|
|
}
|
|
|
|
func (idx *planningIndex) compactionEligible(config Config) (bool, bool) {
|
|
if idx == nil || idx.Compaction == nil {
|
|
return false, false
|
|
}
|
|
if idx.Compaction.ConfigHash != compactionPlanningConfigHash(config) {
|
|
return false, false
|
|
}
|
|
return idx.Compaction.Eligible, true
|
|
}
|
|
|
|
func (idx *planningIndex) rewriteManifestsEligible(config Config) (bool, bool) {
|
|
if idx == nil || idx.RewriteManifests == nil {
|
|
return false, false
|
|
}
|
|
if idx.RewriteManifests.Threshold != config.MinManifestsToRewrite {
|
|
return false, false
|
|
}
|
|
return idx.RewriteManifests.Eligible, true
|
|
}
|
|
|
|
func compactionPlanningConfigHash(config Config) string {
|
|
return fmt.Sprintf("target=%d|min=%d|strategy=%s|sortcap=%d",
|
|
config.TargetFileSizeBytes, config.MinInputFiles,
|
|
config.RewriteStrategy, config.SortMaxInputBytes)
|
|
}
|
|
|
|
func operationRequested(ops []string, wanted string) bool {
|
|
for _, op := range ops {
|
|
if op == wanted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mergePlanningIndexSections(index, existing *planningIndex) *planningIndex {
|
|
if index == nil || existing == nil {
|
|
return index
|
|
}
|
|
if index.SnapshotID != existing.SnapshotID || index.ManifestList != existing.ManifestList {
|
|
return index
|
|
}
|
|
if index.Compaction == nil && existing.Compaction != nil {
|
|
compactionCopy := *existing.Compaction
|
|
index.Compaction = &compactionCopy
|
|
}
|
|
if index.RewriteManifests == nil && existing.RewriteManifests != nil {
|
|
rewriteCopy := *existing.RewriteManifests
|
|
index.RewriteManifests = &rewriteCopy
|
|
}
|
|
return index
|
|
}
|
|
|
|
func buildPlanningIndexFromManifests(
|
|
ctx context.Context,
|
|
filerClient filer_pb.SeaweedFilerClient,
|
|
bucketName, dataPath string,
|
|
meta table.Metadata,
|
|
config Config,
|
|
ops []string,
|
|
manifests []iceberg.ManifestFile,
|
|
) (*planningIndex, error) {
|
|
currentSnap := meta.CurrentSnapshot()
|
|
if currentSnap == nil || currentSnap.ManifestList == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
index := &planningIndex{
|
|
SnapshotID: currentSnap.SnapshotID,
|
|
ManifestList: currentSnap.ManifestList,
|
|
UpdatedAtMs: time.Now().UnixMilli(),
|
|
DataManifestCount: countDataManifests(manifests),
|
|
}
|
|
|
|
if operationRequested(ops, "compact") {
|
|
eligible, err := hasEligibleCompaction(ctx, filerClient, bucketName, dataPath, manifests, config, meta, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index.Compaction = &planningIndexCompaction{
|
|
ConfigHash: compactionPlanningConfigHash(config),
|
|
Eligible: eligible,
|
|
}
|
|
}
|
|
|
|
if operationRequested(ops, "rewrite_manifests") {
|
|
index.RewriteManifests = &planningIndexRewriteManifests{
|
|
Threshold: config.MinManifestsToRewrite,
|
|
Eligible: index.DataManifestCount >= config.MinManifestsToRewrite,
|
|
}
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
|
|
func persistPlanningIndex(
|
|
ctx context.Context,
|
|
client filer_pb.SeaweedFilerClient,
|
|
bucketName, tablePath string,
|
|
index *planningIndex,
|
|
) error {
|
|
if index == nil {
|
|
return nil
|
|
}
|
|
|
|
tableDir := path.Join(s3tables.TablesPath, bucketName, tablePath)
|
|
tableName := path.Base(tableDir)
|
|
parentDir := path.Dir(tableDir)
|
|
|
|
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: parentDir,
|
|
Name: tableName,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("lookup table entry: %w", err)
|
|
}
|
|
if resp == nil || resp.Entry == nil {
|
|
return fmt.Errorf("table entry not found")
|
|
}
|
|
|
|
existingXattr, ok := resp.Entry.Extended[s3tables.ExtendedKeyMetadata]
|
|
if !ok || len(existingXattr) == 0 {
|
|
return fmt.Errorf("no metadata xattr on table entry")
|
|
}
|
|
|
|
var internalMeta map[string]json.RawMessage
|
|
if err := json.Unmarshal(existingXattr, &internalMeta); err != nil {
|
|
return fmt.Errorf("unmarshal metadata xattr: %w", err)
|
|
}
|
|
if existingState, err := parseTableMetadataEnvelope(existingXattr, bucketName, tablePath); err == nil {
|
|
index = mergePlanningIndexSections(index, existingState.PlanningIndex)
|
|
}
|
|
|
|
indexJSON, err := json.Marshal(index)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal planning index: %w", err)
|
|
}
|
|
internalMeta["planningIndex"] = indexJSON
|
|
|
|
updatedXattr, err := json.Marshal(internalMeta)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal updated metadata xattr: %w", err)
|
|
}
|
|
|
|
expectedExtended := map[string][]byte{
|
|
s3tables.ExtendedKeyMetadata: existingXattr,
|
|
}
|
|
if expectedVersionXattr, ok := resp.Entry.Extended[s3tables.ExtendedKeyMetadataVersion]; ok && len(expectedVersionXattr) > 0 {
|
|
expectedExtended[s3tables.ExtendedKeyMetadataVersion] = expectedVersionXattr
|
|
}
|
|
resp.Entry.Extended[s3tables.ExtendedKeyMetadata] = updatedXattr
|
|
_, err = client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{
|
|
Directory: parentDir,
|
|
Entry: resp.Entry,
|
|
ExpectedExtended: expectedExtended,
|
|
})
|
|
if err != nil {
|
|
if status.Code(err) == codes.FailedPrecondition {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("update table entry: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|