Implement crash-safe --force-full sync with persistent BLOCK_STATE_REBUILD

This commit is contained in:
Andrea Mazzoleni 2026-08-28 12:11:05 +02:00
parent cea7ce7f0a
commit 395291bd9a
11 changed files with 281 additions and 43 deletions

View file

@ -68,6 +68,7 @@ The platform abstraction layer is separated into `os/`. Note that `os/` acts as
| `BLOCK_STATE_CHG` | 2 | File block changed; hash may be old (pre-sync); parity not yet updated |
| `BLOCK_STATE_REP` | 3 | File block hashed (from copy heuristic); parity not yet updated |
| `BLOCK_STATE_DELETED` | 4 | File deleted; block retains old hash for parity recovery; parity not yet zeroed |
| `BLOCK_STATE_REBUILD` | 5 | File block with valid hash; parity untrusted, rebuild in place without reallocation |
### Crash-Recovery Invariants

View file

@ -1153,6 +1153,36 @@ endif # !HAVE_MEMORY_CHECKER
mv bench/b bench/disk1/b
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
#### FORCE-FULL REBUILD TESTS ####
$(MSG) Test interrupted force-full with complete parity
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) list > bench/mapping_before.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) --force-full --test-kill-before-sync sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) list > bench/mapping_rebuild.txt
diff bench/mapping_before.txt bench/mapping_rebuild.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) list > bench/mapping_after.txt
diff bench/mapping_before.txt bench/mapping_after.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
$(MSG) Test interrupted force-full with physically truncated parity
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) list > bench/mapping_trunc_before.txt
@ orig_size=$$(wc -c < bench/parity.0) && truncate -s $$(( orig_size / 2 )) bench/parity.0
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) --force-full --test-kill-before-sync sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) list > bench/mapping_trunc_after.txt
diff bench/mapping_trunc_before.txt bench/mapping_trunc_after.txt
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
$(MSG) Test check and status on interrupted REBUILD state
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) --force-full --test-kill-before-sync sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) status
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
$(MSG) Test explicit force-realloc after REBUILD
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) --force-full --test-kill-before-sync sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) -R sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check
#### RECOVER MISSING ####
$(MSG) Delete some files, fix with -m and check with PAR1
rm bench/disk1/a/8*

View file

@ -43,6 +43,7 @@
* CHG OLD/ZERO/INVALID present may be OLD or CURRENT
* REP NEW present may be OLD or NEW
* DELETED OLD/INVALID none may be OLD or zero
* REBUILD CURRENT present parity untrusted / rebuild pending
*
* BLK has a hash for the data that should currently be present and normally
* represents a synchronized contribution.
@ -528,6 +529,7 @@ static int repair(struct snapraid_state* state, int rehash, block_off_t pos, uns
case BLOCK_STATE_CHG : desc = "change"; break;
case BLOCK_STATE_REP : desc = "replace"; break;
case BLOCK_STATE_BLK : desc = "block"; break;
case BLOCK_STATE_REBUILD : desc = "rebuild"; break;
/* LCOV_EXCL_START */
default : desc = "unknown"; break;
/* LCOV_EXCL_STOP */
@ -596,7 +598,7 @@ static int repair(struct snapraid_state* state, int rehash, block_off_t pos, uns
* A CHG hash identifies the OLD contents, so fetching data by that hash would
* not prove that we recovered the current version required by this strategy.
*/
if ((block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP)
if ((block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP || block_state == BLOCK_STATE_REBUILD)
/* try to fetch the block using the known hash */
&& (state_import_fetch(state, rehash, failed[j].block, buffer[failed[j].index]) == 0
|| state_search_fetch(state, rehash, failed[j].file, failed[j].file_pos, failed[j].block, buffer[failed[j].index]) == 0)
@ -641,7 +643,7 @@ static int repair(struct snapraid_state* state, int rehash, block_off_t pos, uns
* we have already checked their hash
*/
if (block_state != BLOCK_STATE_CHG) {
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP);
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP || block_state == BLOCK_STATE_REBUILD);
continue;
}
@ -788,7 +790,7 @@ static int repair(struct snapraid_state* state, int rehash, block_off_t pos, uns
* At this point, we can have only BLK ones
*/
assert(block_state == BLOCK_STATE_BLK);
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REBUILD);
/* we have something we are interested to recover */
something_to_recover = 1;
@ -1610,7 +1612,7 @@ static int state_check_process(struct snapraid_state* state, int fix, struct sna
continue;
}
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP);
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP || block_state == BLOCK_STATE_REBUILD);
/* compute the hash of the block just read */
if (rehash) {

View file

@ -909,6 +909,7 @@ void dealloc_import(struct snapraid_dealloc* dealloc, struct snapraid_file* file
switch (state) {
case BLOCK_STATE_BLK :
case BLOCK_STATE_REP :
case BLOCK_STATE_REBUILD :
memcpy(hash, block->hash, BLOCK_HASH_SIZE);
break;
case BLOCK_STATE_CHG :

View file

@ -162,6 +162,25 @@ struct snapraid_filter {
*/
#define BLOCK_STATE_DELETED 4
/**
* The block contains current data and a current hash, but its parity must be
* rebuilt in place.
*
* The block hash is the hash of the CURRENT data.
*
* Physical parity for this block MUST NOT be trusted. It may still be correct,
* may be missing, or may have been only partly rebuilt by an interrupted
* --force-full sync.
*
* Unlike CHG and REP, this state does not permit automatic parity
* reallocation. The existing data-to-parity mapping must be preserved.
*
* REBUILD is converted back to BLK only after the corresponding parity update
* has completed and the normal sync durability ordering allows that progress
* to be persisted.
*/
#define BLOCK_STATE_REBUILD 5
/**
* Block hash size.
*
@ -797,7 +816,9 @@ static inline int block_has_updated_hash(const struct snapraid_block* block)
{
unsigned state = block_state_get(block);
return state == BLOCK_STATE_BLK || state == BLOCK_STATE_REP;
return state == BLOCK_STATE_BLK
|| state == BLOCK_STATE_REP
|| state == BLOCK_STATE_REBUILD;
}
/**
@ -810,7 +831,9 @@ static inline int block_has_file(const struct snapraid_block* block)
unsigned state = block_state_get(block);
return state == BLOCK_STATE_BLK
|| state == BLOCK_STATE_CHG || state == BLOCK_STATE_REP;
|| state == BLOCK_STATE_CHG
|| state == BLOCK_STATE_REP
|| state == BLOCK_STATE_REBUILD;
}
/**
@ -823,7 +846,9 @@ static inline int block_has_invalid_parity(const struct snapraid_block* block)
unsigned state = block_state_get(block);
return state == BLOCK_STATE_DELETED
|| state == BLOCK_STATE_CHG || state == BLOCK_STATE_REP;
|| state == BLOCK_STATE_CHG
|| state == BLOCK_STATE_REP
|| state == BLOCK_STATE_REBUILD;
}
/**
@ -838,6 +863,21 @@ static inline int block_has_file_and_valid_parity(const struct snapraid_block* b
return state == BLOCK_STATE_BLK;
}
/**
* Check if a live block with invalid parity may participate in automatic
* parity reallocation.
*
* REBUILD is deliberately excluded because it requires an in-place parity
* rebuild preserving the existing data-to-parity mapping.
*/
static inline int block_has_reallocatable_parity(const struct snapraid_block* block)
{
unsigned state = block_state_get(block);
return state == BLOCK_STATE_CHG
|| state == BLOCK_STATE_REP;
}
static inline int file_flag_has(const struct snapraid_file* file, unsigned mask)
{
if (mask >= (1U << FILE_FLAGS_SHARED_BIT)) {

View file

@ -1240,3 +1240,4 @@ void io_done(struct snapraid_io* io)
}
#endif
}

View file

@ -242,8 +242,15 @@ void state_locate_mark_tail_blocks_for_resync(struct snapraid_state* state, uint
}
struct snapraid_block* block = fs_file2block_get(file, f);
if (block_state_get(block) == BLOCK_STATE_BLK) {
/* convert from BLK to REP */
unsigned block_state = block_state_get(block);
if (block_state == BLOCK_STATE_BLK
|| block_state == BLOCK_STATE_REBUILD
) {
/*
* Explicit force-realloc overrides an in-place REBUILD request.
* REP makes the block eligible for the normal reallocation path.
*/
block_state_set(block, BLOCK_STATE_REP);
}
}

View file

@ -359,6 +359,15 @@ static void scan_file_deallocate(struct snapraid_scan* scan, struct snapraid_fil
case BLOCK_STATE_BLK :
/* we keep the hash making it an "old" hash, because the parity is still containing data for it */
break;
case BLOCK_STATE_REBUILD :
/*
* The stored hash describes the current file contents and therefore
* becomes a valid OLD hash after deletion.
*
* Physical parity was already untrusted, but DELETED already represents
* that condition conservatively.
*/
break;
case BLOCK_STATE_CHG :
/*
* In this cases we don't know if the old state is still the one
@ -427,14 +436,14 @@ static void scan_file_delayed_allocate(struct snapraid_scan* scan, struct snapra
}
/**
* Check if a file is completely formed of blocks with invalid parity,
* Check if a file is completely formed of blocks with reallocatable parity,
* and no rehash is tagged, and if it has at least one block.
*/
static int file_is_full_invalid_parity_and_stable(struct snapraid_state* state, struct snapraid_disk* disk, struct snapraid_file* file)
static int file_is_full_reallocatable_and_stable(struct snapraid_state* state, struct snapraid_disk* disk, struct snapraid_file* file)
{
block_off_t i;
/* with no block, it never has an invalid parity */
/* with no block, it never has reallocatable parity */
if (file->blockmax == 0)
return 0;
@ -444,8 +453,8 @@ static int file_is_full_invalid_parity_and_stable(struct snapraid_state* state,
struct snapraid_block* block = fs_file2block_get(file, i);
block_off_t parity_pos;
/* exclude blocks with parity */
if (!block_has_invalid_parity(block))
/* exclude blocks without reallocatable parity */
if (!block_has_reallocatable_parity(block))
return 0;
/*
@ -703,7 +712,7 @@ static void scan_file_remove(struct snapraid_scan* scan, struct snapraid_file* f
*
* If the file is kept, nothing has to be done.
*
* But if a file contains only blocks with invalid parity, it's reallocated to ensure
* But if a file is fully reallocatable, it's reallocated to ensure
* to always minimize the space used in the parity.
*
* This could happen after a failed sync, when some other files are deleted,
@ -713,8 +722,8 @@ static void scan_file_keep(struct snapraid_scan* scan, struct snapraid_file* fil
{
struct snapraid_disk* disk = scan->disk;
/* if the file is full invalid, schedule a reinsert at later stage */
if (file_is_full_invalid_parity_and_stable(scan->state, disk, file)) {
/* if the file is fully reallocatable, schedule a reinsert at later stage */
if (file_is_full_reallocatable_and_stable(scan->state, disk, file)) {
struct snapraid_file* copy = file_dup(file);
file_flag_set(copy, FILE_IS_REALLOC_NEW);
@ -1109,8 +1118,8 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub,
*
* This is safe to execute unlocked because:
* 1. Modified files deallocations are deferred to Phase 2 (mono-threaded phase).
* 2. file_is_full_invalid_parity_and_stable() only accepts files with complete
* invalid parity, preventing them from being selected by file_is_full_hashed_and_stable().
* 2. file_is_full_reallocatable_and_stable() only accepts files with complete
* reallocatable parity, preventing them from being selected by file_is_full_hashed_and_stable().
*/
if (other_file && file_is_full_hashed_and_stable(scan->state, other_disk, other_file)) {
char path_other[PATH_MAX];

View file

@ -529,6 +529,8 @@ int snapraid_main(int argc, char* argv[])
int operation;
block_off_t blockstart;
block_off_t blockcount;
int blockstart_set;
int blockcount_set;
int ret;
tommy_list filterlist_file;
tommy_list filterlist_disk;
@ -567,6 +569,8 @@ int snapraid_main(int argc, char* argv[])
opt.io_error_limit = 100;
blockstart = 0;
blockcount = 0;
blockstart_set = 0;
blockcount_set = 0;
tommy_list_init(&filterlist_file);
tommy_list_init(&filterlist_disk);
speed_test_period = -1;
@ -690,6 +694,7 @@ int snapraid_main(int argc, char* argv[])
}
break;
case 'S' :
blockstart_set = 1;
blockstart = strtoull(optarg, &e, 0);
if (e == optarg || *e) {
/* LCOV_EXCL_START */
@ -699,6 +704,7 @@ int snapraid_main(int argc, char* argv[])
}
break;
case 'B' :
blockcount_set = 1;
blockcount = strtoull(optarg, &e, 0);
if (e == optarg || *e) {
/* LCOV_EXCL_START */
@ -1220,6 +1226,20 @@ int snapraid_main(int argc, char* argv[])
/* LCOV_EXCL_STOP */
}
if (opt.force_full && blockstart_set) {
/* LCOV_EXCL_START */
log_fatal(EUSER, "You cannot use -S, --start with --force-full\n");
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
if (opt.force_full && blockcount_set) {
/* LCOV_EXCL_START */
log_fatal(EUSER, "You cannot use -B, --count with --force-full\n");
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
if (opt.prehash && opt.force_nocopy) {
/* LCOV_EXCL_START */
log_fatal(EUSER, "You cannot use the -h, --pre-hash and -N, --force-nocopy options simultaneously\n");

View file

@ -2275,7 +2275,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
* - SNAPCNT3/SnapRAID 11.0 Adds entry 'y' for hash size.
* - SNAPCNT3/SnapRAID 11.0 Adds entry 'Q' for multi parity file.
* The previous 'P' entry is now deprecated, but supported for importing.
* - SNAPCNT4/SnapRAID 15.0 Adds entry 'd' for dealloc file.
* - SNAPCNT4 adds:
* - entry 'd' for deallocated files;
* - block type 'r' / BLOCK_STATE_REBUILD for persistent in-place parity
* rebuilds.
*/
if (memcmp(buffer, "SNAPCNT1\n\3\0\0", 12) != 0
&& memcmp(buffer, "SNAPCNT2\n\3\0\0", 12) != 0
@ -2413,7 +2416,7 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
}
/* allocate the file */
file = file_alloc(state->block_size, sub, v_size, v_mtime_sec, v_mtime_nsec, v_inode, 0);
file = file_alloc(state->block_size, sub, v_size, v_mtime_sec, v_mtime_nsec, v_inode, FILEPHY_UNREAD_OFFSET);
/* insert the file in the file containers */
if (file->inode != INODE_INVALID)
@ -2471,6 +2474,9 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
case 'b' :
block_state_set(block, BLOCK_STATE_BLK);
break;
case 'r' :
block_state_set(block, BLOCK_STATE_REBUILD);
break;
case 'n' :
/* deprecated NEW blocks are converted to CHG ones */
block_state_set(block, BLOCK_STATE_CHG);
@ -3703,12 +3709,37 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
tommy_hashdyn_done(&bucket_hash);
}
static int state_has_rbuild(struct snapraid_state* state)
{
tommy_node* i;
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
tommy_node* j;
for (j = disk->filelist; j != 0; j = j->next) {
struct snapraid_file* file = j->data;
block_off_t f;
for (f = 0; f < file->blockmax; ++f) {
struct snapraid_block* block = fs_file2block_get(file, f);
if (block_state_get(block) == BLOCK_STATE_REBUILD)
return 1;
}
}
}
return 0;
}
struct state_write_thread_context {
struct snapraid_state* state;
#if HAVE_MT_WRITE
thread_id_t thread;
#endif
/* input */
int content_version;
block_off_t blockmax;
time_t info_oldest;
time_t info_now;
@ -3763,22 +3794,11 @@ static void* state_write_thread(void* arg)
count_unscrubbed = 0;
tommy_hashdyn_init(&bucket_hash);
/*
* Force at least version 3 a we want to always store the parity size
*
* If there is a dealloc list, force version 4
*/
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
if (!tommy_list_empty(&disk->dealloclist))
break;
}
/* write header */
if (i == 0)
swrite("SNAPCNT3\n\3\0\0", 12, f);
else
if (context->content_version >= 4)
swrite("SNAPCNT4\n\3\0\0", 12, f);
else
swrite("SNAPCNT3\n\3\0\0", 12, f);
/* write block size and block max */
sputc('z', f);
@ -4010,6 +4030,9 @@ static void* state_write_thread(void* arg)
case BLOCK_STATE_BLK :
sputc('b', f);
break;
case BLOCK_STATE_REBUILD :
sputc('r', f);
break;
case BLOCK_STATE_CHG :
sputc('g', f);
break;
@ -4443,6 +4466,24 @@ static void state_write_content(struct snapraid_state* state, uint32_t* out_crc)
}
}
int content_version = 3;
/*
* Force at least version 3 as we want to always store the parity size.
* If there is a REBUILD block or a dealloc list, force version 4.
*/
if (state_has_rbuild(state)) {
content_version = 4;
} else {
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
if (!tommy_list_empty(&disk->dealloclist)) {
content_version = 4;
break;
}
}
}
#if HAVE_MT_WRITE
/* start all writing threads */
first = 1;
@ -4482,6 +4523,7 @@ static void state_write_content(struct snapraid_state* state, uint32_t* out_crc)
/* initialize */
context->state = state;
context->content_version = content_version;
context->blockmax = blockmax;
context->info_oldest = info_oldest;
context->info_now = info_now;
@ -4636,6 +4678,7 @@ static void state_write_content(struct snapraid_state* state, uint32_t* out_crc)
/* initialize */
context->state = state;
context->content_version = content_version;
context->blockmax = blockmax;
context->info_oldest = info_oldest;
context->info_now = info_now;

View file

@ -62,12 +62,14 @@
* REP yes NEW invalid for current data
* BLK yes CURRENT synchronized contribution
* DELETED no OLD / INVALID invalid for logical zero
* REBUILD yes CURRENT physical parity untrusted, rebuild in place
*
* "Invalid" is conservative. It does not necessarily mean that the bytes in
* parity are wrong. After an interrupted sync parity may still represent OLD
* data, may already represent NEW data, or may have been only partly updated.
* CHG, REP and DELETED mean that no particular physical parity state may be
* assumed.
* assumed. REBUILD means current data and hash are known, but physical parity
* must be regenerated using the existing data-to-parity mapping.
*
* EMPTY contributes zero and does not by itself require an update. Therefore
* a parity position containing only EMPTY and DELETED blocks may be skipped:
@ -76,7 +78,7 @@
*
* BLK is a per-data-block property. It means that this block does not itself
* require a parity update. It does not imply that the whole parity stripe is
* valid if another disk at the same position contains CHG, REP or DELETED.
* valid if another disk at the same position contains CHG, REP, DELETED or REBUILD.
*
* The main state transitions are:
*
@ -87,9 +89,13 @@
* EMPTY -> REP NEW represents the previous zero input
* DELETED -> REP NEW unknown and not trusted
*
* BLK -> REBUILD CURRENT parity is deliberately no longer trusted
* REBUILD -> BLK CURRENT parity rebuilt and progress committed
*
* BLK -> DELETED OLD initially still represents OLD data
* CHG -> DELETED unchanged already unknown
* REP -> DELETED INVALID already unknown
* REBUILD -> DELETED OLD untrusted parity, CURRENT hash becomes OLD
*
* REP -> CHG INVALID unchanged and not trusted
* CHG -> REP NEW unchanged and not trusted
@ -102,6 +108,11 @@
* by the filesystem scan before sync starts modifying parity. REP -> CHG is
* also used when a previously stored copy-derived hash is invalidated.
*
* BLK -> REBUILD is produced when --force-full sync is requested to make full
* parity reconstruction persistent across crashes before touching physical parity.
* BLK -> REBUILD does not itself change physical parity; it changes what SnapRAID
* is willing to trust.
*
* CHG -> REP is performed by the optional prehash pass. It means that NEW data
* has been read and hashed, while parity remains explicitly invalid.
*
@ -121,17 +132,18 @@
* -----------------------
*
* State changes made by state_sync_process() are not immediate durability
* guarantees. CHG/REP may become BLK, and DELETED may become EMPTY, in memory
* guarantees. CHG/REP/REBUILD may become BLK, and DELETED may become EMPTY, in memory
* before the corresponding asynchronous parity write has completed.
*
* This is safe because completed state is not published to the content file
* until parity has crossed a durability barrier. The ordering is:
*
* 1. Scan creates CHG, REP and DELETED pending states.
* For --force-full, BLK blocks are marked REBUILD before parity resizing.
*
* 2. The pending content state is durably written before parity is changed.
* A crash after this point therefore leaves enough information to
* identify every parity position that may have been modified.
* identify every parity position that may have been modified or needs rebuild.
*
* 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.
@ -145,7 +157,13 @@
* The persistent crash invariant is therefore:
*
* CHG / REP / DELETED in content
* => physical parity must be treated as unknown.
* => physical parity must be treated according to the existing interrupted
* sync semantics.
*
* REBUILD in content
* => physical parity must not be trusted
* => parity must be recomputed
* => existing data-to-parity allocation must be preserved
*
* BLK / EMPTY progress published by sync
* => the parity required by that progress was made durable before the
@ -1794,6 +1812,44 @@ bail:
return 0;
}
/*
* Make a forced full parity rebuild persistent.
*
* REBUILD means that the current data and hash are known, but physical parity
* must be regenerated using the existing data-to-parity mapping.
*
* Only BLK blocks are converted. CHG, REP and DELETED already carry their own
* unsynchronized semantics and must not be rewritten merely because
* --force-full was requested.
*/
static void state_sync_mark_rebuild(struct snapraid_state* state)
{
tommy_node* i;
int modified = 0;
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
tommy_node* j;
for (j = disk->filelist; j != 0; j = j->next) {
struct snapraid_file* file = j->data;
block_off_t f;
for (f = 0; f < file->blockmax; ++f) {
struct snapraid_block* block = fs_file2block_get(file, f);
if (block_state_get(block) == BLOCK_STATE_BLK) {
block_state_set(block, BLOCK_STATE_REBUILD);
modified = 1;
}
}
}
}
if (modified)
state->need_write = 1;
}
int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t blockcount)
{
block_off_t blockmax;
@ -1826,9 +1882,6 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t
used_paritymax = parity_used_size(state);
used_parity_size = used_paritymax * (data_off_t)state->block_size;
/* effective size of the parity files */
file_parity_size = 0;
if (blockstart > blockmax) {
/* LCOV_EXCL_START */
log_fatal(EUSER, "Error in the starting block %" PRIu64 ". It is larger than the parity size %" PRIu64 ".\n", blockstart, blockmax);
@ -1840,6 +1893,10 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t
if (blockcount != 0 && blockcount < blockmax - blockstart) {
blockmax = blockstart + blockcount;
}
/* effective size of the parity files */
file_parity_size = 0;
for (l = 0; l < state->level; ++l) {
data_off_t out_size;
block_off_t parityblocks;
@ -1908,6 +1965,33 @@ 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.
*
* 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.
*
* 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-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.
*
* 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.
*/
if (state->opt.force_full) {
state_sync_mark_rebuild(state);
if (!state->opt.skip_content_write && state->need_write)
state_write(state);
}
msg_progress("Resizing...\n");
/* now change the size of all parities */