Skip to content

[python] fsync LocalFileIO writes before publishing snapshots - #10853

Open
xlb3055 wants to merge 3 commits into
apache:masterfrom
xlb3055:fix/python-localfileio-fsync
Open

xlb3055 wants to merge 3 commits into
apache:masterfrom
xlb3055:fix/python-localfileio-fsync

Conversation

@xlb3055

@xlb3055 xlb3055 commented Oct 28, 2026

Copy link
Copy Markdown

Purpose

Fixes #10739. Python LocalFileIO did not sync data or temporary snapshot/schema files. On a delayed-allocation filesystem, a rename can publish a snapshot filename before its contents reach disk; after a power loss the latest snapshot may be empty and table reads fail.

This change hardens the local write paths:

  • try_to_write_atomic: flush+fsync the temp file before rename; on fsync failure return False and do not publish; after successful rename, best-effort fsync the parent directory
  • new_output_stream: close flushes+fsyncs file data first; fsync failure is raised after the fd is closed; parent dir fsync only after successful file fsync
  • Parquet/ORC/Avro/Mosaic writers use new_output_stream so they share one file-sync path

Directory fsync is best effort where a platform does not support it. No physical power-loss test was run on this host.

Tests

  • local_file_io_fsync_test.py: 5 passed; checks sync order, fsync failure without publication, stream close behavior, and Parquet writing.
  • Existing file_io_test.py and resolving_file_io_test.py: 56 passed. ORC-focused shared-shredding tests: 6 passed.
  • Local CatalogFactory warehouse commit and read: 2 rows returned; 13 fsync calls during the second commit.
  • git diff --check: passed. One broader shared-shredding test could not run because zstandard is absent on this host. The full Python CI matrix was not run locally.

AI assistance was used for investigation, implementation, tests, review, and this description. No separate human code review is claimed.

Without fsync, rename can publish empty snapshot/schema files after a
crash on delayed-allocation filesystems (apache#10739). Flush and fsync file
data in try_to_write_atomic, output streams, and format writers; fsync
the parent directory after rename/create when the platform allows it.
Narrow durability to try_to_write_atomic and new_output_stream (format
writers reuse the stream). File fsync must succeed before rename/close
returns; fsync failure aborts publish. Tests assert ordering, API
compat, failure paths, and parquet-via-stream; local catalog commit
exercises the real write path.
Remove dead patch/repatch code from the fsync failure test. Keep
ordering assertions, simple first-fsync failure injection, stream API
checks, and parquet-via-stream coverage.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requirement fit: SUPPORTED. Implementation: FINDINGS.

The power-loss scenario in #10739 has clear end-to-end value. The new fsync, local/resolving IO and shared-shredding suites pass locally (76 cases, 31 subtests). Additional real ORC/Avro/ROW/BLOB/incremental Parquet callers round-trip and propagate injected file-sync errors. Two real catalog fault-injection cases below expose error-handling gaps at the durability boundary. No physical power-loss test was performed.

The project-configured Flake8 command also currently fails with F841 at local_file_io_fsync_test.py:168 (unused real_fsync); this revision has red Python CI jobs.

return
try:
os.fsync(fd)
except OSError:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Propagate real storage failures from directory synchronization

This catch also swallows EIO/ENOSPC, although the best-effort exception is described for platforms that do not support directory fsync. With a real filesystem CatalogFactory table, I prepared one row and injected OSError(errno.EIO) only when os.fstat(fd) identified a directory. committer.commit(messages) returned normally and published snapshot 1 after ignoring six directory-sync failures; five regular-file syncs succeeded. A successful file fsync does not establish persistence of its new directory entry or of the snapshot rename, so this still acknowledges a commit when the requested persistence step failed. Please ignore only the explicit unsupported-platform cases and preserve genuine errors/uncertain-publication handling before reporting success.

_fsync_file_obj(f)
except OSError:
# Do not rename: publishing would claim a durable snapshot/schema.
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep file-sync failures distinct from atomic-create contention

Returning False here feeds the unbounded retry loops in SchemaManager.create_table and SchemaManager.commit_changes, which interpret False as a schema publication race. I reproduced a real catalog create_table with persistent EIO on regular-file fsync: it repeatedly rewrites and retries schema-0 without exposing the storage error; the test had to stop at the fourth attempt. A control that propagates this OSError exits on the first attempt with the EIO cause. Please propagate the storage failure (while retaining temporary-file cleanup), or give the schema retry path a bounded error policy, instead of converting it to the contention sentinel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] [python] LocalFileIO never fsyncs: after a power loss the newest snapshot (or schema) file is empty and the table cannot be read

2 participants