laskoviymishka commented on code in PR #1326:
URL: https://github.com/apache/iceberg-go/pull/1326#discussion_r3530090958
##########
catalog/hadoop/hadoop.go:
##########
@@ -689,18 +711,82 @@ func (c *Catalog) CommitTable(ctx context.Context, ident
table.Identifier, reqs
return updated, newMetaPath, nil
}
-func (c *Catalog) commitMetadataFile(ident table.Identifier, tempPath,
metaPath string, conflictErr error) error {
- if err := c.filesystem.RenameNoReplace(tempPath, metaPath); err != nil {
- _ = c.filesystem.Remove(tempPath)
+func (c *Catalog) commitMetadataFile(ident table.Identifier, version int,
tempPath, metaPath string, conflictErr error) error {
+ claimPath := c.metadataVersionClaimPath(ident, version)
+ for {
+ if err := c.filesystem.RenameNoReplace(tempPath, claimPath);
err != nil {
+ if !errors.Is(err, fs.ErrExist) {
+ _ = c.filesystem.Remove(tempPath)
+
+ return fmt.Errorf("hadoop catalog: failed to
claim metadata version: %w", err)
+ }
+
+ existingPath, exists :=
c.metadataVersionLocation(ident, version)
+ if exists {
+ _ = c.filesystem.Remove(tempPath)
+
+ return fmt.Errorf("%w: metadata file already
exists for table %s: %s",
+ conflictErr, strings.Join(ident, "."),
existingPath)
+ }
+
+ claimInfo, err := c.filesystem.Stat(claimPath)
+ if err != nil {
Review Comment:
Small one that folds into the crash-recovery thread zeroshade opened: if a
competitor cleans the claim and publishes between the failing rename above and
this `Stat`, `Stat` returns `fs.ErrNotExist` and we return a generic "failed to
inspect stale metadata claim" that isn't wrapped in `conflictErr` — so a caller
distinguishing retryable-conflict from abort-infra spuriously aborts on a race
that actually resolved fine.
I'd special-case `fs.ErrNotExist` here and just `continue` — the top of the
loop re-checks `metadataVersionLocation` and will surface a proper conflict if
it was published.
##########
catalog/hadoop/hadoop.go:
##########
@@ -450,6 +467,17 @@ func (c *Catalog) metadataVersionExists(ident
table.Identifier, version int) (bo
return found, nil
}
+func (c *Catalog) metadataVersionLocation(ident table.Identifier, version int)
(string, bool) {
+ files, _, err := c.scanMetadataFiles(ident)
+ if err != nil {
+ return "", false
Review Comment:
This is the path canonicalization from last round and it reads well — but
the helper it's built on drops its error: any `scanMetadataFiles` failure
returns `("", false)`, which is indistinguishable from "this version doesn't
exist."
That's load-bearing at the post-claim call to this in `commitMetadataFile` —
it's the only cross-codec conflict guard left. If `WalkDir` hits a transient
error there, we get `false`, fall through to `RenameNoReplace` onto the plain
filename (which succeeds because that path doesn't exist), and end up with
`v2.metadata.json` and `v2.gz.metadata.json` both at the same version.
`betterThan` then prefers the plain file and the compressed commit is silently
gone.
I'd give it an error return and fail the commit on it rather than folding it
into absence:
```go
func (c *Catalog) metadataVersionLocation(ident table.Identifier, version
int) (string, bool, error) {
files, _, err := c.scanMetadataFiles(ident)
if err != nil {
return "", false, err
}
file, ok := files[version]
return file.location, ok, nil
}
```
Then propagate it at the call site as an infra error (not wrapped in
`conflictErr`), so a scan failure aborts the commit instead of publishing over
a live version.
##########
catalog/hadoop/hadoop_test.go:
##########
@@ -1488,7 +1605,144 @@ func (s *HadoopCatalogTestSuite)
TestCreateTableConcurrentMetadataPublishConflic
s.Equal(1, successes)
s.Equal(1, conflicts)
- s.FileExists(s.cat.metadataFilePath([]string{"ns", "tbl"}, 1))
+ metaPath, err := s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecNone)
+ s.Require().NoError(err)
+ s.FileExists(metaPath)
+ s.NoFileExists(s.cat.metadataVersionClaimPath(ident, 1))
+}
+
+func (s *HadoopCatalogTestSuite)
TestCreateTableConcurrentMixedCodecVersionClaim() {
+ ctx := context.Background()
+ s.Require().NoError(os.Mkdir(filepath.Join(s.warehouse, "ns"), 0o755))
+ ident := []string{"ns", "tbl"}
+ s.cat.filesystem =
newBarrierRenameNoReplaceFS(filepath.Base(s.cat.metadataVersionClaimPath(ident,
1)))
+
+ type createResult struct {
+ metaLoc string
+ err error
+ }
+
+ results := make(chan createResult, 2)
+ var wg sync.WaitGroup
+ for _, props := range []iceberg.Properties{
+ nil,
+ {table.MetadataCompressionKey:
table.MetadataCompressionCodecGzip},
+ } {
+ props := props
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ opts := []catalog.CreateTableOpt(nil)
+ if props != nil {
+ opts = append(opts,
catalog.WithProperties(props))
+ }
+
+ tbl, err := s.cat.CreateTable(ctx, ident,
s.testSchema(), opts...)
+ result := createResult{err: err}
+ if err == nil {
+ result.metaLoc = tbl.MetadataLocation()
+ }
+
+ results <- result
+ }()
+ }
+
+ wg.Wait()
+ close(results)
+
+ successes := 0
+ conflicts := 0
+ var successLoc string
+ for result := range results {
+ if result.err == nil {
+ successes++
+ successLoc = result.metaLoc
+ s.Contains([]string{"v1.metadata.json",
"v1.gz.metadata.json"}, filepath.Base(result.metaLoc))
+
+ continue
+ }
+
+ conflicts++
+ s.ErrorIs(result.err, catalog.ErrTableAlreadyExists)
+ }
+
+ s.Equal(1, successes)
+ s.Equal(1, conflicts)
+
+ plainPath, err := s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecNone)
+ s.Require().NoError(err)
+ gzipPath, err := s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecGzip)
+ s.Require().NoError(err)
+
+ existing := 0
+ for _, path := range []string{plainPath, gzipPath} {
+ if _, err := os.Stat(path); err == nil {
+ existing++
+ } else {
+ s.True(os.IsNotExist(err))
Review Comment:
`os.IsNotExist` doesn't unwrap, so this misses a wrapped not-exist.
`s.ErrorIs(err, fs.ErrNotExist)` is the unwrapping form — same pattern in the
other new mixed-codec test.
##########
catalog/hadoop/hadoop_test.go:
##########
@@ -1488,7 +1605,144 @@ func (s *HadoopCatalogTestSuite)
TestCreateTableConcurrentMetadataPublishConflic
s.Equal(1, successes)
s.Equal(1, conflicts)
- s.FileExists(s.cat.metadataFilePath([]string{"ns", "tbl"}, 1))
+ metaPath, err := s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecNone)
+ s.Require().NoError(err)
+ s.FileExists(metaPath)
+ s.NoFileExists(s.cat.metadataVersionClaimPath(ident, 1))
+}
+
+func (s *HadoopCatalogTestSuite)
TestCreateTableConcurrentMixedCodecVersionClaim() {
+ ctx := context.Background()
+ s.Require().NoError(os.Mkdir(filepath.Join(s.warehouse, "ns"), 0o755))
+ ident := []string{"ns", "tbl"}
+ s.cat.filesystem =
newBarrierRenameNoReplaceFS(filepath.Base(s.cat.metadataVersionClaimPath(ident,
1)))
+
+ type createResult struct {
+ metaLoc string
+ err error
+ }
+
+ results := make(chan createResult, 2)
+ var wg sync.WaitGroup
+ for _, props := range []iceberg.Properties{
+ nil,
+ {table.MetadataCompressionKey:
table.MetadataCompressionCodecGzip},
+ } {
+ props := props
Review Comment:
go.mod is on 1.25, so the per-iteration loop var is automatic — this copy
(and the one in the commit-side test) is redundant.
##########
catalog/hadoop/hadoop.go:
##########
@@ -655,31 +687,21 @@ func (c *Catalog) CommitTable(ctx context.Context, ident
table.Identifier, reqs
return nil, "", fmt.Errorf("hadoop catalog: failed to create
metadata directory: %w", err)
}
- newMetaPath := c.metadataFilePath(ident, newVersion)
- tempPath := joinPath(c.isLocal, metaDir,
uuid.New().String()+".metadata.json")
-
compression := updated.Properties().Get(table.MetadataCompressionKey,
table.MetadataCompressionDefault)
+ newMetaPath, err := c.metadataFilePathForCompression(ident, newVersion,
compression)
+ if err != nil {
+ return nil, "", fmt.Errorf("hadoop catalog: failed to determine
metadata file path: %w", err)
+ }
+
+ tempPath := joinPath(c.isLocal, metaDir,
uuid.New().String()+".metadata.json")
if err := internal.WriteTableMetadata(updated, c.filesystem, tempPath,
compression); err != nil {
_ = c.filesystem.Remove(tempPath)
return nil, "", fmt.Errorf("hadoop catalog: failed to write
table metadata: %w", err)
}
- exists, err := c.metadataVersionExists(ident, newVersion)
- if err != nil {
- _ = c.filesystem.Remove(tempPath)
-
- return nil, "", fmt.Errorf("hadoop catalog: failed to inspect
metadata directory for version %d: %w",
- newVersion, err)
- }
- if exists {
- _ = c.filesystem.Remove(tempPath)
-
- return nil, "", fmt.Errorf("hadoop catalog: version %d already
exists for table %s",
- newVersion, strings.Join(ident, "."))
- }
- if err := c.commitMetadataFile(ident, tempPath, newMetaPath,
table.ErrCommitFailed); err != nil {
+ if err := c.commitMetadataFile(ident, newVersion, tempPath,
newMetaPath, table.ErrCommitFailed); err != nil {
Review Comment:
With this pre-check gone, `metadataVersionExists` loses its last caller —
the claim protocol supersedes it. I'd drop it while we're here so it doesn't
drift out of sync with the codec-aware path.
##########
catalog/hadoop/hadoop_test.go:
##########
@@ -564,18 +613,29 @@ func (s *HadoopCatalogTestSuite)
TestIsTableDirWithGzipMetadata() {
s.True(s.requireIsTableDir(tableDir))
}
+func (s *HadoopCatalogTestSuite) TestIsTableDirIgnoresZstdMetadata() {
+ tableDir := filepath.Join(s.warehouse, "ns", "tbl")
+ metaDir := filepath.Join(tableDir, "metadata")
+ s.Require().NoError(os.MkdirAll(metaDir, 0o755))
+ s.Require().NoError(os.WriteFile(filepath.Join(metaDir,
"v1.zstd.metadata.json"), nil, 0o644))
+
+ s.False(isTableDir(s.cat.filesystem, s.cat.isLocal, tableDir))
Review Comment:
`isTableDir` returns an error alongside the bool, and passing the call
straight into `s.False` slides it into the assertion's message args instead of
checking it. The sibling tests go through `requireIsTableDir`, which surfaces
it — I'd use that here too so a real scan failure can't masquerade as a clean
false.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]