Count unsynced blocks using an extent event sweep instead of per-block lookups

This commit is contained in:
Andrea Mazzoleni 2026-10-03 20:31:41 +02:00
parent 3d7a722ab2
commit 0d2f466b11
3 changed files with 264 additions and 24 deletions

View file

@ -2245,6 +2245,130 @@ static void test_extent_run(void)
file_free(file);
}
static void test_unsynced_sweep(void)
{
struct snapraid_state state;
struct snapraid_disk* disk1;
struct snapraid_disk* disk2;
struct snapraid_file* file1;
struct snapraid_file* file2;
struct snapraid_file* file3;
struct snapraid_file* file4;
struct snapraid_file* del;
block_off_t i;
static const struct {
block_off_t blockmax;
uint64_t unsynced;
} bounds[] = {
{ 0, 0 }, { 10, 0 }, { 15, 0 }, { 16, 1 }, { 19, 4 },
{ 20, 5 }, { 21, 5 }, { 22, 6 }, { 23, 7 }, { 29, 7 },
{ 30, 7 }, { 31, 8 }, { 32, 9 }, { 33, 10 }, { 34, 11 },
{ 100, 11 }
};
memset(&state, 0, sizeof(state));
state.block_size = 4096;
tommy_list_init(&state.disklist);
if (fs_count_unsynced(&state, 100) != 0) {
log_fatal(EINTERNAL, "test_unsynced_sweep: no disks failed\n");
exit(EXIT_FAILURE);
}
disk1 = disk_alloc("d1", "", 0, "", 0);
disk2 = disk_alloc("d2", "", 0, "", 0);
tommy_list_insert_tail(&state.disklist, &disk1->node, disk1);
tommy_list_insert_tail(&state.disklist, &disk2->node, disk2);
/* initially both disks are empty */
if (fs_count_unsynced(&state, 100) != 0) {
log_fatal(EINTERNAL, "test_unsynced_sweep: empty disks failed\n");
exit(EXIT_FAILURE);
}
/* file1 on disk1: 10 blocks at parity 10..19 (first 5 BLK, next 5 CHG) */
file1 = file_alloc(4096, "f1", 10 * 4096, 0, 0, 0);
for (i = 0; i < 5; ++i)
block_state_set(file_block(file1, i), BLOCK_STATE_BLK);
for (i = 5; i < 10; ++i)
block_state_set(file_block(file1, i), BLOCK_STATE_CHG);
fs_allocate(disk1, 10, file1, 0, 10);
/* file2 on disk2: 20 blocks at parity 0..19 (all BLK) */
file2 = file_alloc(4096, "f2", 20 * 4096, 0, 0, 0);
for (i = 0; i < 20; ++i)
block_state_set(file_block(file2, i), BLOCK_STATE_BLK);
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) {
log_fatal(EINTERNAL, "test_unsynced_sweep: mixed block run failed\n");
exit(EXIT_FAILURE);
}
/* deleted run on disk1: 3 blocks at parity 20..22 */
del = file_alloc(4096, "<deleted>", 3 * 4096, 0, 0, 0);
for (i = 0; i < 3; ++i)
block_state_set(file_block(del, i), BLOCK_STATE_DELETED);
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) {
log_fatal(EINTERNAL, "test_unsynced_sweep: deleted without other file failed\n");
exit(EXIT_FAILURE);
}
/* file3 on disk2: first 2 blocks at parity 21..22 (BLK) */
file3 = file_alloc(4096, "f3", 6 * 4096, 0, 0, 0);
for (i = 0; i < 2; ++i)
block_state_set(file_block(file3, i), BLOCK_STATE_BLK);
block_state_set(file_block(file3, 2), BLOCK_STATE_REP);
block_state_set(file_block(file3, 3), BLOCK_STATE_REP);
block_state_set(file_block(file3, 4), BLOCK_STATE_BLK);
block_state_set(file_block(file3, 5), BLOCK_STATE_REBUILD);
fs_allocate(disk2, 21, file3, 0, 2);
/* 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) {
log_fatal(EINTERNAL, "test_unsynced_sweep: deleted with file failed\n");
exit(EXIT_FAILURE);
}
/* the second extent of file3 starts at a nonzero file offset after a gap */
fs_allocate(disk2, 30, file3, 2, 4);
/* file4 shares extent boundaries with file3 and changes flags at 32 and 33 */
file4 = file_alloc(4096, "f4", 4 * 4096, 0, 0, 0);
for (i = 0; i < 4; ++i)
block_state_set(file_block(file4, i), BLOCK_STATE_BLK);
block_state_set(file_block(file4, 2), BLOCK_STATE_CHG);
fs_allocate(disk1, 30, file4, 0, 4);
/**
* 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.
*/
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);
}
}
file_free(file1);
file_free(file2);
file_free(file3);
file_free(file4);
file_free(del);
disk_free(disk1);
disk_free(disk2);
}
static void test_raid(void)
{
/* vandermonde raid parity generation with 32 data disks */
@ -2965,6 +3089,9 @@ void test(int argc, char* argv[])
/* extent-aware file-to-parity run lookup */
test_extent_run();
/* extent sweep for unsynced parity blocks */
test_unsynced_sweep();
/* wildcard and path pattern matching */
test_wnmatch();

View file

@ -2197,31 +2197,139 @@ static inline int fs_is_block_deleted(struct snapraid_disk* disk, block_off_t po
}
/**
* Check if a block parity needs sync
*
* This is the same logic used in "status" to detect an incomplete "sync",
* that ignores invalid block, if they are not used by a file in any disk.
*
* This means that DELETED blocks won't necessarily imply an invalid parity.
* Advance a disk cursor to pos and cache its flags and next event boundary.
* Events occur at extent boundaries and changes in block flags within an extent.
*/
static int fs_is_block_unsynced(struct snapraid_state* state, block_off_t pos)
static inline void disk_advance(
tommy_tree_node** cursor_ptr,
block_off_t pos,
block_off_t blockmax,
int* has_file_ptr,
int* has_invalid_ptr,
block_off_t* next_event_ptr,
size_t stride
)
{
int one_invalid = 0;
int one_valid = 0;
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
struct snapraid_block* block = fs_par2block_find(disk, pos);
if (block_has_file(block))
one_valid = 1;
if (block_has_invalid_parity(block))
one_invalid = 1;
if (one_invalid && one_valid)
return 1;
/* advance cursor if the current extent ends at or before pos */
while (*cursor_ptr != 0) {
struct snapraid_extent* ext = (*cursor_ptr)->data;
if (ext->parity_pos + ext->count > pos)
break;
*cursor_ptr = tommy_tree_next(*cursor_ptr);
}
return 0;
if (*cursor_ptr == 0) {
/* no more extents on this disk */
*has_file_ptr = 0;
*has_invalid_ptr = 0;
*next_event_ptr = blockmax;
return;
}
struct snapraid_extent* ext = (*cursor_ptr)->data;
if (ext->parity_pos > pos) {
/* in a gap before the next extent */
*has_file_ptr = 0;
*has_invalid_ptr = 0;
*next_event_ptr = ext->parity_pos;
} else {
/* pos is inside this extent */
block_off_t extent_rem = ext->parity_pos + ext->count - pos;
block_off_t file_pos = ext->file_pos + (pos - ext->parity_pos);
struct snapraid_block* block = file_block(ext->file, file_pos);
int has_file = block_has_file(block);
int has_invalid = block_has_invalid_parity(block);
block_off_t run_rem = 1;
const unsigned char* ptr = (const unsigned char*)block;
*has_file_ptr = has_file;
*has_invalid_ptr = has_invalid;
/* find how many consecutive blocks on this disk share the same flags */
while (run_rem < extent_rem) {
const struct snapraid_block* next_b = (const struct snapraid_block*)(ptr + run_rem * stride);
if (block_has_file(next_b) != has_file || block_has_invalid_parity(next_b) != has_invalid)
break;
++run_rem;
}
*next_event_ptr = pos + run_rem;
}
}
/**
* Count the number of unsynced parity positions using an event sweep across extents.
*
* An unsynced parity position is one where at least one disk has a live file
* 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,
* 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.
*/
uint64_t fs_count_unsynced(struct snapraid_state* state, block_off_t blockmax)
{
tommy_tree_node* cursors[RAID_DATA_MAX];
block_off_t next_events[RAID_DATA_MAX];
int disk_has_file[RAID_DATA_MAX];
int disk_has_invalid[RAID_DATA_MAX];
unsigned disk_count = 0;
tommy_node* node;
uint64_t total_unsynced = 0;
block_off_t pos = 0;
size_t stride = block_sizeof();
unsigned d;
/* state_map() validates the disk limit only after content loading */
if (tommy_list_count(&state->disklist) > RAID_DATA_MAX) {
/* LCOV_EXCL_START */
log_fatal(ESOFT, "Too many data disks. Maximum allowed is %u.\n", RAID_DATA_MAX);
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
for (node = state->disklist; node != 0; node = node->next) {
struct snapraid_disk* disk = node->data;
cursors[disk_count] = tommy_tree_head(&disk->fs_parity);
/* force an initial event so flags are set before their first use */
next_events[disk_count] = 0;
++disk_count;
}
while (pos < blockmax) {
block_off_t next_pos = blockmax;
int one_file = 0;
int one_invalid = 0;
for (d = 0; d < disk_count; ++d) {
/* refresh expired runs before counting the interval starting at pos */
if (next_events[d] == pos) {
disk_advance(
&cursors[d], pos, blockmax,
&disk_has_file[d], &disk_has_invalid[d],
&next_events[d], stride
);
}
if (disk_has_file[d])
one_file = 1;
if (disk_has_invalid[d])
one_invalid = 1;
if (next_events[d] < next_pos)
next_pos = next_events[d];
}
if (one_file && one_invalid)
total_unsynced += (next_pos - pos);
pos = next_pos;
}
return total_unsynced;
}
/**
@ -2880,9 +2988,7 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
* count is certainly zero. Scan the extent maps only when needed.
*/
if (has_invalid_parity) {
for (v_pos = 0; v_pos < blockmax; ++v_pos)
if (fs_is_block_unsynced(state, v_pos))
++count_unsynced;
count_unsynced = fs_count_unsynced(state, blockmax);
}
} else if (c == 'h') {
/* hole */

View file

@ -240,6 +240,13 @@ void state_probe(struct snapraid_state* state);
*/
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,
* 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);
/**
* Write the new state.
*/