Conversation
ieivanov
force-pushed
the
fix/fsync-before-progress-marker
branch
from
September 22, 2026 20:08
458ab44 to
6ba0ce7
Compare
A completion marker only ever meant that the shard files existed. Nothing in the stack forced their bytes to disk: neither iohub, nor zarr, nor the zarrs codec pipeline calls fsync, and Rust's File discards the result of close when it drops. The marker is tens of bytes and commits almost at once, while the shards it vouches for are hundreds of megabytes still draining to a networked filesystem, so a kill in between -- or a write-back error, which without a sync is dropped rather than raised -- leaves a done marker beside data that never landed. unit_is_complete cannot catch that afterwards. Its probe reads one element per shard, which on the geometries these pipelines produce decodes one inner chunk out of a hundred or more, so damage anywhere but the origin reads back clean and the unit is skipped for good. Sync the shards before writing the record, and record only once the store is closed so nothing the pipeline buffered sits outside the barrier. An interruption before the barrier now leaves no record and the unit is recomputed; an I/O error during it propagates instead of being recorded as success. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ieivanov
force-pushed
the
fix/fsync-before-progress-marker
branch
from
September 22, 2026 20:22
6ba0ce7 to
ac44339
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
None filed. Found by debugging a pipeline that failed repeatedly on the same shards. Related upstream report on the layer below: zarrs/zarrs#462.
Summary
WriteUnit.completerecorded a unit as finished if its shard files merely existed:Nothing forced those bytes to disk. Neither iohub, nor zarr, nor the zarrs codec pipeline calls
fsync, and Rust'sFilediscards the result ofclosewhen it drops, so a write-back error is dropped rather than raised. The marker is tens of bytes and commits almost immediately; the shards it vouches for are hundreds of megabytes still draining to a networked filesystem. A kill in between — or a swallowedEIO— leaves a.donemarker beside data that never landed.unit_is_completecannot catch that after the fact. Its probe reads one element per shard, which decodes a single inner chunk: on the geometries our reconstruction pipelines produce that is 1 of 354 (0.28%) for one store and 1 of 132 (0.76%) for another. Damage anywhere but the shard origin reads back clean, so the unit is skipped for good and the corruption becomes permanent.This PR:
_fsync_fileopens read-write rather than read-only because Windows refuses to commit a handle not opened for writing;_fsync_diris a no-op off POSIX, where losing a marker rename costs a recomputation at worst and never a wrong skip.The change is to the write path only. Recovering stores already damaged this way is deliberately out of scope here.
What this looked like in practice
Shards of several hundred MB, each full length with a valid CRC'd shard index and a handful of garbled inner chunks. Each was written by a job that was preempted or cancelled, or that exited 0 while the storage was returning
EIOto other jobs. In one case the retry "completed" the position in 2m28s, skipping 2280 units — including the corrupt ones — and the damage then survived several runs before a downstream stage finally read those voxels.Testing
pytest tests/ngff/test_ngff_utils.py— 77 passed. Two failures,test_apply_transform_to_czyx_and_saveandtest_apply_transform_to_tczyx_and_save, are pre-existing onmainand unrelated; I confirmed they fail identically on an unmodified tree.Three new tests. The first two fail on
mainand pass here:test_complete_syncs_every_shard_before_recording_the_unit— the unit's own marker does not exist while its shards are being syncedtest_a_shard_that_cannot_be_synced_is_not_recorded_as_done— anEIOduring the barrier propagates and leaves no.donetest_resume_skips_corruption_away_from_the_shard_origin— pins the default probe's limit deliberately. It passes before and after, and is here so the assumption is written down: the probe is not an integrity check, which is why the barrier is needed rather than a post-hoc verification.The existing
_tearhelper truncates a shard, which destroys the trailing index and so is caught by the origin probe immediately. The new_corrupt_inner_chunk_away_from_originhelper reproduces the real failure instead: full length, index intact, one inner chunk garbled. It selects its target by position in the shard index — C order over the inner chunk grid per the v3 sharding spec, so entry zero is always the origin's chunk — because choosing by file offset is unstable when the pipeline assembles chunks concurrently.Ran the new tests repeatedly and the full module several times to check for order dependence and flakiness.
Also verified downstream:
biahub's suite passes against this commit (242 passed, 1 skipped).Checklist
🤖 Generated with Claude Code