diff --git a/weed/s3api/iceberg/iceberg_create_table_conflict_test.go b/weed/s3api/iceberg/iceberg_create_table_conflict_test.go index 53b0477de9..7b3c12db54 100644 --- a/weed/s3api/iceberg/iceberg_create_table_conflict_test.go +++ b/weed/s3api/iceberg/iceberg_create_table_conflict_test.go @@ -940,6 +940,26 @@ func TestFinalizeCreateOnCommitLosesToTableWithoutLocation(t *testing.T) { e.assertWinnerRecord("", winnerUUID) } +// A table directory another create made and wrote its record onto after this +// writer's lookup missed it keeps that record when a metadata file is staged. +func TestSaveMetadataBlobKeepsATableDirectoryCreatedSinceTheLookup(t *testing.T) { + e := newConflictEnv(t) + namespaceDir := path.Join(s3tables.TablesPath, conflictBucket, conflictNamespace) + record := `{"Name":"` + conflictTable + `"}` + e.hooks.beforeCreate = func(req *filer_pb.CreateEntryRequest) { + if req.Directory == namespaceDir && req.Entry.Name == conflictTable { + e.hooks.beforeCreate = nil + e.filer.Put(namespaceDir, conflictTable, map[string][]byte{s3tables.ExtendedKeyMetadata: []byte(record)}) + } + } + if err := e.server.saveMetadataBlob(t.Context(), conflictBucket, path.Join(conflictNamespace, conflictTable), "v1.metadata.json", []byte("{}"), "application/json", true, nil); err != nil { + t.Fatalf("save metadata: %v", err) + } + if got := e.filer.Get(namespaceDir, conflictTable); got == nil || string(got.Extended[s3tables.ExtendedKeyMetadata]) != record { + t.Errorf("table record replaced by the directory create: %+v", got) + } +} + // Two stage-create commits for one name: one creates the table and the other is // told it lost, and the file the catalog points at belongs to the winner. func TestConcurrentFinalizeCreateOnCommitHasOneWinner(t *testing.T) { diff --git a/weed/s3api/iceberg/metadata_files.go b/weed/s3api/iceberg/metadata_files.go index 635d36daf6..ad68aa7a6a 100644 --- a/weed/s3api/iceberg/metadata_files.go +++ b/weed/s3api/iceberg/metadata_files.go @@ -68,9 +68,11 @@ func (s *Server) saveMetadataBlob(ctx context.Context, bucketName, tablePath, me return fmt.Errorf("lookup %s failed: %w", errorContext, err) } - // If lookup fails with ErrNotFound, try to create the directory. + // Exclusive, so a directory created since the lookup keeps the + // record a table create may have written onto it. resp, createErr := client.CreateEntry(opCtx, &filer_pb.CreateEntryRequest{ Directory: parent, + OExcl: true, Entry: &filer_pb.Entry{ Name: name, IsDirectory: true,