EC: handle zero-sized shard files uniformly (moves, rebuilds, startup cleanup) (#10753)

* volume_move: treat zero-sized EC shards as absent in move verification

A zero-sized shard file is residue of a failed operation (issue 10730),
not a shard - but VerifyEcShards only checked presence, so a copy that
landed as an empty file passed verification and the source was deleted
behind it. Size zero now reads as absent, with a distinct error naming
the zero-sized shard so the operator can tell a broken copy from a
missing one.

* storage: exclude zero-sized EC shards from rebuilds and clean up stale ones

The reproducer in issue 10730: a zero-sized shard file left by a failed
operation was selected as a Reed-Solomon input and failed the whole
rebuild with an input size mismatch, because input discovery checked
existence, not substance.

- RebuildEcFiles treats a zero-sized shard file as missing and
  regenerates over it in place (the reclassified-corrupt path: temp
  file beside the residue, atomic rename).
- The startup/rescan shard loader, which always skipped zero-sized
  files, now deletes them once they are older than an hour - young
  enough files can be an in-flight copy's just-created file, since the
  same scan runs from LoadNewVolumes while serving.

Regression tests: a rebuild with one emptied shard regenerates it
byte-identical; the loader deletes a stale zero-sized shard and leaves
a fresh one alone.

* storage: age-check each zero-shard cleanup candidate individually

The shard scan merges the data and idx directory listings, so the
age-checked entry and a deletion candidate can be different files
sharing one name - a stale zero-sized file in one directory next to a
fresh same-named file in the other (possibly an in-flight copy's
just-created one) could get the fresh file deleted. Each candidate's
own modification time now decides, both directories are handled in one
pass, and the split-directory case is pinned by a test.
This commit is contained in:
Chris Lu
2026-08-13 21:38:22 -07:00
committed by GitHub
parent 9386a25a4a
commit 94f8e2caf9
9 changed files with 228 additions and 31 deletions
@@ -542,13 +542,13 @@ func TestEcxFileDeletionWithSeparateHandles(t *testing.T) {
// are journaled to .ecj and tracked in an in-memory set — so the
// durability chain decode relies on is:
//
// 1. DeleteNeedleFromEcx appends the needle id to .ecj and fsyncs it.
// 2. Runtime reads via FindNeedleFromEcx consult the in-memory set and
// return TombstoneFileSize even though the sealed .ecx record on
// disk still shows the original size.
// 3. ec.decode later closes the EcVolume and calls RebuildEcxFile on
// the now-quiescent files, which walks .ecj and writes tombstones
// into .ecx. CopyFile then reads the rebuilt .ecx.
// 1. DeleteNeedleFromEcx appends the needle id to .ecj and fsyncs it.
// 2. Runtime reads via FindNeedleFromEcx consult the in-memory set and
// return TombstoneFileSize even though the sealed .ecx record on
// disk still shows the original size.
// 3. ec.decode later closes the EcVolume and calls RebuildEcxFile on
// the now-quiescent files, which walks .ecj and writes tombstones
// into .ecx. CopyFile then reads the rebuilt .ecx.
//
// This test exercises the full chain on a tempdir fixture.
func TestEcVolumeDeleteDurableToJournal(t *testing.T) {