Limit unsynced parity sweep to the invalid window tracked during content parsing
Some checks are pending
Build / build-mac (push) Waiting to run
Build / build-linux (push) Waiting to run
Build / build-arm32 (push) Waiting to run
Build / build-win (push) Waiting to run
Build / build-freebsd (push) Waiting to run
Build / build-musl (push) Waiting to run
CodeQL Advanced / Analyze (c-cpp) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run

This commit is contained in:
Andrea Mazzoleni 2026-10-06 10:42:03 +02:00
parent 2a60279674
commit 8fe9331f42
5 changed files with 76 additions and 32 deletions

View file

@ -1389,6 +1389,20 @@ static int extent_parity_inside_compare_unlock(const void* void_a, const void* v
return 0;
}
struct snapraid_extent* fs_par2extent_find_next(struct snapraid_disk* disk, block_off_t parity_pos)
{
struct extent_parity_inside arg = { parity_pos };
struct snapraid_extent* extent;
fs_lock(disk);
/* non-overlapping extents let this search find the containing extent or the next one */
extent = tommy_tree_search_greater_equal_compare(&disk->fs_parity, extent_parity_inside_compare_unlock, &arg);
fs_unlock(disk);
return extent;
}
/**
* Search the extent at the specified parity position.
* The search is optimized for sequential accesses.

View file

@ -1243,6 +1243,13 @@ void fs_deallocate(struct snapraid_disk* disk, block_off_t pos);
*/
struct snapraid_block* fs_file2block_get(struct snapraid_file* file, block_off_t file_pos);
/**
* Find the first extent ending after the specified parity position.
* Return 0 if no such extent exists. The caller must ensure that the extent
* trees remain unchanged while using the returned extent or its tree node.
*/
struct snapraid_extent* fs_par2extent_find_next(struct snapraid_disk* disk, block_off_t parity_pos);
/**
* Get the file position from the parity position.
* Return 0 if no file is using it.

View file

@ -2270,7 +2270,7 @@ static void test_unsynced_sweep(void)
state.block_size = 4096;
tommy_list_init(&state.disklist);
if (fs_count_unsynced(&state, 100) != 0) {
if (fs_count_unsynced(&state, 0, 100) != 0) {
log_fatal(EINTERNAL, "test_unsynced_sweep: no disks failed\n");
exit(EXIT_FAILURE);
}
@ -2281,7 +2281,7 @@ static void test_unsynced_sweep(void)
tommy_list_insert_tail(&state.disklist, &disk2->node, disk2);
/* initially both disks are empty */
if (fs_count_unsynced(&state, 100) != 0) {
if (fs_count_unsynced(&state, 10, 100) != 0) {
log_fatal(EINTERNAL, "test_unsynced_sweep: empty disks failed\n");
exit(EXIT_FAILURE);
}
@ -2301,7 +2301,7 @@ static void test_unsynced_sweep(void)
fs_allocate(disk2, 0, file2, 0, 20);
/* parity 0..14 has only BLK or EMPTY; parity 15..19 has CHG on disk1 and BLK on disk2 -> 5 unsynced */
if (fs_count_unsynced(&state, 20) != 5) {
if (fs_count_unsynced(&state, 15, 20) != 5) {
log_fatal(EINTERNAL, "test_unsynced_sweep: mixed block run failed\n");
exit(EXIT_FAILURE);
}
@ -2313,7 +2313,7 @@ static void test_unsynced_sweep(void)
fs_allocate(disk1, 20, del, 0, 3);
/* disk2 is empty at 20..22 -> DELETED alone does not count as unsynced */
if (fs_count_unsynced(&state, 23) != 5) {
if (fs_count_unsynced(&state, 15, 23) != 5) {
log_fatal(EINTERNAL, "test_unsynced_sweep: deleted without other file failed\n");
exit(EXIT_FAILURE);
}
@ -2330,7 +2330,7 @@ static void test_unsynced_sweep(void)
/* parity 20: DELETED on d1, empty on d2 -> not unsynced;
* parity 21..22: DELETED on d1, BLK on d2 -> unsynced (2 blocks). Total 5 + 2 = 7 */
if (fs_count_unsynced(&state, 23) != 7) {
if (fs_count_unsynced(&state, 15, 23) != 7) {
log_fatal(EINTERNAL, "test_unsynced_sweep: deleted with file failed\n");
exit(EXIT_FAILURE);
}
@ -2349,14 +2349,19 @@ static void test_unsynced_sweep(void)
* Positions 30..33 are all unsynced: REP, REP, CHG, REBUILD respectively.
* At 32 and 33 the invalid block changes disks at the same event boundary;
* count each position once using the refreshed flags from both disks.
* Check limits in gaps, within extents and runs, and at their boundaries.
* Check every window between limits in gaps, within extents and runs,
* and at their boundaries, including empty windows and nonzero starts.
*/
for (i = 0; i < sizeof(bounds) / sizeof(bounds[0]); ++i) {
uint64_t count = fs_count_unsynced(&state, bounds[i].blockmax);
if (count != bounds[i].unsynced) {
log_fatal(EINTERNAL, "test_unsynced_sweep: limit %" PRIu64 " expected %" PRIu64 ", got %" PRIu64 "\n",
bounds[i].blockmax, bounds[i].unsynced, count);
exit(EXIT_FAILURE);
block_off_t j;
for (j = 0; j <= i; ++j) {
uint64_t expected = bounds[i].unsynced - bounds[j].unsynced;
uint64_t count = fs_count_unsynced(&state, bounds[j].blockmax, bounds[i].blockmax);
if (count != expected) {
log_fatal(EINTERNAL, "test_unsynced_sweep: window [%" PRIu64 ", %" PRIu64 ") expected %" PRIu64 ", got %" PRIu64 "\n",
bounds[j].blockmax, bounds[i].blockmax, expected, count);
exit(EXIT_FAILURE);
}
}
}

View file

@ -2242,6 +2242,10 @@ static inline void disk_advance(
block_off_t run_rem = 1;
const unsigned char* ptr = (const unsigned char*)block;
/* stop inspecting block flags at the end of the requested window */
if (extent_rem > blockmax - pos)
extent_rem = blockmax - pos;
*has_file_ptr = has_file;
*has_invalid_ptr = has_invalid;
@ -2264,14 +2268,16 @@ static inline void disk_advance(
* block, and at least one disk has an invalid parity block (CHG, REP, REBUILD,
* or DELETED).
*
* The sweep takes O(B + R * D) time, where B is the number of blocks inspected,
* Counts only positions in [blockmin, blockmax).
* The sweep takes O(D * log(E) + B + R * D) time, where E is the number of
* extents per disk, B is the number of blocks inspected within the window,
* R is the number of event positions processed (including initialization), and
* D is the number of disks. Cached event boundaries avoid rescanning block runs.
*
* The caller must ensure that the disk list, extent trees, and block states
* remain unchanged during the count, as this function does not acquire locks.
* remain unchanged during the count, as no locks are held during the sweep.
*/
uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax)
uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmin, block_off_t blockmax)
{
tommy_tree_node* cursors[RAID_DATA_MAX];
block_off_t next_events[RAID_DATA_MAX];
@ -2280,7 +2286,7 @@ uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax)
unsigned disk_count = 0;
tommy_node* node;
uint64_t total_unsynced = 0;
block_off_t pos = 0;
block_off_t pos = blockmin;
size_t stride = block_sizeof();
unsigned d;
@ -2294,9 +2300,10 @@ uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax)
for (node = state->disklist; node != 0; node = node->next) {
struct snapraid_disk* disk = node->data;
cursors[disk_count] = tommy_tree_head(&disk->fs_parity);
struct snapraid_extent* ext = fs_par2extent_find_next(disk, blockmin);
cursors[disk_count] = ext ? &ext->parity_node : 0;
/* force an initial event so flags are set before their first use */
next_events[disk_count] = 0;
next_events[disk_count] = blockmin;
++disk_count;
}
@ -2508,6 +2515,8 @@ struct state_read_context {
static void state_read_content(struct snapraid_state* state, const char* path, STREAM* f, struct state_read_context* context)
{
block_off_t blockmax;
block_off_t inv_min;
block_off_t inv_max;
uint64_t count_file;
uint64_t count_hardlink;
uint64_t count_symlink;
@ -2523,7 +2532,6 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
int has_hash;
int has_prevhash;
int has_info;
int has_invalid_parity;
char buffer[PATH_MAX];
ssize_t ret;
tommy_array disk_mapping;
@ -2532,6 +2540,8 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
tommy_hashdyn bucket_hash;
blockmax = 0;
inv_min = BLOCK_MAX;
inv_max = 0;
count_file = 0;
count_hardlink = 0;
count_symlink = 0;
@ -2547,7 +2557,6 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
has_hash = 0;
has_prevhash = 0;
has_info = 0;
has_invalid_parity = 0;
mapping_max = 0;
memset(disk_file_count, 0, sizeof(disk_file_count));
tommy_array_init(&disk_mapping);
@ -2819,8 +2828,12 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
}
/* all file block states except BLK require a parity update */
if (v_state != BLOCK_STATE_BLK)
has_invalid_parity = 1;
if (v_state != BLOCK_STATE_BLK) {
if (v_pos < inv_min)
inv_min = v_pos;
if (v_pos + v_count > inv_max)
inv_max = v_pos + v_count;
}
v_file_pos = v_idx;
ret = state_read_block_run(f, file, v_file_pos, v_count, v_state, discard_hash);
@ -2989,14 +3002,6 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
}
}
}
/*
* A synchronized content has no invalid block state, so its unsynced
* count is certainly zero. Scan the extent maps only when needed.
*/
if (has_invalid_parity) {
count_unsynced = fs_count_unsynced(state, blockmax);
}
} else if (c == 'h') {
/* hole */
block_off_t v_pos;
@ -3049,7 +3054,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
switch (c) {
case 'o' :
/* if it's a run of deleted blocks */
has_invalid_parity = 1;
if (v_pos < inv_min)
inv_min = v_pos;
if (v_pos + v_count > inv_max)
inv_max = v_pos + v_count;
/* ensure the converted byte size fits in signed data_off_t without overflow */
if (v_count > INT64_MAX / (uint64_t)state->block_size) {
@ -4023,6 +4031,16 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
/* LCOV_EXCL_STOP */
}
/*
* Every invalid file or deleted run expands this window while parsing.
* Outside it all parity is valid, so no position can be unsynced.
* Count only after all records are loaded, independently of the info record.
* A synchronized content leaves the window empty and needs no sweep.
*/
if (inv_min < inv_max) {
count_unsynced = fs_count_unsynced(state, inv_min, inv_max);
}
context->blockmax = blockmax;
context->count_map = mapping_max;
context->count_file = count_file;

View file

@ -242,10 +242,10 @@ void state_read(struct snapraid_state* state);
/**
* Count the number of unsynced parity positions using an event sweep across extents.
* Counts positions in [0, blockmax). The caller must ensure that the disk list,
* Counts positions in [blockmin, blockmax). The caller must ensure that the disk list,
* extent trees, and block states remain unchanged for the duration of the call.
*/
uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax);
uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmin, block_off_t blockmax);
/**
* Write the new state.