From 395291bd9a23547a13c832b9f82f78dedd63bc7c Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Fri, 28 Aug 2026 12:11:05 +0200 Subject: [PATCH] Implement crash-safe --force-full sync with persistent BLOCK_STATE_REBUILD --- AGENTS.md | 1 + Makefile.am | 30 ++++++++++++++ cmdline/check.c | 10 +++-- cmdline/elem.c | 1 + cmdline/elem.h | 46 +++++++++++++++++++-- cmdline/io.c | 1 + cmdline/locate.c | 11 ++++- cmdline/scan.c | 29 ++++++++----- cmdline/snapraid.c | 20 +++++++++ cmdline/state.c | 75 ++++++++++++++++++++++++++-------- cmdline/sync.c | 100 +++++++++++++++++++++++++++++++++++++++++---- 11 files changed, 281 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0f17e69a..0c8f23cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/Makefile.am b/Makefile.am index 953425d9..c5927b4b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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* diff --git a/cmdline/check.c b/cmdline/check.c index aa52bf93..d6fb0f79 100644 --- a/cmdline/check.c +++ b/cmdline/check.c @@ -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) { diff --git a/cmdline/elem.c b/cmdline/elem.c index 0b43817d..5017d6a5 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -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 : diff --git a/cmdline/elem.h b/cmdline/elem.h index 7c6319ba..8a76c245 100644 --- a/cmdline/elem.h +++ b/cmdline/elem.h @@ -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)) { diff --git a/cmdline/io.c b/cmdline/io.c index c2f0b8e9..2393a613 100644 --- a/cmdline/io.c +++ b/cmdline/io.c @@ -1240,3 +1240,4 @@ void io_done(struct snapraid_io* io) } #endif } + diff --git a/cmdline/locate.c b/cmdline/locate.c index 0ee55276..8883f15a 100644 --- a/cmdline/locate.c +++ b/cmdline/locate.c @@ -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); } } diff --git a/cmdline/scan.c b/cmdline/scan.c index d4cd10a6..4de55a5d 100644 --- a/cmdline/scan.c +++ b/cmdline/scan.c @@ -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]; diff --git a/cmdline/snapraid.c b/cmdline/snapraid.c index 49ab7465..5adca22d 100644 --- a/cmdline/snapraid.c +++ b/cmdline/snapraid.c @@ -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"); diff --git a/cmdline/state.c b/cmdline/state.c index a45dd0da..f45640a6 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -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; diff --git a/cmdline/sync.c b/cmdline/sync.c index e5d54383..49dac003 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -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 */