Persist reallocated mapping before parity resize in force-realloc sync

This commit is contained in:
Andrea Mazzoleni 2026-08-28 18:08:29 +02:00
parent 395291bd9a
commit 82be7b07b3
5 changed files with 81 additions and 20 deletions

View file

@ -1282,6 +1282,39 @@ endif # !HAVE_MEMORY_CHECKER
$(MSG) Sync after all the fixes
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
#### FORCE-REALLOC CRASH TESTS ####
$(MSG) Test interrupted force-realloc post-resize crash
dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_CRASH1
dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_CRASH2
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
rm bench/disk1/REALLOC_CRASH1
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_crash_before.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) -R --test-kill-after-resize sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_crash_after.txt
! diff bench/realloc_crash_before.txt bench/realloc_crash_after.txt > /dev/null
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_crash_final.txt
diff bench/realloc_crash_after.txt bench/realloc_crash_final.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
rm bench/disk1/REALLOC_CRASH2
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(MSG) Test interrupted force-realloc-tail post-resize crash
dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_TAIL1
dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_TAIL2
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
rm bench/disk1/REALLOC_TAIL1
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_tail_crash_before.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) -W 10M --test-kill-after-resize sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_tail_crash_after.txt
! diff bench/realloc_tail_crash_before.txt bench/realloc_tail_crash_after.txt > /dev/null
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_tail_crash_final.txt
diff bench/realloc_tail_crash_after.txt bench/realloc_tail_crash_final.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
rm bench/disk1/REALLOC_TAIL2
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
endif # HAVE_THREAD_CHECKER
$(MSG) Regression test completed with SUCCESS!
$(MSG) Please ignore any error message printed above, they are expected!

View file

@ -250,8 +250,13 @@ void state_locate_mark_tail_blocks_for_resync(struct snapraid_state* state, uint
/*
* Explicit force-realloc overrides an in-place REBUILD request.
* REP makes the block eligible for the normal reallocation path.
*
* This transition changes persistent block-state semantics. Mark the
* state dirty explicitly rather than relying on the following scan to
* incidentally do so.
*/
block_state_set(block, BLOCK_STATE_REP);
state->need_write = 1;
}
}
}

View file

@ -257,6 +257,7 @@ void log_close(const char* file)
#define OPT_TEST_SPEED_DISKS_NUMBER 308
#define OPT_TEST_SPEED_BLOCKS_SIZE 309
#define OPT_TEST_KILL_BEFORE_SYNC 310
#define OPT_TEST_KILL_AFTER_RESIZE 311
#if HAVE_GETOPT_LONG
@ -312,7 +313,10 @@ static struct option long_options[] = {
/* After syncing, do not write the new content file */
{ "test-kill-after-sync", 0, 0, OPT_TEST_KILL_AFTER_SYNC },
/* Do not execure syncing after writing the new initial content file */
/* Do not execute syncing after resizing parity files, before the normal post-resize content write */
{ "test-kill-after-resize", 0, 0, OPT_TEST_KILL_AFTER_RESIZE },
/* Do not execute syncing after writing the new initial content file */
{ "test-kill-before-sync", 0, 0, OPT_TEST_KILL_BEFORE_SYNC },
/* Exit with failure if after check/fix there ARE NOT unrecoverable errors. */
@ -860,6 +864,9 @@ int snapraid_main(int argc, char* argv[])
case OPT_TEST_KILL_AFTER_SYNC :
opt.kill_after_sync = 1;
break;
case OPT_TEST_KILL_AFTER_RESIZE :
opt.kill_after_resize = 1;
break;
case OPT_TEST_KILL_BEFORE_SYNC :
opt.kill_before_sync = 1;
break;

View file

@ -87,6 +87,7 @@ struct snapraid_option {
int skip_content_access; /**< Skip the content access for commands that don't need it. */
int kill_after_sync; /**< Kill the process after sync without saving the final state. */
int kill_before_sync; /**< Kill the process before sync after saving the initial state. */
int kill_after_resize; /**< Kill the process after parity resize before the normal post-resize state write. */
int force_murmur3; /**< Force Murmur3 choice. */
int force_spooky2; /**< Force Spooky2 choice. */
int force_order; /**< Force sorting order. One of the SORT_* defines. */

View file

@ -140,10 +140,14 @@
*
* 1. Scan creates CHG, REP and DELETED pending states.
* For --force-full, BLK blocks are marked REBUILD before parity resizing.
* For --force-realloc, the scan also constructs the new data-to-parity
* allocation before physical parity is resized.
*
* 2. The pending content state is durably written before parity is changed.
* 2. The pending content state and, when reallocating, the new allocation
* are durably written before physical parity is changed.
* A crash after this point therefore leaves enough information to
* identify every parity position that may have been modified or needs rebuild.
* identify every parity position that may be modified or rebuilt without
* relying on a physical layout referenced only by an older content file.
*
* 3. Sync reads current data and computes new parity. Blocks may already be
* changed to BLK/EMPTY in memory while parity writes are still pending.
@ -1966,30 +1970,36 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t
if (!skip_sync) {
/*
* Persist the full-rebuild intent before changing physical parity.
* Persist the logical state required to recover from a crash before changing
* the physical parity layout.
*
* parity_chsize() may enlarge a truncated parity file. While the parity handle
* remains open, physical_reach_size intentionally does not advance merely because
* of allocation: it is a physical read/truncation extent, not a parity-validity
* marker.
* For --force-full, BLK -> REBUILD records that the existing allocation must
* be preserved but its physical parity must be regenerated.
*
* physical_reach_size is not persisted separately. After a crash and reopen, the
* physical EOF becomes the reconstructed extent, so the previous short EOF can
* no longer be used as evidence that positions beyond it still require work.
* For --force-realloc, state_scan() has already completed the requested
* reallocation before state_sync() is entered. The in-memory content state
* therefore contains the new data-to-parity mapping, with REP/CHG/DELETED
* states describing parity that is not yet synchronized.
*
* For --force-full, that rebuild requirement therefore has to be persisted in
* the content state before resizing. BLK -> REBUILD records that physical
* parity must be regenerated independently of what EOF or physical_reach_size says.
* That post-scan state must be durable before parity_chsize() runs.
* Reallocation can move data backward and reduce the required parity size.
* If parity were truncated first and the process terminated before the new
* mapping was persisted, the old content file could still reference parity
* positions that had already been physically discarded.
*
* This ordering is about preserving rebuild intent across a crash. It is not
* needed because bytes below physical_reach_size are assumed valid: parity
* correctness is determined separately by block state and parity verification.
* The opposite crash ordering is safe: if the new content state is written
* and the process terminates before resizing or rewriting parity, its
* REP/CHG/DELETED states prevent the old physical parity from being trusted
* as synchronized with the new mapping.
*/
if (state->opt.force_full) {
if (state->opt.force_full)
state_sync_mark_rebuild(state);
if (!state->opt.skip_content_write && state->need_write)
state_write(state);
if ((state->opt.force_full || state->opt.force_realloc)
&& !state->opt.skip_content_write
&& state->need_write
) {
state_write(state);
}
msg_progress("Resizing...\n");
@ -2018,6 +2028,11 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t
state->need_write = 1;
}
if (state->opt.kill_after_resize) {
log_fatal(EUSER, "WARNING! Killing due --test-kill-after-resize option.\n");
exit(EXIT_SUCCESS);
}
/* after resizing parity files, refresh again the free info */
state_refresh(state);