This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 48deb03f7 refactor(io)!: add context to IO.Open and IO.Remove (#2067)
48deb03f7 is described below
commit 48deb03f794386c647a5e9b4a3b14a36b269d262
Author: Andrei Tserakhau <[email protected]>
AuthorDate: Mon Oct 26 16:22:15 2026 +0100
refactor(io)!: add context to IO.Open and IO.Remove (#2067)
* refactor(io)!: add context to IO.Open and IO.Remove
`io.IO.Open` and `Remove` now take `context.Context`, matching
`BulkRemovableIO.DeleteFiles`.
`blobfs.FileIO.Remove` forwards the caller context so deletes honor
cancellation/deadlines.
`maintenance.WithCleanupDeleteFunc` now takes `func(context.Context,
string) error`.
Breaking change: all `IO` implementations/callers must pass a context, and
custom delete callbacks must update their signature.
Part of the v1.0 API cleanup in #2062.
Signed-off-by: laskoviymishka <[email protected]>
* fix(io): keep failure cleanup immune to caller cancellation
Moving blobfs Remove onto the caller context made cleanup after a failed
operation fail with it: when the failure was itself a cancellation or a
timeout, the removal was canceled too and files that used to be cleaned
up were left behind on object stores. Before this change Remove ran on
the context captured when the FileIO was built, so a canceled caller
never reached it.
Run those cleanup paths on context.WithoutCancel(ctx), which keeps the
context's values (tracing and similar) but not its cancellation:
- doCommit's deferred removal of orphaned manifest lists
- the hadoop catalog's temp metadata file and version claim removals
- sql removeUncommittedMetadata, which already loaded the filesystem
without cancellation but removed with the caller context
- the removal of a partially written delete manifest in
writeDeletedEntries
- the Glue S3 Tables commit rollback
Paths where removal is the requested work (orphan cleanup, PurgeFiles,
expire snapshots' post-commit deletes, drop namespace and drop view)
keep the caller context so they stay cancellable.
Tests use an IO that rejects Remove on a canceled context, as blobfs
does, to check that doCommit and removeUncommittedMetadata still clean
up after a cancellation.
Also reword the IO.Open doc to say implementations may use ctx to bound
the open, since no backend does yet, and drop the comments claiming
every backend ignores ctx, which would go stale once one honors it.
Signed-off-by: laskoviymishka <[email protected]>
* refactor(table): thread ctx through the manifest content cache IO
The manifest content cache that landed on main wraps the scan FileIO
with the old context-free Open and Remove, so the merge no longer
built. Give manifestContentCacheIO the context-taking signatures.
Opens of other paths and Remove forward the caller's context. The
cached manifest path keeps using the scan context captured in wrap,
because the manifest readers that call Open have no caller context to
pass and would otherwise drop the scan's cancellation. The cache's
own opens, including the population read and the fallback opens, use
that same context.
Signed-off-by: laskoviymishka <[email protected]>
---------
Signed-off-by: laskoviymishka <[email protected]>
---
catalog/glue/glue.go | 6 +-
catalog/glue/glue_test.go | 4 +-
catalog/hadoop/hadoop.go | 36 +++++-----
catalog/hadoop/hadoop_test.go | 6 +-
catalog/hive/hive.go | 2 +-
catalog/hive/hive_test.go | 4 +-
catalog/rest/rest_integration_test.go | 6 +-
catalog/rest/scan_planning_integration_test.go | 2 +-
catalog/rest/scan_planning_test.go | 2 +-
catalog/rest/vended_creds.go | 8 +--
catalog/rest/vended_creds_test.go | 2 +-
.../remove_uncommitted_metadata_internal_test.go | 63 +++++++++++++++++
catalog/sql/sql.go | 9 ++-
catalog/sql/sql_integration_test.go | 2 +-
cmd/iceberg/clean_orphan_files_test.go | 2 +-
internal/iomock/mock_fs.go | 5 +-
io/gocloud/blobfs/blob.go | 10 +--
io/gocloud/blobfs/blob_test.go | 14 ++--
io/io.go | 25 +++++--
io/io_test.go | 5 +-
io/local.go | 5 +-
io/local_test.go | 3 +-
io/mem.go | 4 +-
io/mem_test.go | 28 ++++----
manifest_projection.go | 4 +-
table/all_manifests_internal_test.go | 4 +-
table/arrow_scanner_bench_test.go | 2 +-
table/arrow_scanner_lazy_delete_regression_test.go | 4 +-
table/arrow_scanner_reorder_test.go | 4 +-
table/arrow_scanner_test.go | 6 +-
table/commit_retry_test.go | 80 +++++++++++++++++++++-
table/conflict_validation.go | 9 +--
table/conflict_validation_bench_test.go | 5 +-
table/conflict_validation_test.go | 7 +-
table/dv/deletion_vector.go | 4 +-
table/dv/deletion_vector_test.go | 13 ++--
table/dv/dv_cross_client_test.go | 5 +-
table/dv/dv_writer_test.go | 2 +-
table/dv_scanner_read_test.go | 4 +-
table/empty_scan_task_regression_test.go | 2 +-
table/equality_delete_reader_bench_test.go | 2 +-
table/equality_delete_reader_internal_test.go | 4 +-
table/equality_delete_writer_test.go | 2 +-
table/geo_write_test.go | 2 +-
table/incremental_append_scan_test.go | 6 +-
table/incremental_changelog_scan_test.go | 10 +--
table/inspect_internal_test.go | 4 +-
table/inspect_partitions_parallel_test.go | 4 +-
table/internal/parquet_files.go | 7 +-
table/internal/parquet_files_test.go | 10 +--
table/lazy_deletion_vector_bench_test.go | 4 +-
table/lazy_deletion_vector_test.go | 16 +++--
table/maintenance/manifest_test_helpers_test.go | 21 +++---
table/maintenance/orphan_cleanup.go | 14 ++--
table/maintenance/orphan_cleanup_bench_test.go | 2 +-
.../maintenance/orphan_cleanup_integration_test.go | 10 +--
table/maintenance/orphan_cleanup_test.go | 28 ++++----
table/manifest_content_cache.go | 24 ++++---
table/manifest_content_cache_bench_test.go | 4 +-
table/manifest_content_cache_internal_test.go | 16 ++---
table/manifest_list_partitions_regression_test.go | 2 +-
table/partition_residual_read_test.go | 2 +-
table/rewrite_data_files.go | 2 +-
table/rewrite_manifests.go | 2 +-
table/rewrite_manifests_bench_test.go | 2 +-
table/rewrite_manifests_cluster.go | 3 +-
table/rewrite_manifests_test.go | 4 +-
table/row_lineage_rewrite_bench_test.go | 2 +-
table/row_lineage_scan_test.go | 2 +-
table/scan_splits_test.go | 4 +-
table/scanner_internal_test.go | 16 ++---
table/snapshot_manifest_cache_bench_test.go | 4 +-
table/snapshot_manifest_cache_internal_test.go | 8 +--
table/snapshot_producers.go | 15 ++--
table/snapshot_producers_bench_test.go | 4 +-
table/snapshot_producers_test.go | 14 ++--
table/snapshots.go | 6 +-
table/table.go | 10 ++-
table/table_test.go | 6 +-
table/transaction_test.go | 2 +-
table/updates.go | 2 +-
table/updates_test.go | 8 +--
table/write_records_test.go | 4 +-
view/view.go | 2 +-
view/view_test.go | 2 +-
85 files changed, 459 insertions(+), 267 deletions(-)
diff --git a/catalog/glue/glue.go b/catalog/glue/glue.go
index 9e70b0153..c4fc04f5f 100644
--- a/catalog/glue/glue.go
+++ b/catalog/glue/glue.go
@@ -487,8 +487,10 @@ func (c *Catalog) commitS3TablesTable(ctx context.Context,
database, tableName s
if err != nil {
// Best-effort: drop the metadata object we just wrote; for S3
Tables the
// managed location may be unreachable, so leave any remainder
to the service.
- if fs, fsErr := staged.FS(ctx); fsErr == nil {
- _ = fs.Remove(staged.MetadataLocation())
+ // The commit may have failed on ctx's cancellation, so do not
inherit it.
+ cleanupCtx := context.WithoutCancel(ctx)
+ if fs, fsErr := staged.FS(cleanupCtx); fsErr == nil {
+ _ = fs.Remove(cleanupCtx, staged.MetadataLocation())
}
return fmt.Errorf("failed to commit S3 Tables table %s.%s: %w",
database, tableName, err)
diff --git a/catalog/glue/glue_test.go b/catalog/glue/glue_test.go
index 2e20db483..07f688f33 100644
--- a/catalog/glue/glue_test.go
+++ b/catalog/glue/glue_test.go
@@ -913,7 +913,7 @@ func TestGluePurgeTableSwallowsPurgeFilesError(t
*testing.T) {
assert.True(dropCalled.Load())
assert.Positive(removeCalls.Load())
assert.False(removeBeforeDrop.Load(), "PurgeTable should drop the
catalog entry before removing files")
- file, err := failingFS.Open(dataFile)
+ file, err := failingFS.Open(context.Background(), dataFile)
assert.NoError(err, "data file should remain when FileIO remove fails")
assert.NotNil(file)
assert.NoError(file.Close())
@@ -952,7 +952,7 @@ type failRemoveIO struct {
onRemove func()
}
-func (f failRemoveIO) Remove(string) error {
+func (f failRemoveIO) Remove(context.Context, string) error {
if f.onRemove != nil {
f.onRemove()
}
diff --git a/catalog/hadoop/hadoop.go b/catalog/hadoop/hadoop.go
index 822126bf1..005015b9c 100644
--- a/catalog/hadoop/hadoop.go
+++ b/catalog/hadoop/hadoop.go
@@ -425,7 +425,8 @@ func (c *Catalog) writeVersionHint(ident table.Identifier,
version int) {
if err := c.filesystem.Rename(tempPath, hintPath); err != nil {
log.Printf("hadoop catalog: failed to rename version hint: %v",
err)
- _ = c.filesystem.Remove(tempPath)
+ // Best-effort cleanup of the version-hint temp file; no caller
context.
+ _ = c.filesystem.Remove(context.Background(), tempPath)
}
}
@@ -573,12 +574,12 @@ func (c *Catalog) CreateTable(ctx context.Context, ident
table.Identifier, sc *i
tempPath := joinPath(c.isLocal, metaDir,
uuid.New().String()+".metadata.json")
if err := internal.WriteTableMetadata(metadata, c.filesystem, tempPath,
compression); err != nil {
- _ = c.filesystem.Remove(tempPath)
+ _ = c.filesystem.Remove(context.WithoutCancel(ctx), tempPath)
return nil, fmt.Errorf("hadoop catalog: failed to write table
metadata: %w", err)
}
- if err := c.commitMetadataFile(ident, version, tempPath, metaPath,
catalog.ErrTableAlreadyExists); err != nil {
+ if err := c.commitMetadataFile(ctx, ident, version, tempPath, metaPath,
catalog.ErrTableAlreadyExists); err != nil {
return nil, err
}
@@ -712,12 +713,12 @@ func (c *Catalog) CommitTable(ctx context.Context, ident
table.Identifier, reqs
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)
+ _ = c.filesystem.Remove(context.WithoutCancel(ctx), tempPath)
return nil, "", fmt.Errorf("hadoop catalog: failed to write
table metadata: %w", err)
}
- if err := c.commitMetadataFile(ident, newVersion, tempPath,
newMetaPath, table.ErrCommitFailed); err != nil {
+ if err := c.commitMetadataFile(ctx, ident, newVersion, tempPath,
newMetaPath, table.ErrCommitFailed); err != nil {
return nil, "", err
}
@@ -727,25 +728,28 @@ func (c *Catalog) CommitTable(ctx context.Context, ident
table.Identifier, reqs
return updated, newMetaPath, nil
}
-func (c *Catalog) commitMetadataFile(ident table.Identifier, version int,
tempPath, metaPath string, conflictErr error) error {
+func (c *Catalog) commitMetadataFile(ctx context.Context, ident
table.Identifier, version int, tempPath, metaPath string, conflictErr error)
error {
claimPath := c.metadataVersionClaimPath(ident, version)
+ // Cleanup on a failed commit must not inherit ctx's cancellation, or a
+ // canceled commit would leave the temp file or the version claim
behind.
+ cleanupCtx := context.WithoutCancel(ctx)
for {
if err := c.filesystem.RenameNoReplace(tempPath, claimPath);
err != nil {
if !errors.Is(err, fs.ErrExist) {
- _ = c.filesystem.Remove(tempPath)
+ _ = c.filesystem.Remove(cleanupCtx, tempPath)
return fmt.Errorf("hadoop catalog: failed to
claim metadata version: %w", err)
}
existingPath, exists, err :=
c.metadataVersionLocation(ident, version)
if err != nil {
- _ = c.filesystem.Remove(tempPath)
+ _ = c.filesystem.Remove(cleanupCtx, tempPath)
return fmt.Errorf("hadoop catalog: failed to
inspect metadata directory for version %d: %w",
version, err)
}
if exists {
- _ = c.filesystem.Remove(tempPath)
+ _ = c.filesystem.Remove(cleanupCtx, tempPath)
return fmt.Errorf("%w: metadata file already
exists for table %s: %s",
conflictErr, strings.Join(ident, "."),
existingPath)
@@ -757,19 +761,19 @@ func (c *Catalog) commitMetadataFile(ident
table.Identifier, version int, tempPa
continue
}
- _ = c.filesystem.Remove(tempPath)
+ _ = c.filesystem.Remove(cleanupCtx, tempPath)
return fmt.Errorf("hadoop catalog: failed to
inspect stale metadata claim %s: %w", claimPath, err)
}
if time.Since(claimInfo.ModTime()) <
metadataClaimStaleAfter {
- _ = c.filesystem.Remove(tempPath)
+ _ = c.filesystem.Remove(cleanupCtx, tempPath)
return fmt.Errorf("%w: metadata version already
claimed for table %s: %s",
conflictErr, strings.Join(ident, "."),
claimPath)
}
- if err := c.filesystem.Remove(claimPath); err != nil &&
!errors.Is(err, fs.ErrNotExist) {
- _ = c.filesystem.Remove(tempPath)
+ if err := c.filesystem.Remove(ctx, claimPath); err !=
nil && !errors.Is(err, fs.ErrNotExist) {
+ _ = c.filesystem.Remove(cleanupCtx, tempPath)
return fmt.Errorf("hadoop catalog: failed to
clear stale metadata claim %s: %w", claimPath, err)
}
@@ -783,7 +787,7 @@ func (c *Catalog) commitMetadataFile(ident
table.Identifier, version int, tempPa
removeClaim := true
defer func() {
if removeClaim {
- _ = c.filesystem.Remove(claimPath)
+ _ = c.filesystem.Remove(cleanupCtx, claimPath)
}
}()
@@ -957,7 +961,7 @@ func (c *Catalog) CreateNamespace(_ context.Context, ns
table.Identifier, props
return nil
}
-func (c *Catalog) DropNamespace(_ context.Context, ns table.Identifier) error {
+func (c *Catalog) DropNamespace(ctx context.Context, ns table.Identifier)
error {
if err := validateIdentifier(ns); err != nil {
return err
}
@@ -999,7 +1003,7 @@ func (c *Catalog) DropNamespace(_ context.Context, ns
table.Identifier) error {
return fmt.Errorf("%w: %s", catalog.ErrNamespaceNotEmpty,
strings.Join(ns, "."))
}
- return c.filesystem.Remove(path)
+ return c.filesystem.Remove(ctx, path)
}
func (c *Catalog) CheckNamespaceExists(_ context.Context, ns table.Identifier)
(bool, error) {
diff --git a/catalog/hadoop/hadoop_test.go b/catalog/hadoop/hadoop_test.go
index f3761237c..9571e6308 100644
--- a/catalog/hadoop/hadoop_test.go
+++ b/catalog/hadoop/hadoop_test.go
@@ -123,11 +123,11 @@ var _ HadoopCatalogFS = (*stubHadoopCatalogFS)(nil)
type stubIO struct{}
-func (stubIO) Open(string) (icebergio.File, error) {
+func (stubIO) Open(context.Context, string) (icebergio.File, error) {
return nil, fs.ErrNotExist
}
-func (stubIO) Remove(string) error {
+func (stubIO) Remove(context.Context, string) error {
return nil
}
@@ -1857,7 +1857,7 @@ func (s *HadoopCatalogTestSuite)
TestCommitMetadataFileFailsClosedOnMetadataScan
s.cat.filesystem = originalFS
}()
- err = s.cat.commitMetadataFile(ident, 1, tempPath, metaPath,
table.ErrCommitFailed)
+ err = s.cat.commitMetadataFile(context.Background(), ident, 1,
tempPath, metaPath, table.ErrCommitFailed)
s.Require().Error(err)
s.Contains(err.Error(), "failed to inspect metadata directory for
version 1")
s.ErrorIs(err, fs.ErrPermission)
diff --git a/catalog/hive/hive.go b/catalog/hive/hive.go
index 3354affa0..bb064cd63 100644
--- a/catalog/hive/hive.go
+++ b/catalog/hive/hive.go
@@ -784,7 +784,7 @@ func (c *Catalog) DropView(ctx context.Context, identifier
table.Identifier) err
return fmt.Errorf("failed to load filesystem for view metadata:
%w", err)
}
- if err := fs.Remove(metadataLocation); err != nil {
+ if err := fs.Remove(ctx, metadataLocation); err != nil {
return fmt.Errorf("failed to remove view metadata file at %s:
%w", metadataLocation, err)
}
diff --git a/catalog/hive/hive_test.go b/catalog/hive/hive_test.go
index e47024ed9..221413fc5 100644
--- a/catalog/hive/hive_test.go
+++ b/catalog/hive/hive_test.go
@@ -1016,7 +1016,7 @@ func TestHivePurgeTableSwallowsPurgeFilesError(t
*testing.T) {
assert.True(dropCalled.Load())
assert.Positive(removeCalls.Load())
assert.False(removeBeforeDrop.Load(), "PurgeTable should drop the
catalog entry before removing files")
- file, err := failingFS.Open(dataFile)
+ file, err := failingFS.Open(context.Background(), dataFile)
assert.NoError(err, "data file should remain when FileIO remove fails")
assert.NotNil(file)
assert.NoError(file.Close())
@@ -1053,7 +1053,7 @@ type failRemoveIO struct {
onRemove func()
}
-func (f failRemoveIO) Remove(string) error {
+func (f failRemoveIO) Remove(context.Context, string) error {
if f.onRemove != nil {
f.onRemove()
}
diff --git a/catalog/rest/rest_integration_test.go
b/catalog/rest/rest_integration_test.go
index 35dd2bd5b..7c4dfffb5 100644
--- a/catalog/rest/rest_integration_test.go
+++ b/catalog/rest/rest_integration_test.go
@@ -382,7 +382,7 @@ func (s *RestIntegrationSuite) TestWriteCommitTable() {
s.Require().NoError(err)
s.Require().NoError(pqarrow.WriteTable(table, fw, table.NumRows(),
nil, pqarrow.DefaultWriterProps()))
- defer mustFS(s.T(), tbl).Remove(pqfile)
+ defer mustFS(s.T(), tbl).Remove(s.ctx, pqfile)
txn := tbl.NewTransaction()
s.Require().NoError(txn.AddFiles(s.ctx, []string{pqfile}, nil, false))
@@ -458,10 +458,10 @@ func (s *RestIntegrationSuite) TestMultiTableCommit() {
// Write a parquet data file for each table.
pq1 := s.writeParquetFile(tbl1, tableSchemaSimple, "multi-txn-1")
- defer mustFS(s.T(), tbl1).Remove(pq1)
+ defer mustFS(s.T(), tbl1).Remove(s.ctx, pq1)
pq2 := s.writeParquetFile(tbl2, tableSchemaSimple, "multi-txn-2")
- defer mustFS(s.T(), tbl2).Remove(pq2)
+ defer mustFS(s.T(), tbl2).Remove(s.ctx, pq2)
// Build transactions that add data files to each table.
tx1 := tbl1.NewTransaction()
diff --git a/catalog/rest/scan_planning_integration_test.go
b/catalog/rest/scan_planning_integration_test.go
index 6d679c623..8f0e7cc94 100644
--- a/catalog/rest/scan_planning_integration_test.go
+++ b/catalog/rest/scan_planning_integration_test.go
@@ -265,7 +265,7 @@ func (s *RestIntegrationSuite) createScanPlanningTable(cat
*rest.Catalog, ident
s.Require().NoError(err)
s.Require().NoError(pqarrow.WriteTable(arrowTable, file,
arrowTable.NumRows(),
nil, pqarrow.DefaultWriterProps()))
- s.T().Cleanup(func() { s.Require().NoError(mustFS(s.T(),
tbl).Remove(dataPath)) })
+ s.T().Cleanup(func() { s.Require().NoError(mustFS(s.T(),
tbl).Remove(s.ctx, dataPath)) })
txn := tbl.NewTransaction()
s.Require().NoError(txn.AddFiles(s.ctx, []string{dataPath}, nil, false))
diff --git a/catalog/rest/scan_planning_test.go
b/catalog/rest/scan_planning_test.go
index 498050a4d..88f733a10 100644
--- a/catalog/rest/scan_planning_test.go
+++ b/catalog/rest/scan_planning_test.go
@@ -1977,7 +1977,7 @@ func TestPlanScopedIOAlreadyExpiredCredentials(t
*testing.T) {
fs, err := p.Load(context.Background())
require.NoError(t, err)
- _, err = fs.Open("file:///bucket/data.parquet")
+ _, err = fs.Open(context.Background(), "file:///bucket/data.parquet")
require.ErrorIs(t, err, ErrVendedCredentialsExpired)
}
diff --git a/catalog/rest/vended_creds.go b/catalog/rest/vended_creds.go
index 50f21ff04..81e190f02 100644
--- a/catalog/rest/vended_creds.go
+++ b/catalog/rest/vended_creds.go
@@ -362,22 +362,22 @@ func newPrefixScopedIO(ctx context.Context, baseProps
iceberg.Properties, creden
}
}
-func (p *prefixScopedIO) Open(name string) (iceio.File, error) {
+func (p *prefixScopedIO) Open(ctx context.Context, name string) (iceio.File,
error) {
fs, err := p.filesystemFor(name)
if err != nil {
return nil, err
}
- return fs.Open(name)
+ return fs.Open(ctx, name)
}
-func (p *prefixScopedIO) Remove(name string) error {
+func (p *prefixScopedIO) Remove(ctx context.Context, name string) error {
fs, err := p.filesystemFor(name)
if err != nil {
return err
}
- return fs.Remove(name)
+ return fs.Remove(ctx, name)
}
func (p *prefixScopedIO) filesystemFor(name string) (iceio.IO, error) {
diff --git a/catalog/rest/vended_creds_test.go
b/catalog/rest/vended_creds_test.go
index 463f71da5..6d6e1e0f6 100644
--- a/catalog/rest/vended_creds_test.go
+++ b/catalog/rest/vended_creds_test.go
@@ -1303,7 +1303,7 @@ func writeAndRead(t *testing.T, fs iceio.IO, location
string) {
require.True(t, ok, "%T is not writable", fs)
require.NoError(t, wfs.WriteFile(location, []byte("data")))
- f, err := fs.Open(location)
+ f, err := fs.Open(t.Context(), location)
require.NoError(t, err)
defer f.Close()
diff --git a/catalog/sql/remove_uncommitted_metadata_internal_test.go
b/catalog/sql/remove_uncommitted_metadata_internal_test.go
new file mode 100644
index 000000000..8d41f68b9
--- /dev/null
+++ b/catalog/sql/remove_uncommitted_metadata_internal_test.go
@@ -0,0 +1,63 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package sql
+
+import (
+ "context"
+ "testing"
+
+ iceio "github.com/apache/iceberg-go/io"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// cancelAwareRemoveIO records removals and, like blobfs, rejects Remove on a
+// canceled context.
+type cancelAwareRemoveIO struct {
+ iceio.IO
+ removed []string
+}
+
+func (c *cancelAwareRemoveIO) Remove(ctx context.Context, name string) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ c.removed = append(c.removed, name)
+
+ return nil
+}
+
+func TestRemoveUncommittedMetadataIgnoresCancellation(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ cancel()
+
+ fs := &cancelAwareRemoveIO{}
+ loadFS := func(ctx context.Context) (iceio.IO, error) {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ return fs, nil
+ }
+
+ // A commit that failed because ctx was canceled must still remove the
+ // metadata file it wrote.
+ const location =
"s3://bucket/db/tbl/metadata/00001-uncommitted.metadata.json"
+ require.NoError(t, removeUncommittedMetadata(ctx, location, loadFS))
+ assert.Equal(t, []string{location}, fs.removed)
+}
diff --git a/catalog/sql/sql.go b/catalog/sql/sql.go
index 6eaaef4db..c16ba6ae4 100644
--- a/catalog/sql/sql.go
+++ b/catalog/sql/sql.go
@@ -754,12 +754,15 @@ func checkValidNamespace(ident table.Identifier) error {
}
func removeUncommittedMetadata(ctx context.Context, metadataLocation string,
loadFS table.FSysF) error {
- fs, err := loadFS(context.WithoutCancel(ctx))
+ // This runs after a failed commit, which may have failed because ctx
was
+ // canceled, so neither the load nor the removal inherits that
cancellation.
+ cleanupCtx := context.WithoutCancel(ctx)
+ fs, err := loadFS(cleanupCtx)
if err != nil {
return fmt.Errorf("failed to load filesystem while removing
uncommitted metadata %s: %w", metadataLocation, err)
}
- if err := fs.Remove(metadataLocation); err != nil {
+ if err := fs.Remove(cleanupCtx, metadataLocation); err != nil {
return fmt.Errorf("failed to remove uncommitted metadata %s:
%w", metadataLocation, err)
}
@@ -1705,7 +1708,7 @@ func (c *Catalog) DropView(ctx context.Context,
identifier table.Identifier) err
return err
}
- _ = fs.Remove(metadataLocation)
+ _ = fs.Remove(ctx, metadataLocation)
}
return nil
diff --git a/catalog/sql/sql_integration_test.go
b/catalog/sql/sql_integration_test.go
index 9f8a27932..78ece6998 100644
--- a/catalog/sql/sql_integration_test.go
+++ b/catalog/sql/sql_integration_test.go
@@ -292,7 +292,7 @@ func (s *SQLIntegrationSuite) TestWriteCommitTable() {
s.Require().NoError(pqarrow.WriteTable(table, fw, table.NumRows(),
nil, pqarrow.DefaultWriterProps()))
defer func(fs io.IO, name string) {
- err = fs.Remove(name)
+ err = fs.Remove(s.ctx, name)
s.Require().NoError(err)
}(fs, pqfile)
diff --git a/cmd/iceberg/clean_orphan_files_test.go
b/cmd/iceberg/clean_orphan_files_test.go
index ea1aecc22..3393e4029 100644
--- a/cmd/iceberg/clean_orphan_files_test.go
+++ b/cmd/iceberg/clean_orphan_files_test.go
@@ -96,7 +96,7 @@ func TestRunCleanOrphanFilesPreviewsPlanBeforeDeletion(t
*testing.T) {
assert.False(t, deleted.DryRun)
assert.Equal(t, 1, deleted.OrphanFileCount)
assert.Equal(t, orphanPath, deleted.OrphanFiles[0].Path)
- _, err = memFS.Open(orphanPath)
+ _, err = memFS.Open(context.Background(), orphanPath)
assert.ErrorIs(t, err, fs.ErrNotExist)
}
diff --git a/internal/iomock/mock_fs.go b/internal/iomock/mock_fs.go
index 0abfbbb2e..40afbe2a4 100644
--- a/internal/iomock/mock_fs.go
+++ b/internal/iomock/mock_fs.go
@@ -19,6 +19,7 @@ package iomock
import (
"bytes"
+ "context"
"errors"
sio "io"
"io/fs"
@@ -31,7 +32,7 @@ type MockFS struct {
mock.Mock
}
-func (m *MockFS) Open(name string) (io.File, error) {
+func (m *MockFS) Open(_ context.Context, name string) (io.File, error) {
args := m.Called(name)
return args.Get(0).(io.File), args.Error(1)
@@ -47,7 +48,7 @@ func (m *MockFS) WriteFile(name string, content []byte) error
{
return m.Called(name, content).Error(0)
}
-func (m *MockFS) Remove(name string) error {
+func (m *MockFS) Remove(_ context.Context, name string) error {
return m.Called(name).Error(0)
}
diff --git a/io/gocloud/blobfs/blob.go b/io/gocloud/blobfs/blob.go
index e260db8b2..248469383 100644
--- a/io/gocloud/blobfs/blob.go
+++ b/io/gocloud/blobfs/blob.go
@@ -285,7 +285,9 @@ func directoryName(key string) string {
return pathpkg.Base(key)
}
-func (bfs *FileIO) Open(path string) (icebergio.File, error) {
+func (bfs *FileIO) Open(_ context.Context, path string) (icebergio.File,
error) {
+ // The returned File is read after Open returns, so its reads stay
bound to
+ // the FileIO's lifetime context (bfs.ctx) rather than this per-call
ctx.
originalPath := path
var err error
path, err = bfs.preprocess(path)
@@ -306,17 +308,17 @@ func (bfs *FileIO) Open(path string) (icebergio.File,
error) {
return &blobOpenFile{Reader: r, name: name, key: key, b: bfs, ctx:
bfs.ctx}, nil
}
-func (bfs *FileIO) Remove(name string) error {
+func (bfs *FileIO) Remove(ctx context.Context, name string) error {
key, err := bfs.preprocess(name)
if err != nil {
return &fs.PathError{Op: "remove", Path: name, Err: err}
}
- if err := bfs.Delete(bfs.ctx, key); err != nil {
+ if err := bfs.Delete(ctx, key); err != nil {
if gcerrors.Code(err) == gcerrors.NotFound {
marker := directoryMarker(key)
if marker != "" {
- if markerErr := bfs.Delete(bfs.ctx, marker);
markerErr == nil {
+ if markerErr := bfs.Delete(ctx, marker);
markerErr == nil {
return nil
} else if gcerrors.Code(markerErr) !=
gcerrors.NotFound {
return &fs.PathError{Op: "remove",
Path: name, Err: markerErr}
diff --git a/io/gocloud/blobfs/blob_test.go b/io/gocloud/blobfs/blob_test.go
index 57228b271..ed826e05a 100644
--- a/io/gocloud/blobfs/blob_test.go
+++ b/io/gocloud/blobfs/blob_test.go
@@ -176,7 +176,7 @@ func TestBlobFileIOOpenMissingReturnsPathError(t
*testing.T) {
fileIO := testBlobFileIO(context.Background(), "my-bucket", bucket)
name := "s3://my-bucket/missing.parquet"
- _, err := fileIO.Open(name)
+ _, err := fileIO.Open(context.Background(), name)
require.ErrorIs(t, err, fs.ErrNotExist)
var pathErr *fs.PathError
@@ -193,7 +193,7 @@ func TestBlobFileIOOpenPreprocessErrorRetainsOriginalPath(t
*testing.T) {
fileIO := testBlobFileIO(context.Background(), "my-bucket", bucket)
name := "s3://other-bucket/file.parquet"
- _, err := fileIO.Open(name)
+ _, err := fileIO.Open(context.Background(), name)
require.Error(t, err)
var pathErr *fs.PathError
@@ -369,7 +369,7 @@ func TestReadAtResourceCleanup(t *testing.T) {
},
}
- file, err := bfs.Open("test-file")
+ file, err := bfs.Open(context.Background(), "test-file")
require.NoError(t, err)
defer file.Close()
@@ -607,7 +607,7 @@ func TestBlobFileIORawQueryFragmentRoundTrip(t *testing.T) {
content := []byte("data")
require.NoError(t, bfs.WriteFile(location, content))
- file, err := bfs.Open(location)
+ file, err := bfs.Open(context.Background(), location)
require.NoError(t, err)
defer file.Close()
@@ -676,7 +676,7 @@ func TestBlobFileIORemoveMissingFileReturnsNotExist(t
*testing.T) {
extractor := DefaultObjectLocationExtractor("test-bucket")
bfs := New(ctx, bucket, extractor)
- err := bfs.Remove("s3://test-bucket/data/nonexistent.parquet")
+ err := bfs.Remove(context.Background(),
"s3://test-bucket/data/nonexistent.parquet")
require.ErrorIs(t, err, fs.ErrNotExist)
}
@@ -1017,7 +1017,7 @@ func TestBlobFileIOPreprocessErrorRetainsOriginalPath(t
*testing.T) {
op string
run func() error
}{
- {"remove", func() error { return fileIO.Remove(name) }},
+ {"remove", func() error { return
fileIO.Remove(context.Background(), name) }},
{"write file", func() error { return fileIO.WriteFile(name,
nil) }},
{"new writer", func() error {
_, err := fileIO.NewWriter(context.Background(), name,
true, nil)
@@ -1043,7 +1043,7 @@ func TestBlobFileIORemoveFallsBackToDirectoryMarker(t
*testing.T) {
require.NoError(t, bucket.WriteAll(ctx, "ns/tbl/", nil, nil))
fileIO := testBlobFileIO(ctx, "test-bucket", bucket)
- require.NoError(t, fileIO.Remove("s3://test-bucket/ns/tbl"))
+ require.NoError(t, fileIO.Remove(context.Background(),
"s3://test-bucket/ns/tbl"))
exists, err := bucket.Exists(ctx, "ns/tbl/")
require.NoError(t, err)
diff --git a/io/io.go b/io/io.go
index a788a6954..1e45cb80f 100644
--- a/io/io.go
+++ b/io/io.go
@@ -47,6 +47,10 @@ import (
type IO interface {
// Open opens the named file.
//
+ // Implementations may use ctx to bound the open; those backed by a
+ // plain io/fs.FS ignore it since fs.FS.Open takes no context. Reads on
+ // the returned File are not bound by ctx.
+ //
// When Open returns an error, it should be of type *PathError
// with the Op field set to "open", the Path field set to name,
// and the Err field describing the problem.
@@ -54,13 +58,14 @@ type IO interface {
// Open should reject attempts to open names that do not satisfy
// fs.ValidPath(name), returning a *PathError with Err set to
// ErrInvalid or ErrNotExist.
- Open(name string) (File, error)
+ Open(ctx context.Context, name string) (File, error)
- // Remove removes the named file or (empty) directory.
+ // Remove removes the named file or (empty) directory. The context
bounds
+ // the delete; local filesystem implementations ignore it.
//
// If there is an error, it will be of type *PathError.
// Implementations must be safe for concurrent use by multiple
goroutines.
- Remove(name string) error
+ Remove(ctx context.Context, name string) error
}
// ReadFileIO is the interface implemented by a file system that
@@ -256,7 +261,9 @@ type ioFS struct {
preProcessName func(string) string
}
-func (f ioFS) Open(name string) (File, error) {
+func (f ioFS) Open(_ context.Context, name string) (File, error) {
+ // The wrapped io/fs.FS has no context-aware Open, so ctx cannot be
+ // forwarded; it only bounds context-aware implementations.
name = f.processName(name)
file, err := f.fsys.Open(name)
if err != nil {
@@ -280,7 +287,15 @@ func (f ioFS) processName(name string) string {
return name
}
-func (f ioFS) Remove(name string) error {
+func (f ioFS) Remove(ctx context.Context, name string) error {
+ // Prefer a context-aware Remove when the wrapped fsys offers one, and
fall
+ // back to the plain io/fs-style Remove otherwise.
+ if r, ok := f.fsys.(interface {
+ Remove(context.Context, string) error
+ }); ok {
+ return r.Remove(ctx, f.processName(name))
+ }
+
r, ok := f.fsys.(interface{ Remove(name string) error })
if !ok {
return errMissingRemove
diff --git a/io/io_test.go b/io/io_test.go
index d473789c7..3d870dd96 100644
--- a/io/io_test.go
+++ b/io/io_test.go
@@ -18,6 +18,7 @@
package io_test
import (
+ "context"
"io/fs"
"testing"
"testing/fstest"
@@ -59,12 +60,12 @@ func TestFSPreProcNameTransformsAllOperations(t *testing.T)
{
return "/rewritten/file.txt"
})
- file, err := fsys.Open("file://bucket/original.txt")
+ file, err := fsys.Open(context.Background(),
"file://bucket/original.txt")
require.NoError(t, err)
require.NoError(t, file.Close())
_, err = fsys.(iceio.ReadFileIO).ReadFile("file://bucket/original.txt")
require.NoError(t, err)
- require.NoError(t, fsys.Remove("file://bucket/original.txt"))
+ require.NoError(t, fsys.Remove(context.Background(),
"file://bucket/original.txt"))
require.Equal(t, "rewritten/file.txt", underlying.opened)
require.Equal(t, underlying.opened, underlying.read)
diff --git a/io/local.go b/io/local.go
index f6329ae61..d129487d1 100644
--- a/io/local.go
+++ b/io/local.go
@@ -18,6 +18,7 @@
package io
import (
+ "context"
"fmt"
"io/fs"
"os"
@@ -68,7 +69,7 @@ func localPath(name string) (string, error) {
return filepath.FromSlash(path), nil
}
-func (LocalFS) Open(name string) (File, error) {
+func (LocalFS) Open(_ context.Context, name string) (File, error) {
path, err := localPath(name)
if err != nil {
return nil, err
@@ -110,7 +111,7 @@ func (LocalFS) WriteFile(name string, content []byte) error
{
return os.WriteFile(filename, content, 0o644)
}
-func (LocalFS) Remove(name string) error {
+func (LocalFS) Remove(_ context.Context, name string) error {
path, err := localPath(name)
if err != nil {
return err
diff --git a/io/local_test.go b/io/local_test.go
index addf2225e..23b47abfb 100644
--- a/io/local_test.go
+++ b/io/local_test.go
@@ -18,6 +18,7 @@
package io
import (
+ "context"
"io/fs"
"net/url"
"os"
@@ -163,7 +164,7 @@ func TestLocalFSParsesEscapedFileURIsConsistently(t
*testing.T) {
func TestLocalFSRejectsUnsupportedFileURIAuthority(t *testing.T) {
t.Parallel()
- _, err := (LocalFS{}).Open("file://remotehost/tmp/metadata.json")
+ _, err := (LocalFS{}).Open(context.Background(),
"file://remotehost/tmp/metadata.json")
require.ErrorContains(t, err, `unsupported file URI authority
"remotehost"`)
}
diff --git a/io/mem.go b/io/mem.go
index e7244fb40..ecc5541d7 100644
--- a/io/mem.go
+++ b/io/mem.go
@@ -66,7 +66,7 @@ func NewMemFS() *MemFS {
return &MemFS{files: make(map[string][]byte)}
}
-func (m *MemFS) Open(name string) (File, error) {
+func (m *MemFS) Open(_ context.Context, name string) (File, error) {
m.mu.RLock()
data, ok := m.files[name]
m.mu.RUnlock()
@@ -79,7 +79,7 @@ func (m *MemFS) Open(name string) (File, error) {
return &memFile{data: cp, name: path.Base(name)}, nil
}
-func (m *MemFS) Remove(name string) error {
+func (m *MemFS) Remove(_ context.Context, name string) error {
m.mu.Lock()
defer m.mu.Unlock()
diff --git a/io/mem_test.go b/io/mem_test.go
index cdc62dba5..4d6d21939 100644
--- a/io/mem_test.go
+++ b/io/mem_test.go
@@ -43,7 +43,7 @@ func TestMemIO_BasicOperations(t *testing.T) {
err = writeIO.WriteFile("test-file.txt", testData)
require.NoError(t, err)
- file, err := memIO.Open("test-file.txt")
+ file, err := memIO.Open(context.Background(), "test-file.txt")
require.NoError(t, err)
defer file.Close()
@@ -51,10 +51,10 @@ func TestMemIO_BasicOperations(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, testData, content)
- err = memIO.Remove("test-file.txt")
+ err = memIO.Remove(context.Background(), "test-file.txt")
require.NoError(t, err)
- _, err = memIO.Open("test-file.txt")
+ _, err = memIO.Open(context.Background(), "test-file.txt")
assert.Error(t, err)
}
@@ -78,7 +78,7 @@ func TestMemIO_Create(t *testing.T) {
err = writer.Close()
require.NoError(t, err)
- file, err := memIO.Open("created-file.txt")
+ file, err := memIO.Open(context.Background(), "created-file.txt")
require.NoError(t, err)
defer file.Close()
@@ -107,7 +107,7 @@ func TestMemIO_MultipleFiles(t *testing.T) {
}
for name, expectedContent := range files {
- file, err := memIO.Open(name)
+ file, err := memIO.Open(context.Background(), name)
require.NoError(t, err)
content, err := io.ReadAll(file)
@@ -118,17 +118,17 @@ func TestMemIO_MultipleFiles(t *testing.T) {
require.NoError(t, err)
}
- err = memIO.Remove("file2.txt")
+ err = memIO.Remove(context.Background(), "file2.txt")
require.NoError(t, err)
- _, err = memIO.Open("file2.txt")
+ _, err = memIO.Open(context.Background(), "file2.txt")
assert.Error(t, err)
- file1, err := memIO.Open("file1.txt")
+ file1, err := memIO.Open(context.Background(), "file1.txt")
require.NoError(t, err)
file1.Close()
- file3, err := memIO.Open("file3.txt")
+ file3, err := memIO.Open(context.Background(), "file3.txt")
require.NoError(t, err)
file3.Close()
}
@@ -138,14 +138,14 @@ func TestMemIO_RemoveMissingReturnsNotExist(t *testing.T)
{
memIO, err := icebergio.LoadFS(ctx, map[string]string{},
"mem://bucket/")
require.NoError(t, err)
- require.ErrorIs(t, memIO.Remove("does-not-exist.txt"), fs.ErrNotExist)
+ require.ErrorIs(t, memIO.Remove(context.Background(),
"does-not-exist.txt"), fs.ErrNotExist)
}
func TestMemIO_FileRejectsInvalidOffsets(t *testing.T) {
memIO := icebergio.NewMemFS()
require.NoError(t, memIO.WriteFile("file.txt", []byte("abc")))
- file, err := memIO.Open("file.txt")
+ file, err := memIO.Open(context.Background(), "file.txt")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, file.Close()) })
@@ -242,7 +242,7 @@ func TestMemIO_WalkDirCallbackCanRemoveFiles(t *testing.T) {
return nil
}
- return memIO.Remove(path)
+ return memIO.Remove(context.Background(), path)
})
}()
@@ -253,9 +253,9 @@ func TestMemIO_WalkDirCallbackCanRemoveFiles(t *testing.T) {
t.Fatal("WalkDir deadlocked when its callback removed a file")
}
- _, err := memIO.Open("mem://bucket/root/1.txt")
+ _, err := memIO.Open(context.Background(), "mem://bucket/root/1.txt")
require.ErrorIs(t, err, fs.ErrNotExist)
- _, err = memIO.Open("mem://bucket/root/2.txt")
+ _, err = memIO.Open(context.Background(), "mem://bucket/root/2.txt")
require.ErrorIs(t, err, fs.ErrNotExist)
}
diff --git a/manifest_projection.go b/manifest_projection.go
index c49bdeeff..e0cb8f17a 100644
--- a/manifest_projection.go
+++ b/manifest_projection.go
@@ -18,6 +18,7 @@
package iceberg
import (
+ "context"
"errors"
"fmt"
"iter"
@@ -83,7 +84,8 @@ func manifestEntries(
projection *ManifestEntryProjection,
) iter.Seq2[ManifestEntry, error] {
return func(yield func(ManifestEntry, error) bool) {
- f, err := fs.Open(m.FilePath())
+ // No caller context to thread here.
+ f, err := fs.Open(context.Background(), m.FilePath())
if err != nil {
yield(nil, err)
diff --git a/table/all_manifests_internal_test.go
b/table/all_manifests_internal_test.go
index 5bda050ca..b413cf270 100644
--- a/table/all_manifests_internal_test.go
+++ b/table/all_manifests_internal_test.go
@@ -134,8 +134,8 @@ type manifestTrackingIO struct {
delay time.Duration
}
-func (fs *manifestTrackingIO) Open(name string) (iceio.File, error) {
- f, err := fs.IO.Open(name)
+func (fs *manifestTrackingIO) Open(_ context.Context, name string)
(iceio.File, error) {
+ f, err := fs.IO.Open(context.Background(), name)
if err != nil {
return nil, err
}
diff --git a/table/arrow_scanner_bench_test.go
b/table/arrow_scanner_bench_test.go
index e5aec94bc..f0cdeff7b 100644
--- a/table/arrow_scanner_bench_test.go
+++ b/table/arrow_scanner_bench_test.go
@@ -625,7 +625,7 @@ func BenchmarkArrowScanTaskResidual(b *testing.B) {
if err := writer.Close(); err != nil {
b.Fatal(err)
}
- file, err := fs.Open(dataPath)
+ file, err := fs.Open(context.Background(), dataPath)
if err != nil {
b.Fatal(err)
}
diff --git a/table/arrow_scanner_lazy_delete_regression_test.go
b/table/arrow_scanner_lazy_delete_regression_test.go
index 87ff58848..f4f0cd065 100644
--- a/table/arrow_scanner_lazy_delete_regression_test.go
+++ b/table/arrow_scanner_lazy_delete_regression_test.go
@@ -50,7 +50,7 @@ type countingOpenMemFS struct {
unblock <-chan struct{}
}
-func (f *countingOpenMemFS) Open(name string) (iceio.File, error) {
+func (f *countingOpenMemFS) Open(_ context.Context, name string) (iceio.File,
error) {
f.opens.Add(1)
if name == f.trackedPath {
f.trackedOpens.Add(1)
@@ -59,7 +59,7 @@ func (f *countingOpenMemFS) Open(name string) (iceio.File,
error) {
<-f.unblock
}
- return f.MemFS.Open(name)
+ return f.MemFS.Open(context.Background(), name)
}
func newLazyDataFile(t *testing.T, path string) iceberg.DataFile {
diff --git a/table/arrow_scanner_reorder_test.go
b/table/arrow_scanner_reorder_test.go
index 1cd5c90b9..907f7de75 100644
--- a/table/arrow_scanner_reorder_test.go
+++ b/table/arrow_scanner_reorder_test.go
@@ -63,7 +63,7 @@ func (g *reorderGateIO) arm(headPath string, closes chan<-
struct{}) {
g.closed = 0
}
-func (g *reorderGateIO) Open(name string) (iceio.File, error) {
+func (g *reorderGateIO) Open(_ context.Context, name string) (iceio.File,
error) {
g.mu.Lock()
headPath, gate := g.headPath, g.gate
g.mu.Unlock()
@@ -72,7 +72,7 @@ func (g *reorderGateIO) Open(name string) (iceio.File, error)
{
<-gate
}
- f, err := g.LocalFS.Open(name)
+ f, err := g.LocalFS.Open(context.Background(), name)
if err != nil {
return nil, err
}
diff --git a/table/arrow_scanner_test.go b/table/arrow_scanner_test.go
index c35e41d35..095e82bb7 100644
--- a/table/arrow_scanner_test.go
+++ b/table/arrow_scanner_test.go
@@ -50,9 +50,9 @@ type failAfterGoodCloseFS struct {
goodClosedOnce sync.Once
}
-func (f *failAfterGoodCloseFS) Open(name string) (iceio.File, error) {
+func (f *failAfterGoodCloseFS) Open(_ context.Context, name string)
(iceio.File, error) {
if name == f.goodPath {
- file, err := f.MemFS.Open(name)
+ file, err := f.MemFS.Open(context.Background(), name)
if err != nil {
return nil, err
}
@@ -79,7 +79,7 @@ func (f *failAfterGoodCloseFS) Open(name string) (iceio.File,
error) {
return nil, &iofs.PathError{Op: "open", Path: name, Err:
iofs.ErrNotExist}
}
- return f.MemFS.Open(name)
+ return f.MemFS.Open(context.Background(), name)
}
type closeSignalFile struct {
diff --git a/table/commit_retry_test.go b/table/commit_retry_test.go
index 415276606..b0ab5aa45 100644
--- a/table/commit_retry_test.go
+++ b/table/commit_retry_test.go
@@ -40,8 +40,13 @@ import (
// Used to verify that doCommit fails early when the FS cannot write.
type readOnlyIO struct{}
-func (readOnlyIO) Open(_ string) (iceio.File, error) { return nil,
errors.New("readOnlyIO: no files") }
-func (readOnlyIO) Remove(_ string) error { return
errors.New("readOnlyIO: read-only") }
+func (readOnlyIO) Open(_ context.Context, _ string) (iceio.File, error) {
+ return nil, errors.New("readOnlyIO: no files")
+}
+
+func (readOnlyIO) Remove(_ context.Context, _ string) error {
+ return errors.New("readOnlyIO: read-only")
+}
// sequentialCatalog returns a predetermined error per CommitTable attempt.
// If attempts exceed the len(errs) slice it returns nil (success).
@@ -725,6 +730,77 @@ func TestDoCommit_OrphanCleanedOnRetriesExhausted(t
*testing.T) {
"none of the retry attempts were accepted, so all
orphans are safe to delete")
}
+// cancelAwareRemoveIO wraps memIO and, like blobfs, rejects Remove on a
+// canceled context.
+type cancelAwareRemoveIO struct {
+ *memIO
+}
+
+func (c *cancelAwareRemoveIO) Remove(ctx context.Context, name string) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+
+ return c.memIO.Remove(ctx, name)
+}
+
+// cancelAfterCommitCatalog cancels the commit context once CommitTable has
+// been called cancelAfter times.
+type cancelAfterCommitCatalog struct {
+ *sequentialCatalog
+ cancelAfter int32
+ cancel context.CancelFunc
+}
+
+func (c *cancelAfterCommitCatalog) CommitTable(ctx context.Context, ident
Identifier, reqs []Requirement, updates []Update) (Metadata, string, error) {
+ meta, loc, err := c.sequentialCatalog.CommitTable(ctx, ident, reqs,
updates)
+ if c.attempts.Load() == c.cancelAfter {
+ c.cancel()
+ }
+
+ return meta, loc, err
+}
+
+// TestDoCommit_OrphanCleanedOnCancellation verifies that orphan cleanup does
+// not inherit the caller's cancellation: when doCommit exits because ctx was
+// canceled, an IO that honors ctx on Remove must still remove the orphans.
+func TestDoCommit_OrphanCleanedOnCancellation(t *testing.T) {
+ spec := iceberg.NewPartitionSpec()
+ mem, meta := newMemIOWithRetryMeta(t, spec)
+ wfs := &cancelAwareRemoveIO{memIO: mem}
+
+ tbl := newOCCTable(t, meta, wfs, nil)
+ txn := tbl.NewTransaction()
+ sp := newFastAppendFilesProducer(OpAppend, txn, wfs, nil, nil)
+ sp.appendDataFile(newTestDataFile(t, spec,
"mem://default/table-location/data/f.parquet", nil))
+
+ updates, reqs, err := sp.commit(context.Background())
+ require.NoError(t, err)
+ originalManifestList :=
updates[0].(*addSnapshotUpdate).Snapshot.ManifestList
+ require.Contains(t, mem.files, originalManifestList)
+
+ // The first failure triggers a rebuild that orphans the original
manifest
+ // list; the context is canceled during the second attempt, so doCommit
+ // exits on the cancellation with orphan cleanup still enabled.
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ cat := &cancelAfterCommitCatalog{
+ sequentialCatalog: &sequentialCatalog{
+ metadata: meta,
+ errs: []error{ErrCommitFailed, ErrCommitFailed},
+ },
+ cancelAfter: 2,
+ cancel: cancel,
+ }
+ tbl = newOCCTable(t, meta, wfs, cat)
+
+ _, err = tbl.doCommit(ctx, updates, reqs, withCommitBranch(MainBranch))
+ require.ErrorIs(t, err, context.Canceled)
+
+ assert.NotContains(t, mem.files, originalManifestList,
+ "orphaned manifest list must be removed even when the commit
ends on cancellation")
+}
+
// ---------------------------------------------------------------------------
// Fix 7 (R4) — retry-progression: every fix exercised end-to-end
// ---------------------------------------------------------------------------
diff --git a/table/conflict_validation.go b/table/conflict_validation.go
index c47eee777..6ca0af9ce 100644
--- a/table/conflict_validation.go
+++ b/table/conflict_validation.go
@@ -48,6 +48,7 @@ package table
import (
"bytes"
+ "context"
"encoding/hex"
"errors"
"fmt"
@@ -230,12 +231,12 @@ func newConflictManifestIO(base iceio.IO)
*conflictManifestIO {
}
}
-func (c *conflictManifestIO) Open(name string) (iceio.File, error) {
+func (c *conflictManifestIO) Open(ctx context.Context, name string)
(iceio.File, error) {
if data, ok := c.files[name]; ok {
return newConflictManifestFile(name, data), nil
}
- f, err := c.base.Open(name)
+ f, err := c.base.Open(ctx, name)
if err != nil {
return nil, err
}
@@ -264,7 +265,7 @@ func (c *conflictManifestIO) Open(name string) (iceio.File,
error) {
}, nil
}
-func (c *conflictManifestIO) Remove(string) error {
+func (c *conflictManifestIO) Remove(context.Context, string) error {
return errConflictManifestIOReadOnly
}
@@ -277,7 +278,7 @@ func (c *conflictManifestIO) Stat(name string)
(fs.FileInfo, error) {
return statIO.Stat(name)
}
- f, err := c.Open(name)
+ f, err := c.Open(context.Background(), name)
if err != nil {
return nil, err
}
diff --git a/table/conflict_validation_bench_test.go
b/table/conflict_validation_bench_test.go
index 9a994fb72..c0d66a7c3 100644
--- a/table/conflict_validation_bench_test.go
+++ b/table/conflict_validation_bench_test.go
@@ -19,6 +19,7 @@ package table
import (
"bytes"
+ "context"
"errors"
"fmt"
"testing"
@@ -151,10 +152,10 @@ type conflictValidationBenchmarkIO struct {
bytes int64
}
-func (f *conflictValidationBenchmarkIO) Open(name string) (iceio.File, error) {
+func (f *conflictValidationBenchmarkIO) Open(_ context.Context, name string)
(iceio.File, error) {
f.opens++
- file, err := f.IO.Open(name)
+ file, err := f.IO.Open(context.Background(), name)
if err != nil {
return nil, err
}
diff --git a/table/conflict_validation_test.go
b/table/conflict_validation_test.go
index 9a968a1f8..ee5a6a0ca 100644
--- a/table/conflict_validation_test.go
+++ b/table/conflict_validation_test.go
@@ -19,6 +19,7 @@ package table
import (
"bytes"
+ "context"
"errors"
"fmt"
"io/fs"
@@ -992,7 +993,7 @@ func TestConflictManifestIODoesNotCachePartialRead(t
*testing.T) {
tio.files[manifestPath] = []byte("abc")
cache :=
newConflictManifestIO(&conflictValidationStatIO{trackingCallsIO: tio})
- f, err := cache.Open(manifestPath)
+ f, err := cache.Open(context.Background(), manifestPath)
require.NoError(t, err)
buf := make([]byte, 1)
_, err = f.Read(buf)
@@ -1006,7 +1007,7 @@ func TestConflictManifestIOIsReadOnly(t *testing.T) {
tio := newTrackingCallsIO()
cache := newConflictManifestIO(tio)
- err := cache.Remove("manifest.avro")
+ err := cache.Remove(context.Background(), "manifest.avro")
require.Error(t, err)
assert.Zero(t, tio.removeCount["manifest.avro"])
}
@@ -1021,7 +1022,7 @@ func TestConflictManifestIOStopsCachingAtByteLimit(t
*testing.T) {
cachedBytes: maxConflictManifestCacheBytes - 1,
}
- f, err := cache.Open(manifestPath)
+ f, err := cache.Open(context.Background(), manifestPath)
require.NoError(t, err)
buf := make([]byte, 2)
_, err = f.Read(buf)
diff --git a/table/dv/deletion_vector.go b/table/dv/deletion_vector.go
index 206b91338..a03cd2db1 100644
--- a/table/dv/deletion_vector.go
+++ b/table/dv/deletion_vector.go
@@ -20,6 +20,7 @@ package dv
import (
"bytes"
"cmp"
+ "context"
"encoding/binary"
"errors"
"fmt"
@@ -427,7 +428,8 @@ func validateDVFile(dvFile iceberg.DataFile) error {
// the still-open file, so the caller can read bare blobs without a second
// Open round-trip; the caller owns closing f whenever err is nil.
func openDVReader(fs iceio.IO, filePath string) (*puffin.Reader, iceio.File,
error) {
- f, err := fs.Open(filePath)
+ // No caller context to thread here.
+ f, err := fs.Open(context.Background(), filePath)
if err != nil {
return nil, nil, fmt.Errorf("open DV file %s: %w", filePath,
err)
}
diff --git a/table/dv/deletion_vector_test.go b/table/dv/deletion_vector_test.go
index 15239908a..d87384b74 100644
--- a/table/dv/deletion_vector_test.go
+++ b/table/dv/deletion_vector_test.go
@@ -19,6 +19,7 @@ package dv
import (
"bytes"
+ "context"
"encoding/binary"
"encoding/json"
"errors"
@@ -260,8 +261,8 @@ type failingOpenFS struct {
err error
}
-func (f failingOpenFS) Open(string) (iceio.File, error) { return nil, f.err }
-func (f failingOpenFS) Remove(string) error { return nil }
+func (f failingOpenFS) Open(context.Context, string) (iceio.File, error) {
return nil, f.err }
+func (f failingOpenFS) Remove(context.Context, string) error {
return nil }
type countingReadIO struct {
base iceio.IO
@@ -269,8 +270,8 @@ type countingReadIO struct {
readCalls []readAtCall
}
-func (f *countingReadIO) Open(name string) (iceio.File, error) {
- file, err := f.base.Open(name)
+func (f *countingReadIO) Open(_ context.Context, name string) (iceio.File,
error) {
+ file, err := f.base.Open(context.Background(), name)
if err != nil {
return nil, err
}
@@ -278,8 +279,8 @@ func (f *countingReadIO) Open(name string) (iceio.File,
error) {
return &countingReadFile{File: file, reads: &f.reads, readCalls:
&f.readCalls}, nil
}
-func (f *countingReadIO) Remove(name string) error {
- return f.base.Remove(name)
+func (f *countingReadIO) Remove(_ context.Context, name string) error {
+ return f.base.Remove(context.Background(), name)
}
func (f *countingReadIO) reset() {
diff --git a/table/dv/dv_cross_client_test.go b/table/dv/dv_cross_client_test.go
index 3352dfb90..69d92ff4d 100644
--- a/table/dv/dv_cross_client_test.go
+++ b/table/dv/dv_cross_client_test.go
@@ -18,6 +18,7 @@
package dv
import (
+ "context"
"path/filepath"
"strconv"
"testing"
@@ -70,7 +71,7 @@ func TestCrossClientReadJavaMultiBlobDV(t *testing.T) {
require.NoError(t, err)
fs := iceio.LocalFS{}
- f, err := fs.Open(abs)
+ f, err := fs.Open(context.Background(), abs)
require.NoError(t, err)
t.Cleanup(func() { f.Close() })
@@ -210,7 +211,7 @@ func loadJavaPuffinBlob(t *testing.T, fixture string,
blobIdx int, referencedDat
require.NoError(t, err)
fs := iceio.LocalFS{}
- f, err := fs.Open(abs)
+ f, err := fs.Open(context.Background(), abs)
require.NoError(t, err)
defer f.Close()
diff --git a/table/dv/dv_writer_test.go b/table/dv/dv_writer_test.go
index dddf09589..8d8670570 100644
--- a/table/dv/dv_writer_test.go
+++ b/table/dv/dv_writer_test.go
@@ -520,7 +520,7 @@ func TestDVWriterFlushUnknownSpecID(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown partition spec id 99")
- _, openErr := fs.Open(location)
+ _, openErr := fs.Open(context.Background(), location)
assert.ErrorIs(t, openErr, stdfs.ErrNotExist)
}
diff --git a/table/dv_scanner_read_test.go b/table/dv_scanner_read_test.go
index ab94fba93..b1562435d 100644
--- a/table/dv_scanner_read_test.go
+++ b/table/dv_scanner_read_test.go
@@ -129,8 +129,8 @@ type countingDVOpenIO struct {
opens atomic.Int64
}
-func (c *countingDVOpenIO) Open(name string) (iceio.File, error) {
- f, err := c.LocalFS.Open(name)
+func (c *countingDVOpenIO) Open(_ context.Context, name string) (iceio.File,
error) {
+ f, err := c.LocalFS.Open(context.Background(), name)
if err == nil {
c.opens.Add(1)
}
diff --git a/table/empty_scan_task_regression_test.go
b/table/empty_scan_task_regression_test.go
index 87c48d90b..be3abe143 100644
--- a/table/empty_scan_task_regression_test.go
+++ b/table/empty_scan_task_regression_test.go
@@ -96,7 +96,7 @@ func TestScanSurvivesFullyPrunedTask(t *testing.T) {
require.NoError(t, pqarrow.WriteTable(arrTbl, fo,
arrTbl.NumRows(),
nil, pqarrow.DefaultWriterProps()))
- st, err := fs.Open(path)
+ st, err := fs.Open(context.Background(), path)
require.NoError(t, err)
defer st.Close()
info, err := st.Stat()
diff --git a/table/equality_delete_reader_bench_test.go
b/table/equality_delete_reader_bench_test.go
index 4e55edc5d..52d05afb1 100644
--- a/table/equality_delete_reader_bench_test.go
+++ b/table/equality_delete_reader_bench_test.go
@@ -296,7 +296,7 @@ func newEqualityDeleteLoadingBenchmarkInput(b *testing.B)
equalityDeleteLoadingB
firstPath := "mem://benchmark-lazy-equality/delete-0000.parquet"
writeEqualityDeleteParquetToMemFS(b, fs.MemFS, firstPath,
`[{"id": 0}, {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id":
5}, {"id": 6}, {"id": 7}, {"id": 8}, {"id": 9}]`)
- file, err := fs.MemFS.Open(firstPath)
+ file, err := fs.MemFS.Open(context.Background(), firstPath)
if err != nil {
b.Fatal(err)
}
diff --git a/table/equality_delete_reader_internal_test.go
b/table/equality_delete_reader_internal_test.go
index 3870bba85..fe458453e 100644
--- a/table/equality_delete_reader_internal_test.go
+++ b/table/equality_delete_reader_internal_test.go
@@ -70,9 +70,9 @@ func (f *countingEqualityFieldDataFile)
DataFileCollectionsRef(_ iceinternal.Dat
return iceinternal.BorrowedDataFileCollections(f.DataFile)
}
-func (f *countingEqualityDeleteOpenFS) Open(name string) (iceio.File, error) {
+func (f *countingEqualityDeleteOpenFS) Open(_ context.Context, name string)
(iceio.File, error) {
f.attempts.Add(1)
- file, err := f.MemFS.Open(name)
+ file, err := f.MemFS.Open(context.Background(), name)
if err == nil {
f.opens.Add(1)
}
diff --git a/table/equality_delete_writer_test.go
b/table/equality_delete_writer_test.go
index cb0537944..c9a3df6ce 100644
--- a/table/equality_delete_writer_test.go
+++ b/table/equality_delete_writer_test.go
@@ -99,7 +99,7 @@ func TestWriteEqualityDeleteFiles(t *testing.T) {
// Verify the file was actually written to disk
fs := iceio.LocalFS{}
- f, err := fs.Open(df.FilePath())
+ f, err := fs.Open(context.Background(), df.FilePath())
require.NoError(t, err)
require.NoError(t, f.Close())
}
diff --git a/table/geo_write_test.go b/table/geo_write_test.go
index 804ff6738..ae0f90ac6 100644
--- a/table/geo_write_test.go
+++ b/table/geo_write_test.go
@@ -388,7 +388,7 @@ func TestWriteGeoColumnMultiRowGroupStats(t *testing.T) {
func sumColumnChunkCompressedSize(t *testing.T, path, colName string) int64 {
t.Helper()
- f, err := (io.LocalFS{}).Open(path)
+ f, err := (io.LocalFS{}).Open(t.Context(), path)
require.NoError(t, err)
defer f.Close()
diff --git a/table/incremental_append_scan_test.go
b/table/incremental_append_scan_test.go
index a6ace522b..fb83a00ec 100644
--- a/table/incremental_append_scan_test.go
+++ b/table/incremental_append_scan_test.go
@@ -375,7 +375,7 @@ func incrementalAppendTestTable(t *testing.T) *Table {
}
manifestList1Path := "mem://default/table-location/metadata/snap-1.avro"
writeManifestList(1, manifestList1Path,
[]iceberg.ManifestFile{manifest1})
- listFile, err := fs.Open(manifestList1Path)
+ listFile, err := fs.Open(context.Background(), manifestList1Path)
require.NoError(t, err)
manifestList1, err := iceberg.ReadManifestList(listFile)
require.NoError(t, err)
@@ -443,7 +443,7 @@ func incrementalAppendMixedOperationTable(t *testing.T)
*Table {
require.NoError(t, iceberg.WriteManifestList(2, &listBuf,
snapshotID, nil,
&sequenceNumber, 0, manifests))
require.NoError(t, fs.WriteFile(path, listBuf.Bytes()))
- listFile, err := fs.Open(path)
+ listFile, err := fs.Open(context.Background(), path)
require.NoError(t, err)
manifestList, err := iceberg.ReadManifestList(listFile)
require.NoError(t, err)
@@ -581,7 +581,7 @@ func incrementalAppendExpiredExclusiveTable(t *testing.T)
*Table {
}
manifestListBPath := "mem://default/table-location/metadata/snap-b.avro"
writeManifestList(2, manifestListBPath,
[]iceberg.ManifestFile{manifestB})
- listFile, err := fs.Open(manifestListBPath)
+ listFile, err := fs.Open(context.Background(), manifestListBPath)
require.NoError(t, err)
manifestListB, err := iceberg.ReadManifestList(listFile)
require.NoError(t, err)
diff --git a/table/incremental_changelog_scan_test.go
b/table/incremental_changelog_scan_test.go
index f94e67f09..ba530acd7 100644
--- a/table/incremental_changelog_scan_test.go
+++ b/table/incremental_changelog_scan_test.go
@@ -511,7 +511,7 @@ func incrementalChangelogTestTable(t *testing.T) *Table {
&sequenceNumber, 0, manifests))
require.NoError(t, fs.WriteFile(path, buf.Bytes()))
- listFile, err := fs.Open(path)
+ listFile, err := fs.Open(context.Background(), path)
require.NoError(t, err)
list, err := iceberg.ReadManifestList(listFile)
require.NoError(t, err)
@@ -611,7 +611,7 @@ func incrementalChangelogManifestRewriteTable(t *testing.T)
*Table {
&sequenceNumber, 0, manifests))
require.NoError(t, fs.WriteFile(path, buf.Bytes()))
- listFile, err := fs.Open(path)
+ listFile, err := fs.Open(context.Background(), path)
require.NoError(t, err)
list, err := iceberg.ReadManifestList(listFile)
require.NoError(t, err)
@@ -711,7 +711,7 @@ func incrementalChangelogDeleteManifestTable(t *testing.T)
*Table {
&sequenceNumber, 0, manifests))
require.NoError(t, fs.WriteFile(path, buf.Bytes()))
- listFile, err := fs.Open(path)
+ listFile, err := fs.Open(context.Background(), path)
require.NoError(t, err)
list, err := iceberg.ReadManifestList(listFile)
require.NoError(t, err)
@@ -774,11 +774,11 @@ type countingOpenIO struct {
afterOpen func()
}
-func (io *countingOpenIO) Open(name string) (iceio.File, error) {
+func (io *countingOpenIO) Open(_ context.Context, name string) (iceio.File,
error) {
io.opens.Add(1)
if io.afterOpen != nil {
io.afterOpen()
}
- return io.IO.Open(name)
+ return io.IO.Open(context.Background(), name)
}
diff --git a/table/inspect_internal_test.go b/table/inspect_internal_test.go
index 893144e7f..ef1328e0b 100644
--- a/table/inspect_internal_test.go
+++ b/table/inspect_internal_test.go
@@ -2202,12 +2202,12 @@ type inspectManifestListContextIO struct {
rejectManifestList bool
}
-func (fs inspectManifestListContextIO) Open(path string) (iceio.File, error) {
+func (fs inspectManifestListContextIO) Open(_ context.Context, path string)
(iceio.File, error) {
if fs.rejectManifestList && strings.Contains(path, "manifest-list") {
return nil, errors.New("manifest list opened with uncancellable
IO")
}
- return fs.IO.Open(path)
+ return fs.IO.Open(context.Background(), path)
}
func TestInspectFilesKeepCallerContextForManifestListRead(t *testing.T) {
diff --git a/table/inspect_partitions_parallel_test.go
b/table/inspect_partitions_parallel_test.go
index d03e9c6c6..9802c1273 100644
--- a/table/inspect_partitions_parallel_test.go
+++ b/table/inspect_partitions_parallel_test.go
@@ -38,7 +38,7 @@ type inspectPartitionManifestTrackingIO struct {
failPath string
}
-func (f *inspectPartitionManifestTrackingIO) Open(path string) (iceio.File,
error) {
+func (f *inspectPartitionManifestTrackingIO) Open(ctx context.Context, path
string) (iceio.File, error) {
if strings.Contains(path, "/metadata/inspect-") &&
strings.HasSuffix(path, ".avro") {
if f.failPath != "" && strings.Contains(path, f.failPath) {
return nil, fmt.Errorf("injected manifest read error
for %s", path)
@@ -57,7 +57,7 @@ func (f *inspectPartitionManifestTrackingIO) Open(path
string) (iceio.File, erro
f.active.Add(-1)
}
- return f.IO.Open(path)
+ return f.IO.Open(ctx, path)
}
func inspectParallelPartitionManifestTable(t *testing.T, count int) *Table {
diff --git a/table/internal/parquet_files.go b/table/internal/parquet_files.go
index eb714a07d..7238b6347 100644
--- a/table/internal/parquet_files.go
+++ b/table/internal/parquet_files.go
@@ -105,7 +105,7 @@ const (
type parquetFormat struct{}
func (parquetFormat) Open(ctx context.Context, fs iceio.IO, path string) (_
FileReader, err error) {
- inputfile, err := fs.Open(path)
+ inputfile, err := fs.Open(ctx, path)
if err != nil {
return nil, err
}
@@ -1308,7 +1308,8 @@ func singleReferencedDataFile(stats *DataFileStatistics)
*string {
func (w *ParquetFileWriter) Abort() error {
closeErr := w.fileCloser.Close()
- removeErr := w.fs.Remove(w.info.FileName)
+ // Abort is a best-effort cleanup with no caller context to thread.
+ removeErr := w.fs.Remove(context.Background(), w.info.FileName)
if errors.Is(removeErr, fs.ErrNotExist) || os.IsNotExist(removeErr) {
removeErr = nil
}
@@ -2698,7 +2699,7 @@ func checkRowGroupBloomFilters(
}
func (pfs *ParquetFileSource) GetReader(ctx context.Context) (result
FileReader, err error) {
- pf, err := pfs.fs.Open(pfs.file.FilePath())
+ pf, err := pfs.fs.Open(ctx, pfs.file.FilePath())
if err != nil {
return nil, err
}
diff --git a/table/internal/parquet_files_test.go
b/table/internal/parquet_files_test.go
index 2b477cdaa..c4dc92bf3 100644
--- a/table/internal/parquet_files_test.go
+++ b/table/internal/parquet_files_test.go
@@ -102,11 +102,11 @@ type trackingFileSystem struct {
file *trackingOpenFile
}
-func (f *trackingFileSystem) Open(name string) (iceio.File, error) {
+func (f *trackingFileSystem) Open(_ context.Context, name string) (iceio.File,
error) {
return f.file, nil
}
-func (f *trackingFileSystem) Remove(name string) error {
+func (f *trackingFileSystem) Remove(_ context.Context, name string) error {
return nil
}
@@ -1249,7 +1249,7 @@ func TestWriteDataFileNormalizesEWKBOnDisk(t *testing.T) {
}, []arrow.RecordBatch{record})
require.NoError(t, err)
- output, err := fs.Open("geo.parquet")
+ output, err := fs.Open(context.Background(), "geo.parquet")
require.NoError(t, err)
data, err := io.ReadAll(output)
closeErr := output.Close()
@@ -1400,7 +1400,7 @@ func TestParquetFileWriterAbortRemovesFile(t *testing.T) {
require.NoError(t, writer.Abort())
- _, err := memFS.Open(fileName)
+ _, err := memFS.Open(context.Background(), fileName)
require.ErrorIs(t, err, iofs.ErrNotExist)
}
@@ -2004,7 +2004,7 @@ func TestParquetRowGroupTargetRotatesBeforeNextBatch(t
*testing.T) {
}, []arrow.RecordBatch{first, second})
require.NoError(t, err)
- input, err := fsys.Open("row-groups.parquet")
+ input, err := fsys.Open(context.Background(),
"row-groups.parquet")
require.NoError(t, err)
defer input.Close()
diff --git a/table/lazy_deletion_vector_bench_test.go
b/table/lazy_deletion_vector_bench_test.go
index 43f70225d..ccdb11437 100644
--- a/table/lazy_deletion_vector_bench_test.go
+++ b/table/lazy_deletion_vector_bench_test.go
@@ -46,8 +46,8 @@ type countingLazyDVOpenIO struct {
opens atomic.Int64
}
-func (f *countingLazyDVOpenIO) Open(name string) (iceio.File, error) {
- file, err := f.LocalFS.Open(name)
+func (f *countingLazyDVOpenIO) Open(_ context.Context, name string)
(iceio.File, error) {
+ file, err := f.LocalFS.Open(context.Background(), name)
if err == nil {
f.opens.Add(1)
}
diff --git a/table/lazy_deletion_vector_test.go
b/table/lazy_deletion_vector_test.go
index 34aaff2c8..6179822fd 100644
--- a/table/lazy_deletion_vector_test.go
+++ b/table/lazy_deletion_vector_test.go
@@ -130,13 +130,13 @@ type failingLazyDeletionVectorIO struct {
err error
}
-func (f *failingLazyDeletionVectorIO) Open(string) (iceio.File, error) {
+func (f *failingLazyDeletionVectorIO) Open(context.Context, string)
(iceio.File, error) {
f.opens.Add(1)
return nil, f.err
}
-func (f *failingLazyDeletionVectorIO) Remove(string) error { return nil }
+func (f *failingLazyDeletionVectorIO) Remove(context.Context, string) error {
return nil }
func TestLazyDeletionVectorLoaderCachesGroupErrors(t *testing.T) {
const dataFilePath = "file:///table/data/missing.parquet"
@@ -173,8 +173,8 @@ type cancelOnOpenIO struct {
opens atomic.Int64
}
-func (f *cancelOnOpenIO) Open(name string) (iceio.File, error) {
- file, err := f.LocalFS.Open(name)
+func (f *cancelOnOpenIO) Open(_ context.Context, name string) (iceio.File,
error) {
+ file, err := f.LocalFS.Open(context.Background(), name)
if err == nil {
f.opens.Add(1)
f.cancel()
@@ -224,8 +224,8 @@ type countingPuffinOpenIO struct {
opens atomic.Int64
}
-func (f *countingPuffinOpenIO) Open(name string) (iceio.File, error) {
- file, err := f.base.Open(name)
+func (f *countingPuffinOpenIO) Open(_ context.Context, name string)
(iceio.File, error) {
+ file, err := f.base.Open(context.Background(), name)
if err == nil && strings.HasSuffix(name, ".puffin") {
f.opens.Add(1)
}
@@ -233,7 +233,9 @@ func (f *countingPuffinOpenIO) Open(name string)
(iceio.File, error) {
return file, err
}
-func (f *countingPuffinOpenIO) Remove(name string) error { return
f.base.Remove(name) }
+func (f *countingPuffinOpenIO) Remove(_ context.Context, name string) error {
+ return f.base.Remove(context.Background(), name)
+}
func TestReadTasksDoesNotLoadDeletionVectorsBeforeIteration(t *testing.T) {
baseFS := iceio.LocalFS{}
diff --git a/table/maintenance/manifest_test_helpers_test.go
b/table/maintenance/manifest_test_helpers_test.go
index 86ca1fc1a..4c02d9fca 100644
--- a/table/maintenance/manifest_test_helpers_test.go
+++ b/table/maintenance/manifest_test_helpers_test.go
@@ -19,6 +19,7 @@ package maintenance
import (
"bytes"
+ "context"
"fmt"
stdfs "io/fs"
"sync"
@@ -54,7 +55,7 @@ func newTrackingIO() *trackingIO {
return &trackingIO{files: make(map[string][]byte)}
}
-func (t *trackingIO) Open(name string) (iceio.File, error) {
+func (t *trackingIO) Open(_ context.Context, name string) (iceio.File, error) {
data, ok := t.files[name]
if !ok {
return nil, stdfs.ErrNotExist
@@ -69,7 +70,7 @@ func (t *trackingIO) WriteFile(name string, content []byte)
error {
return nil
}
-func (t *trackingIO) Remove(name string) error {
+func (t *trackingIO) Remove(_ context.Context, name string) error {
delete(t.files, name)
return nil
@@ -91,20 +92,20 @@ func newTrackingCallsIO() *trackingCallsIO {
}
}
-func (c *trackingCallsIO) Open(name string) (iceio.File, error) {
+func (c *trackingCallsIO) Open(ctx context.Context, name string) (iceio.File,
error) {
c.mu.Lock()
c.openCount[name]++
c.mu.Unlock()
- return c.trackingIO.Open(name)
+ return c.trackingIO.Open(ctx, name)
}
-func (c *trackingCallsIO) Remove(name string) error {
+func (c *trackingCallsIO) Remove(ctx context.Context, name string) error {
c.mu.Lock()
c.removeCount[name]++
c.mu.Unlock()
- return c.trackingIO.Remove(name)
+ return c.trackingIO.Remove(ctx, name)
}
// writeManifest writes a v2 data manifest with a single ADDED
@@ -192,8 +193,8 @@ type manifestTrackingIO struct {
delay time.Duration
}
-func (fs *manifestTrackingIO) Open(name string) (iceio.File, error) {
- f, err := fs.IO.Open(name)
+func (fs *manifestTrackingIO) Open(ctx context.Context, name string)
(iceio.File, error) {
+ f, err := fs.IO.Open(ctx, name)
if err != nil {
return nil, err
}
@@ -232,8 +233,8 @@ type benchmarkDelayIO struct {
delay time.Duration
}
-func (fs *benchmarkDelayIO) Open(name string) (iceio.File, error) {
- f, err := fs.IO.Open(name)
+func (fs *benchmarkDelayIO) Open(ctx context.Context, name string)
(iceio.File, error) {
+ f, err := fs.IO.Open(ctx, name)
if err != nil {
return nil, err
}
diff --git a/table/maintenance/orphan_cleanup.go
b/table/maintenance/orphan_cleanup.go
index e56c58219..faf2039ca 100644
--- a/table/maintenance/orphan_cleanup.go
+++ b/table/maintenance/orphan_cleanup.go
@@ -87,7 +87,7 @@ type orphanCleanupConfig struct {
location string
olderThan time.Duration
dryRun bool
- deleteFunc func(string) error
+ deleteFunc func(context.Context, string) error
maxConcurrency int
prefixMismatchMode PrefixMismatchMode
equalSchemes map[string]string
@@ -135,7 +135,7 @@ func WithCleanupDryRun(enabled bool) OrphanCleanupOption {
// WithCleanupDeleteFunc sets a custom delete function. If not provided, the
table's FileIO
// delete method will be used.
-func WithCleanupDeleteFunc(deleteFunc func(string) error) OrphanCleanupOption {
+func WithCleanupDeleteFunc(deleteFunc func(context.Context, string) error)
OrphanCleanupOption {
return func(cfg *orphanCleanupConfig) {
cfg.deleteFunc = deleteFunc
}
@@ -783,7 +783,7 @@ func deleteFilesSequential(ctx context.Context, fs io.IO,
orphanFiles []string,
break
}
- if err := deleteFunc(file); err != nil {
+ if err := deleteFunc(ctx, file); err != nil {
result = errors.Join(result, fmt.Errorf("failed to
delete orphan file %s: %w", file, err))
continue
@@ -799,7 +799,7 @@ func deleteFilesParallel(
ctx context.Context,
files []string,
maxConcurrency int,
- deleteFunc func(string) error,
+ deleteFunc func(context.Context, string) error,
wrapError func(string, error) error,
) ([]string, error) {
workers := min(max(maxConcurrency, 1), len(files))
@@ -838,7 +838,7 @@ func deleteFilesParallel(
return
}
- if err := deleteFunc(files[index]); err
!= nil {
+ if err := deleteFunc(ctx,
files[index]); err != nil {
wrappedErr :=
wrapError(files[index], err)
if wrappedErr == nil {
// Keep failed
deletions observable if the wrapper suppresses the error.
@@ -1432,8 +1432,8 @@ func PurgeFiles(ctx context.Context, tbl *table.Table)
error {
ctx,
files,
defaultPurgeMaxConcurrency,
- func(file string) error {
- if err := fs.Remove(file); err != nil
&& !os.IsNotExist(err) {
+ func(ctx context.Context, file string) error {
+ if err := fs.Remove(ctx, file); err !=
nil && !os.IsNotExist(err) {
return err
}
diff --git a/table/maintenance/orphan_cleanup_bench_test.go
b/table/maintenance/orphan_cleanup_bench_test.go
index 2f8aee49f..3ccd4ac31 100644
--- a/table/maintenance/orphan_cleanup_bench_test.go
+++ b/table/maintenance/orphan_cleanup_bench_test.go
@@ -124,7 +124,7 @@ func BenchmarkPurgeFilesNonBulkDeletion(b *testing.B) {
for _, delay := range []time.Duration{0, 100 *
time.Microsecond, time.Millisecond} {
for _, concurrency := range []int{1, 4, 16, 32, 64} {
b.Run(fmt.Sprintf("files=%d/delay=%s/concurrency=%d", fileCount, delay,
concurrency), func(b *testing.B) {
- deleteFunc := func(string) error {
+ deleteFunc := func(context.Context,
string) error {
time.Sleep(delay)
return nil
diff --git a/table/maintenance/orphan_cleanup_integration_test.go
b/table/maintenance/orphan_cleanup_integration_test.go
index 268adcdda..2c097e654 100644
--- a/table/maintenance/orphan_cleanup_integration_test.go
+++ b/table/maintenance/orphan_cleanup_integration_test.go
@@ -195,7 +195,7 @@ func (s *OrphanCleanupIntegrationSuite) mustFS(tbl
*table.Table) io.IO {
}
func (s *OrphanCleanupIntegrationSuite) fileExists(fs io.IO, path string) bool
{
- f, err := fs.Open(path)
+ f, err := fs.Open(s.ctx, path)
if err != nil {
return false
}
@@ -249,7 +249,7 @@ func (s *OrphanCleanupIntegrationSuite)
TestOrphanCleanupDryRun() {
}
for _, orphanFile := range orphanFiles {
- s.NoError(fs.Remove(orphanFile))
+ s.NoError(fs.Remove(s.ctx, orphanFile))
}
}
@@ -381,7 +381,7 @@ func (s *OrphanCleanupIntegrationSuite)
TestOrphanCleanupWithConcurrency() {
// Clean up original orphan files
fs := s.mustFS(tbl)
for _, orphanFile := range orphanFiles {
- fs.Remove(orphanFile) // Ignore errors, might already be deleted
+ fs.Remove(s.ctx, orphanFile) // Ignore errors, might already be
deleted
}
}
@@ -393,10 +393,10 @@ func (s *OrphanCleanupIntegrationSuite)
TestOrphanCleanupCustomDeleteFunction()
time.Sleep(100 * time.Millisecond)
var deletedByCustomFunc []string
- customDeleteFunc := func(filePath string) error {
+ customDeleteFunc := func(ctx context.Context, filePath string) error {
deletedByCustomFunc = append(deletedByCustomFunc, filePath)
fs := s.mustFS(tbl)
- return fs.Remove(filePath)
+ return fs.Remove(ctx, filePath)
}
result, err := maintenance.DeleteOrphanFiles(s.ctx, tbl,
diff --git a/table/maintenance/orphan_cleanup_test.go
b/table/maintenance/orphan_cleanup_test.go
index 941036f2e..a13c49c42 100644
--- a/table/maintenance/orphan_cleanup_test.go
+++ b/table/maintenance/orphan_cleanup_test.go
@@ -73,7 +73,7 @@ func TestOrphanCleanupOptions(t *testing.T) {
WithCleanupDryRun(true)(cfg)
assert.True(t, cfg.dryRun)
- deleteFunc := func(string) error { return nil }
+ deleteFunc := func(context.Context, string) error { return nil }
WithCleanupDeleteFunc(deleteFunc)(cfg)
assert.NotNil(t, cfg.deleteFunc)
@@ -200,9 +200,9 @@ func TestOrphanCleanupPlanDoesNotExpandAfterPlanning(t
*testing.T) {
require.NoError(t, err)
assert.Equal(t, []string{plannedOrphan}, result.DeletedFiles)
- _, err = fs.Open(plannedOrphan)
+ _, err = fs.Open(context.Background(), plannedOrphan)
assert.ErrorIs(t, err, stdfs.ErrNotExist)
- file, err := fs.Open(newOrphan)
+ file, err := fs.Open(context.Background(), newOrphan)
require.NoError(t, err)
assert.NoError(t, file.Close())
}
@@ -1519,11 +1519,11 @@ type mockBulkRemovableIO struct {
bulkPaths []string
}
-func (m *mockBulkRemovableIO) Open(string) (iceio.File, error) {
+func (m *mockBulkRemovableIO) Open(context.Context, string) (iceio.File,
error) {
return nil, errors.New("not implemented")
}
-func (m *mockBulkRemovableIO) Remove(string) error {
+func (m *mockBulkRemovableIO) Remove(context.Context, string) error {
return errors.New("Remove should not be called when BulkRemovableIO is
available")
}
@@ -1551,7 +1551,7 @@ func TestDeleteFilesWithCustomDeleteFunc(t *testing.T) {
var customDeleted []string
cfg := &orphanCleanupConfig{
- deleteFunc: func(path string) error {
+ deleteFunc: func(_ context.Context, path string) error {
customDeleted = append(customDeleted, path)
return nil
@@ -1579,11 +1579,11 @@ type mockPlainIO struct {
removed []string
}
-func (m *mockPlainIO) Open(string) (iceio.File, error) {
+func (m *mockPlainIO) Open(context.Context, string) (iceio.File, error) {
return nil, errors.New("not implemented")
}
-func (m *mockPlainIO) Remove(name string) error {
+func (m *mockPlainIO) Remove(_ context.Context, name string) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -1639,7 +1639,7 @@ type purgeDeleteTrackingIO struct {
removed []string
}
-func (m *purgeDeleteTrackingIO) Remove(name string) error {
+func (m *purgeDeleteTrackingIO) Remove(_ context.Context, name string) error {
m.mu.Lock()
m.active++
m.maxActive = max(m.maxActive, m.active)
@@ -1680,7 +1680,7 @@ func TestDeleteFilesParallelCollectsPurgeErrors(t
*testing.T) {
context.Background(),
files,
4,
- func(path string) error {
+ func(_ context.Context, path string) error {
mu.Lock()
calls[path]++
mu.Unlock()
@@ -1734,7 +1734,7 @@ func
TestDeleteFilesParallelPreservesErrorWithNilWrapper(t *testing.T) {
context.Background(),
files,
2,
- func(path string) error {
+ func(_ context.Context, path string) error {
if path == files[1] {
return deleteErr
}
@@ -1757,7 +1757,7 @@ func
TestDeleteFilesParallelStopsQueuedWorkOnCancellation(t *testing.T) {
ctx,
[]string{"s3://bucket/table/file.parquet"},
4,
- func(string) error {
+ func(context.Context, string) error {
called = true
return nil
@@ -1817,7 +1817,7 @@ func
TestDeleteFilesParallelStopsQueuedWorkOnMidFlightCancellation(t *testing.T)
gatedErrContext{Context: ctx, checks: checks,
errCalls: errCalls},
files,
maxConcurrency,
- func(string) error {
+ func(context.Context, string) error {
if calls.Add(1) > maxConcurrency {
extraCalls <- struct{}{}
}
@@ -2015,7 +2015,7 @@ func TestDeleteOrphanFilesPrefixMismatchModes(t
*testing.T) {
WithCleanupLocation("s3://bucket/path"),
WithCleanupFilesOlderThan(0),
WithPrefixMismatchMode(mode),
- WithCleanupDeleteFunc(func(path
string) error {
+ WithCleanupDeleteFunc(func(_
context.Context, path string) error {
deleted =
append(deleted, path)
return nil
diff --git a/table/manifest_content_cache.go b/table/manifest_content_cache.go
index 7079016d1..4b450a24a 100644
--- a/table/manifest_content_cache.go
+++ b/table/manifest_content_cache.go
@@ -160,7 +160,7 @@ func (c *manifestContentCache) open(
location := manifest.FilePath()
expectedLength := manifest.Length()
if expectedLength <= 0 || expectedLength > c.maxContentLength ||
expectedLength > c.maxTotalBytes {
- return base.Open(location)
+ return base.Open(ctx, location)
}
for {
@@ -194,7 +194,7 @@ func (c *manifestContentCache) open(
if load.err != nil {
// A failed population must not make concurrent
scans fail
// when the ordinary FileIO path would have
succeeded.
- file, err := base.Open(location)
+ file, err := base.Open(ctx, location)
if err != nil {
return nil, fmt.Errorf("open manifest
%s: cache population failed: %w; fallback open failed: %w",
location, load.err, err)
@@ -203,7 +203,7 @@ func (c *manifestContentCache) open(
return file, nil
}
if int64(len(load.content)) != expectedLength {
- return base.Open(location)
+ return base.Open(ctx, location)
}
return newCachedManifestFile(location, load.content),
nil
@@ -227,7 +227,7 @@ func (c *manifestContentCache) open(
}
c.finishLoad(location, expectedLength, load,
content, err, canceled)
}()
- content, err = readManifestContent(base, location,
expectedLength)
+ content, err = readManifestContent(ctx, base, location,
expectedLength)
canceled = err != nil && ctx.Err() != nil
}()
if err != nil {
@@ -238,7 +238,7 @@ func (c *manifestContentCache) open(
// Preserve the ordinary-open fallback for the
producer, and
// explain repeated uncached reads for bad manifest
descriptors.
log.Printf("Warning: manifest cache population failed
for %s: %v; falling back to FileIO", location, err)
- file, fallbackErr := base.Open(location)
+ file, fallbackErr := base.Open(ctx, location)
if fallbackErr != nil {
return nil, fmt.Errorf("open manifest %s: cache
population failed: %w; fallback open failed: %w",
location, err, fallbackErr)
@@ -361,8 +361,8 @@ func (c *manifestContentCache) removeEntryLocked(entry
*manifestContentCacheEntr
c.totalBytes -= int64(len(entry.content))
}
-func readManifestContent(base iceio.IO, location string, expectedLength int64)
(content []byte, err error) {
- file, err := base.Open(location)
+func readManifestContent(ctx context.Context, base iceio.IO, location string,
expectedLength int64) (content []byte, err error) {
+ file, err := base.Open(ctx, location)
if err != nil {
return nil, err
}
@@ -401,16 +401,18 @@ type manifestContentCacheIO struct {
manifest iceberg.ManifestFile
}
-func (m *manifestContentCacheIO) Open(name string) (iceio.File, error) {
+func (m *manifestContentCacheIO) Open(ctx context.Context, name string)
(iceio.File, error) {
if name != m.manifest.FilePath() {
- return m.base.Open(name)
+ return m.base.Open(ctx, name)
}
+ // The manifest readers above this wrapper have no caller context to
pass,
+ // so the cached path is bound by the scan context captured in wrap.
return m.cache.open(m.ctx, m.base, m.manifest)
}
-func (m *manifestContentCacheIO) Remove(name string) error {
- return m.base.Remove(name)
+func (m *manifestContentCacheIO) Remove(ctx context.Context, name string)
error {
+ return m.base.Remove(ctx, name)
}
type cachedManifestFile struct {
diff --git a/table/manifest_content_cache_bench_test.go
b/table/manifest_content_cache_bench_test.go
index 42e2a9e8a..4840c866a 100644
--- a/table/manifest_content_cache_bench_test.go
+++ b/table/manifest_content_cache_bench_test.go
@@ -87,7 +87,7 @@ type manifestContentCacheBenchmarkIO struct {
opens int64
}
-func (fs *manifestContentCacheBenchmarkIO) Open(name string) (iceio.File,
error) {
+func (fs *manifestContentCacheBenchmarkIO) Open(ctx context.Context, name
string) (iceio.File, error) {
if strings.Contains(name, "/manifest-") {
if fs.latency > 0 {
time.Sleep(fs.latency)
@@ -97,7 +97,7 @@ func (fs *manifestContentCacheBenchmarkIO) Open(name string)
(iceio.File, error)
fs.mu.Unlock()
}
- return fs.IO.Open(name)
+ return fs.IO.Open(ctx, name)
}
func (fs *manifestContentCacheBenchmarkIO) reset() {
diff --git a/table/manifest_content_cache_internal_test.go
b/table/manifest_content_cache_internal_test.go
index f1abe524f..a4fd5189d 100644
--- a/table/manifest_content_cache_internal_test.go
+++ b/table/manifest_content_cache_internal_test.go
@@ -136,8 +136,8 @@ type manifestContentCountingIO struct {
files []*manifestContentCountingFile
}
-func (f *manifestContentCountingIO) Open(name string) (iceio.File, error) {
- file, err := f.IO.Open(name)
+func (f *manifestContentCountingIO) Open(ctx context.Context, name string)
(iceio.File, error) {
+ file, err := f.IO.Open(ctx, name)
if err != nil {
return nil, err
}
@@ -508,14 +508,14 @@ type manifestContentCacheFailOnceIO struct {
failed bool
}
-func (f *manifestContentCacheFailOnceIO) Open(name string) (iceio.File, error)
{
+func (f *manifestContentCacheFailOnceIO) Open(ctx context.Context, name
string) (iceio.File, error) {
if !f.failed {
f.failed = true
return nil, io.ErrUnexpectedEOF
}
- return f.IO.Open(name)
+ return f.IO.Open(ctx, name)
}
func TestManifestContentCacheFailedPopulationFallsBackWithoutCaching(t
*testing.T) {
@@ -537,7 +537,7 @@ type manifestContentCacheOpenErrorsIO struct {
openErrors []error
}
-func (f *manifestContentCacheOpenErrorsIO) Open(string) (iceio.File, error) {
+func (f *manifestContentCacheOpenErrorsIO) Open(context.Context, string)
(iceio.File, error) {
err := f.openErrors[0]
f.openErrors = f.openErrors[1:]
@@ -564,7 +564,7 @@ type manifestContentCachePanicIO struct {
iceio.IO
}
-func (manifestContentCachePanicIO) Open(string) (iceio.File, error) {
+func (manifestContentCachePanicIO) Open(context.Context, string) (iceio.File,
error) {
panic("unexpected backend panic")
}
@@ -657,7 +657,7 @@ type manifestContentFailingBlockedIO struct {
opens int
}
-func (f *manifestContentFailingBlockedIO) Open(name string) (iceio.File,
error) {
+func (f *manifestContentFailingBlockedIO) Open(ctx context.Context, name
string) (iceio.File, error) {
f.mu.Lock()
f.opens++
attempt := f.opens
@@ -676,7 +676,7 @@ func (f *manifestContentFailingBlockedIO) Open(name string)
(iceio.File, error)
return nil, f.fallbackError
}
- return f.IO.Open(name)
+ return f.IO.Open(ctx, name)
}
// Failed population is a cache optimization failure, not a scan failure.
diff --git a/table/manifest_list_partitions_regression_test.go
b/table/manifest_list_partitions_regression_test.go
index 998a7727c..56c3afa4a 100644
--- a/table/manifest_list_partitions_regression_test.go
+++ b/table/manifest_list_partitions_regression_test.go
@@ -84,7 +84,7 @@ type manifestListRecordV2 struct {
func readManifestListPartitionFields(t *testing.T, fs iceio.IO,
manifestListPath string) []any {
t.Helper()
- f, err := fs.Open(manifestListPath)
+ f, err := fs.Open(context.Background(), manifestListPath)
require.NoError(t, err)
defer func() { require.NoError(t, f.Close()) }()
diff --git a/table/partition_residual_read_test.go
b/table/partition_residual_read_test.go
index f9bfc6021..5fb45a2ea 100644
--- a/table/partition_residual_read_test.go
+++ b/table/partition_residual_read_test.go
@@ -188,7 +188,7 @@ func writePartitionResidualParquet(
parquet.NewWriterProperties(parquet.WithStats(true)),
pqarrow.DefaultWriterProps()))
require.NoError(t, writer.Close())
- file, err := fs.Open(path)
+ file, err := fs.Open(context.Background(), path)
require.NoError(t, err)
defer file.Close()
info, err := file.Stat()
diff --git a/table/rewrite_data_files.go b/table/rewrite_data_files.go
index a0d167989..a1a4f7159 100644
--- a/table/rewrite_data_files.go
+++ b/table/rewrite_data_files.go
@@ -783,7 +783,7 @@ func cleanupCompactionOutputs(fs iceio.IO, batchResults
[]CompactionGroupResult)
var cleanupErr error
for path := range paths {
- if err := fs.Remove(path); err != nil && !errors.Is(err,
iofs.ErrNotExist) {
+ if err := fs.Remove(context.Background(), path); err != nil &&
!errors.Is(err, iofs.ErrNotExist) {
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("remove
%s: %w", path, err))
}
}
diff --git a/table/rewrite_manifests.go b/table/rewrite_manifests.go
index d12cbcaaf..e57ad73f8 100644
--- a/table/rewrite_manifests.go
+++ b/table/rewrite_manifests.go
@@ -314,7 +314,7 @@ func (r *rewriteManifests) deleteWritten(input, merged
[]iceberg.ManifestFile) {
if _, ok := inPaths[m.FilePath()]; ok {
continue
}
- if err := r.base.io.Remove(m.FilePath()); err != nil {
+ if err := r.base.io.Remove(context.Background(), m.FilePath());
err != nil {
log.Printf("Warning: failed to delete orphaned merged
manifest %s: %v", m.FilePath(), err)
}
}
diff --git a/table/rewrite_manifests_bench_test.go
b/table/rewrite_manifests_bench_test.go
index f6d59e14c..0f8892857 100644
--- a/table/rewrite_manifests_bench_test.go
+++ b/table/rewrite_manifests_bench_test.go
@@ -107,7 +107,7 @@ func benchmarkManifestMergeMode(b *testing.B, cluster bool)
{
}
b.StopTimer()
for _, manifest := range output {
- if err := mem.Remove(manifest.FilePath()); err != nil {
+ if err := mem.Remove(context.Background(),
manifest.FilePath()); err != nil {
b.Fatal(err)
}
}
diff --git a/table/rewrite_manifests_cluster.go
b/table/rewrite_manifests_cluster.go
index 1f26f5778..fbc6b48d3 100644
--- a/table/rewrite_manifests_cluster.go
+++ b/table/rewrite_manifests_cluster.go
@@ -18,6 +18,7 @@
package table
import (
+ "context"
"errors"
"fmt"
"io"
@@ -152,7 +153,7 @@ func (m *manifestMergeManager) clusterManifests(manifests
[]iceberg.ManifestFile
writer.abort()
}
for _, path := range paths {
- if removeErr := m.snap.io.Remove(path); removeErr !=
nil {
+ if removeErr := m.snap.io.Remove(context.Background(),
path); removeErr != nil {
log.Printf("Warning: failed to delete orphaned
clustered manifest %s: %v", path, removeErr)
}
}
diff --git a/table/rewrite_manifests_test.go b/table/rewrite_manifests_test.go
index c27356870..963830206 100644
--- a/table/rewrite_manifests_test.go
+++ b/table/rewrite_manifests_test.go
@@ -817,12 +817,12 @@ func (f *trackingFS) Create(name string)
(iceio.FileWriter, error) {
return f.LocalFS.Create(name)
}
-func (f *trackingFS) Remove(name string) error {
+func (f *trackingFS) Remove(_ context.Context, name string) error {
f.mu.Lock()
f.removed = append(f.removed, name)
f.mu.Unlock()
- return f.LocalFS.Remove(name)
+ return f.LocalFS.Remove(context.Background(), name)
}
func (f *trackingFS) snapshotCreated() map[string]struct{} {
diff --git a/table/row_lineage_rewrite_bench_test.go
b/table/row_lineage_rewrite_bench_test.go
index b9afd326c..fb74dc542 100644
--- a/table/row_lineage_rewrite_bench_test.go
+++ b/table/row_lineage_rewrite_bench_test.go
@@ -158,7 +158,7 @@ func (f *rowLineageRewriteBenchmarkFixture) reset(b
*testing.B) {
if _, ok := f.initialFiles[path]; ok {
continue
}
- require.NoError(b, f.fs.Remove(path))
+ require.NoError(b, f.fs.Remove(context.Background(), path))
}
f.catalog.metadata = f.baseMetadata
}
diff --git a/table/row_lineage_scan_test.go b/table/row_lineage_scan_test.go
index 619c872e1..aa845df8d 100644
--- a/table/row_lineage_scan_test.go
+++ b/table/row_lineage_scan_test.go
@@ -171,7 +171,7 @@ func parquetRowGroupCount(t *testing.T, tbl *table.Table)
int {
require.NoError(t, err)
require.Len(t, tasks, 1)
- f, err := iceio.LocalFS{}.Open(tasks[0].File.FilePath())
+ f, err := iceio.LocalFS{}.Open(context.Background(),
tasks[0].File.FilePath())
require.NoError(t, err)
rdr, err := file.NewParquetReader(f)
require.NoError(t, err)
diff --git a/table/scan_splits_test.go b/table/scan_splits_test.go
index f81c35322..6bcd2c1a0 100644
--- a/table/scan_splits_test.go
+++ b/table/scan_splits_test.go
@@ -472,8 +472,8 @@ type prepareReadTrackingIO struct {
closed int
}
-func (fs *prepareReadTrackingIO) Open(path string) (iceio.File, error) {
- file, err := fs.IO.Open(path)
+func (fs *prepareReadTrackingIO) Open(_ context.Context, path string)
(iceio.File, error) {
+ file, err := fs.IO.Open(context.Background(), path)
if err != nil {
return nil, err
}
diff --git a/table/scanner_internal_test.go b/table/scanner_internal_test.go
index fa90c0d11..808a80aa9 100644
--- a/table/scanner_internal_test.go
+++ b/table/scanner_internal_test.go
@@ -82,12 +82,12 @@ type expiringManifestIO struct {
expired atomic.Bool
}
-func (fs *expiringManifestIO) Open(name string) (iceio.File, error) {
+func (fs *expiringManifestIO) Open(_ context.Context, name string)
(iceio.File, error) {
if name != fs.manifestList && fs.expired.Load() {
return nil, errors.New("credentials expired")
}
- file, err := fs.base.Open(name)
+ file, err := fs.base.Open(context.Background(), name)
if err == nil && name == fs.manifestList {
fs.expired.Store(true)
}
@@ -95,8 +95,8 @@ func (fs *expiringManifestIO) Open(name string) (iceio.File,
error) {
return file, err
}
-func (fs *expiringManifestIO) Remove(name string) error {
- return fs.base.Remove(name)
+func (fs *expiringManifestIO) Remove(_ context.Context, name string) error {
+ return fs.base.Remove(context.Background(), name)
}
type failingManifestIO struct {
@@ -105,18 +105,18 @@ type failingManifestIO struct {
laterOpens atomic.Int64
}
-func (fs *failingManifestIO) Open(name string) (iceio.File, error) {
+func (fs *failingManifestIO) Open(_ context.Context, name string) (iceio.File,
error) {
if name == fs.failingPath {
return nil, errors.New("manifest failed")
}
fs.laterOpens.Add(1)
- return fs.base.Open(name)
+ return fs.base.Open(context.Background(), name)
}
-func (fs *failingManifestIO) Remove(name string) error {
- return fs.base.Remove(name)
+func (fs *failingManifestIO) Remove(_ context.Context, name string) error {
+ return fs.base.Remove(context.Background(), name)
}
func TestPlanFilesRefreshesFileIODuringManifestPlanning(t *testing.T) {
diff --git a/table/snapshot_manifest_cache_bench_test.go
b/table/snapshot_manifest_cache_bench_test.go
index 21aa6340f..bf3e400e2 100644
--- a/table/snapshot_manifest_cache_bench_test.go
+++ b/table/snapshot_manifest_cache_bench_test.go
@@ -91,12 +91,12 @@ type snapshotManifestCacheBenchmarkIO struct {
opens map[string]int
}
-func (fs *snapshotManifestCacheBenchmarkIO) Open(name string) (iceio.File,
error) {
+func (fs *snapshotManifestCacheBenchmarkIO) Open(_ context.Context, name
string) (iceio.File, error) {
fs.mu.Lock()
fs.opens[name]++
fs.mu.Unlock()
- return fs.IO.Open(name)
+ return fs.IO.Open(context.Background(), name)
}
var snapshotManifestSliceBenchmarkSink iter.Seq[iceberg.ManifestFile]
diff --git a/table/snapshot_manifest_cache_internal_test.go
b/table/snapshot_manifest_cache_internal_test.go
index 203ee76ca..c347d2b18 100644
--- a/table/snapshot_manifest_cache_internal_test.go
+++ b/table/snapshot_manifest_cache_internal_test.go
@@ -270,7 +270,7 @@ type blockingSnapshotManifestIO struct {
opens map[string]int
}
-func (fs *blockingSnapshotManifestIO) Open(name string) (iceio.File, error) {
+func (fs *blockingSnapshotManifestIO) Open(_ context.Context, name string)
(iceio.File, error) {
if name == fs.blockedPath {
fs.once.Do(func() { close(fs.started) })
<-fs.release
@@ -280,7 +280,7 @@ func (fs *blockingSnapshotManifestIO) Open(name string)
(iceio.File, error) {
fs.opens[name]++
fs.mu.Unlock()
- return fs.IO.Open(name)
+ return fs.IO.Open(context.Background(), name)
}
type notifyingContext struct {
@@ -730,9 +730,9 @@ type contextBoundSnapshotManifestIO struct {
blockedPath string
}
-func (fs *contextBoundSnapshotManifestIO) Open(name string) (iceio.File,
error) {
+func (fs *contextBoundSnapshotManifestIO) Open(_ context.Context, name string)
(iceio.File, error) {
if fs.blockedPath != "" && name != fs.blockedPath {
- return fs.IO.Open(name)
+ return fs.IO.Open(context.Background(), name)
}
fs.startedOnce.Do(func() { close(fs.started) })
select {
diff --git a/table/snapshot_producers.go b/table/snapshot_producers.go
index ed6bd6c3b..31de7f14b 100644
--- a/table/snapshot_producers.go
+++ b/table/snapshot_producers.go
@@ -292,7 +292,7 @@ func (of *overwriteFiles) rewriteManifest(
if retErr == nil {
return
}
- if cleanupErr := of.base.io.Remove(path); cleanupErr != nil {
+ if cleanupErr := of.base.io.Remove(context.Background(), path);
cleanupErr != nil {
retErr = errors.Join(retErr, fmt.Errorf("remove failed
manifest %s: %w", path, cleanupErr))
}
}()
@@ -343,7 +343,7 @@ func (of *overwriteFiles) cleanupFilteredManifests(input
[]iceberg.ManifestFile,
continue
}
seen[path] = struct{}{}
- if err := of.base.io.Remove(path); err != nil {
+ if err := of.base.io.Remove(context.Background(),
path); err != nil {
cleanupErr = errors.Join(cleanupErr,
fmt.Errorf("remove filtered manifest %s: %w", path, err))
}
}
@@ -424,7 +424,7 @@ func (of *overwriteFiles) filterAndRewriteParentManifests(
func (sp *snapshotProducer) cleanupGeneratedManifests(manifests
[]iceberg.ManifestFile) error {
var cleanupErr error
for _, manifest := range manifests {
- if err := sp.io.Remove(manifest.FilePath()); err != nil {
+ if err := sp.io.Remove(context.Background(),
manifest.FilePath()); err != nil {
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("remove
generated manifest %s: %w", manifest.FilePath(), err))
}
}
@@ -648,7 +648,7 @@ func (m *manifestMergeManager) removeOrphans(input, output
[]iceberg.ManifestFil
if _, ok := inPaths[mf.FilePath()]; ok {
continue
}
- if err := m.snap.io.Remove(mf.FilePath()); err != nil {
+ if err := m.snap.io.Remove(context.Background(),
mf.FilePath()); err != nil {
log.Printf("Warning: failed to delete orphaned merged
manifest %s: %v", mf.FilePath(), err)
}
}
@@ -881,7 +881,7 @@ func (sp *snapshotProducer) newManifestWriter(spec
iceberg.PartitionSpec, opts .
wr, err := iceberg.NewManifestWriter(sp.txn.meta.formatVersion,
counter, spec,
sp.txn.meta.CurrentSchema(), sp.snapshotID, opts...)
if err != nil {
- return nil, "", nil, nil, errors.Join(err, out.Close(),
sp.io.Remove(path))
+ return nil, "", nil, nil, errors.Join(err, out.Close(),
sp.io.Remove(context.Background(), path))
}
return wr, path, counter, out, nil
@@ -1104,7 +1104,8 @@ func (sp *snapshotProducer) writeDeletedEntries(ctx
context.Context, deleted []i
if retErr == nil {
return
}
- if cleanupErr := sp.io.Remove(path); cleanupErr != nil {
+ // retErr may be ctx's cancellation; remove the partial
manifest anyway.
+ if cleanupErr :=
sp.io.Remove(context.WithoutCancel(ctx), path); cleanupErr != nil {
retErr = errors.Join(retErr, fmt.Errorf("remove
failed delete manifest %s: %w", path, cleanupErr))
}
}()
@@ -1521,7 +1522,7 @@ func writeManifestListFile(
defer func() {
if err != nil {
- err = errors.Join(err, fs.Remove(path))
+ err = errors.Join(err, fs.Remove(context.Background(),
path))
}
}()
defer internal.CheckedClose(out, &err)
diff --git a/table/snapshot_producers_bench_test.go
b/table/snapshot_producers_bench_test.go
index c02ea6092..ccb08088a 100644
--- a/table/snapshot_producers_bench_test.go
+++ b/table/snapshot_producers_bench_test.go
@@ -35,10 +35,10 @@ type delayedManifestIO struct {
delay time.Duration
}
-func (io *delayedManifestIO) Open(name string) (iceio.File, error) {
+func (io *delayedManifestIO) Open(_ context.Context, name string) (iceio.File,
error) {
time.Sleep(io.delay)
- return io.memIO.Open(name)
+ return io.memIO.Open(context.Background(), name)
}
func (io *delayedManifestIO) Create(name string) (iceio.FileWriter, error) {
diff --git a/table/snapshot_producers_test.go b/table/snapshot_producers_test.go
index 8346fe377..fb6209946 100644
--- a/table/snapshot_producers_test.go
+++ b/table/snapshot_producers_test.go
@@ -97,7 +97,7 @@ func newMemIO(limit int, err error) *memIO {
}
}
-func (m *memIO) Open(name string) (iceio.File, error) {
+func (m *memIO) Open(_ context.Context, name string) (iceio.File, error) {
m.mu.Lock()
data, ok := m.files[name]
m.mu.Unlock()
@@ -120,7 +120,7 @@ func (m *memIO) WriteFile(name string, content []byte)
error {
return nil
}
-func (m *memIO) Remove(name string) error {
+func (m *memIO) Remove(_ context.Context, name string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.files, name)
@@ -452,7 +452,7 @@ func TestCommitV3RowLineageDeltaIncludesExistingRows(t
*testing.T) {
func readManifestListFromPath(t *testing.T, fs iceio.IO, path string)
[]iceberg.ManifestFile {
t.Helper()
- f, err := fs.Open(path)
+ f, err := fs.Open(context.Background(), path)
require.NoError(t, err, "open manifest list: %s", path)
defer f.Close()
@@ -908,7 +908,7 @@ func newTrackingIO() *trackingIO {
}
}
-func (t *trackingIO) Open(name string) (iceio.File, error) {
+func (t *trackingIO) Open(_ context.Context, name string) (iceio.File, error) {
data, ok := t.files[name]
if !ok {
return nil, fs.ErrNotExist
@@ -933,7 +933,7 @@ func (t *trackingIO) WriteFile(name string, content []byte)
error {
return nil
}
-func (t *trackingIO) Remove(name string) error {
+func (t *trackingIO) Remove(_ context.Context, name string) error {
delete(t.files, name)
return nil
@@ -992,13 +992,13 @@ func (c *closeErrorIO) Create(name string)
(iceio.FileWriter, error) {
return writer, err
}
-func (c *closeErrorIO) Remove(name string) error {
+func (c *closeErrorIO) Remove(_ context.Context, name string) error {
c.removes = append(c.removes, name)
if c.removeErr != nil {
return c.removeErr
}
- return c.trackingIO.Remove(name)
+ return c.trackingIO.Remove(context.Background(), name)
}
func TestCommitManifestsCloseFailureReturnsNoUpdates(t *testing.T) {
diff --git a/table/snapshots.go b/table/snapshots.go
index 6e75bbce5..38696a3ff 100644
--- a/table/snapshots.go
+++ b/table/snapshots.go
@@ -18,6 +18,7 @@
package table
import (
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -406,7 +407,8 @@ func (s Snapshot) ValidateRowLineage() error {
func (s Snapshot) Manifests(fio iceio.IO) (_ []iceberg.ManifestFile, err
error) {
if s.ManifestList != "" {
- f, err := fio.Open(s.ManifestList)
+ // No caller context to thread here.
+ f, err := fio.Open(context.Background(), s.ManifestList)
if err != nil {
return nil, fmt.Errorf("could not open manifest file:
%w", err)
}
@@ -452,7 +454,7 @@ func embeddedManifestLength(fio iceio.IO, path string) (_
int64, err error) {
return info.Size(), nil
}
- f, err := fio.Open(path)
+ f, err := fio.Open(context.Background(), path)
if err != nil {
return 0, fmt.Errorf("could not open embedded manifest %q: %w",
path, err)
}
diff --git a/table/table.go b/table/table.go
index 3807f5e7f..ad2c6792b 100644
--- a/table/table.go
+++ b/table/table.go
@@ -649,8 +649,11 @@ func (t Table) doCommit(ctx context.Context, updates
[]Update, reqs []Requiremen
if !cleanupOrphans || len(orphanedManifests) == 0 {
return
}
+ // The commit may have failed because ctx was canceled; remove
the
+ // orphans without that cancellation so they are not left
behind.
+ cleanupCtx := context.WithoutCancel(ctx)
for _, path := range orphanedManifests {
- if removeErr := wfs.Remove(path); removeErr != nil {
+ if removeErr := wfs.Remove(cleanupCtx, path); removeErr
!= nil {
log.Printf("Warning: failed to delete orphaned
manifest list %s: %v", path, removeErr)
}
}
@@ -1212,7 +1215,8 @@ func deleteOldMetadata(fs icebergio.IO, baseMeta, newMeta
Metadata) {
toRemove := internal.Difference(removedPrevious,
currentMetadata)
for _, file := range toRemove {
- if err := fs.Remove(file); err != nil {
+ // deleteOldMetadata is best-effort cleanup with no
caller context.
+ if err := fs.Remove(context.Background(), file); err !=
nil {
// Log the error instead of raising it when
deleting old metadata files, as an external entity like a compactor may have
already deleted them
log.Printf("Warning: Failed to delete old
metadata file: %s error: %v", file, err)
}
@@ -1541,7 +1545,7 @@ func NewFromLocation(
return nil, err
}
} else {
- f, err := fsys.Open(metalocation)
+ f, err := fsys.Open(ctx, metalocation)
if err != nil {
return nil, err
}
diff --git a/table/table_test.go b/table/table_test.go
index c00672a4e..1467d2d97 100644
--- a/table/table_test.go
+++ b/table/table_test.go
@@ -2619,7 +2619,7 @@ func arrowTableWithNull() arrow.Table {
}
func (t *TableWritingTestSuite) validateManifestFileLength(fs iceio.IO, m
iceberg.ManifestFile) {
- f, err := fs.Open(m.FilePath())
+ f, err := fs.Open(context.Background(), m.FilePath())
t.Require().NoError(err)
defer f.Close()
@@ -3416,7 +3416,7 @@ func (t *TableTestSuite)
TestMetadataCompressionRoundTrip() {
t.Require().NoError(err)
// Read the metadata file and verify it's gzipped
- file, err := fs.Open(metadataLoc)
+ file, err := fs.Open(context.Background(), metadataLoc)
t.Require().NoError(err)
defer file.Close()
@@ -3475,7 +3475,7 @@ func (t *TableTestSuite)
TestMetadataCompressionRoundTripZstd() {
fs, err := tbl.FS(context.Background())
t.Require().NoError(err)
- file, err := fs.Open(metadataLoc)
+ file, err := fs.Open(context.Background(), metadataLoc)
t.Require().NoError(err)
defer file.Close()
diff --git a/table/transaction_test.go b/table/transaction_test.go
index ad08e47c3..b3c02361f 100644
--- a/table/transaction_test.go
+++ b/table/transaction_test.go
@@ -653,7 +653,7 @@ func (s *SparkIntegrationTestSuite) variantField(obj
variant.ObjectValue, key st
func (s *SparkIntegrationTestSuite) assertVariantFileShredded(tbl
*table.Table, path string) {
fs, err := tbl.FS(s.ctx)
s.Require().NoError(err)
- f, err := fs.Open(path)
+ f, err := fs.Open(s.ctx, path)
s.Require().NoError(err)
defer f.Close()
diff --git a/table/updates.go b/table/updates.go
index fa50ac034..4c4783ad8 100644
--- a/table/updates.go
+++ b/table/updates.go
@@ -803,7 +803,7 @@ func (u *removeSnapshotsUpdate) PostCommit(ctx
context.Context, preTable *Table,
var res error
for _, f := range paths {
- if err := prefs.Remove(f); err != nil {
+ if err := prefs.Remove(ctx, f); err != nil {
res = errors.Join(res, err)
}
}
diff --git a/table/updates_test.go b/table/updates_test.go
index 60e48c72c..bb4b15895 100644
--- a/table/updates_test.go
+++ b/table/updates_test.go
@@ -52,20 +52,20 @@ func newTrackingCallsIO() *trackingCallsIO {
}
}
-func (c *trackingCallsIO) Open(name string) (iceio.File, error) {
+func (c *trackingCallsIO) Open(_ context.Context, name string) (iceio.File,
error) {
c.mu.Lock()
c.openCount[name]++
c.mu.Unlock()
- return c.trackingIO.Open(name)
+ return c.trackingIO.Open(context.Background(), name)
}
-func (c *trackingCallsIO) Remove(name string) error {
+func (c *trackingCallsIO) Remove(_ context.Context, name string) error {
c.mu.Lock()
c.removeCount[name]++
c.mu.Unlock()
- return c.trackingIO.Remove(name)
+ return c.trackingIO.Remove(context.Background(), name)
}
// writeManifest writes a v2 data manifest with a single ADDED
diff --git a/table/write_records_test.go b/table/write_records_test.go
index 6273c060b..cc2efb1ce 100644
--- a/table/write_records_test.go
+++ b/table/write_records_test.go
@@ -517,10 +517,10 @@ func (s *WriteRecordsTestSuite)
TestTimestampNanosecondsShouldFloorNegativeValue
type readOnlyFS struct{}
-func (readOnlyFS) Open(name string) (iceio.File, error) {
+func (readOnlyFS) Open(_ context.Context, name string) (iceio.File, error) {
return nil, errors.New("not supported")
}
-func (readOnlyFS) Remove(name string) error {
+func (readOnlyFS) Remove(_ context.Context, name string) error {
return errors.New("not supported")
}
diff --git a/view/view.go b/view/view.go
index 3a97e0ab3..835214825 100644
--- a/view/view.go
+++ b/view/view.go
@@ -117,7 +117,7 @@ func NewFromLocation(
return nil, err
}
} else {
- f, err := fsys.Open(metalocation)
+ f, err := fsys.Open(ctx, metalocation)
if err != nil {
return nil, err
}
diff --git a/view/view_test.go b/view/view_test.go
index 0345f199a..e1329f27b 100644
--- a/view/view_test.go
+++ b/view/view_test.go
@@ -110,7 +110,7 @@ func (t *ViewTestSuite)
TestCreateViewJoinsTrailingSlashMetadataLocation() {
fs, err := iceio.LoadFS(t.T().Context(), nil, metadataLocation)
t.Require().NoError(err)
- metadataFile, err := fs.Open(metadataLocation)
+ metadataFile, err := fs.Open(context.Background(), metadataLocation)
t.Require().NoError(err)
t.Require().NoError(metadataFile.Close())
}