mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 10:51:54 -04:00
Count unsynced blocks using an extent event sweep instead of per-block lookups
This commit is contained in:
parent
3d7a722ab2
commit
0d2f466b11
3 changed files with 264 additions and 24 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
154
cmdline/state.c
154
cmdline/state.c
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue