mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 19:01:57 -04:00
Implement crash-safe --force-full sync with persistent BLOCK_STATE_REBUILD
This commit is contained in:
parent
cea7ce7f0a
commit
395291bd9a
11 changed files with 281 additions and 43 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
30
Makefile.am
30
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*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 :
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -1240,3 +1240,4 @@ void io_done(struct snapraid_io* io)
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
100
cmdline/sync.c
100
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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue