diff --git a/AGENTS.md b/AGENTS.md index 0c8f23cb..1a8ecc0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,7 @@ SnapRAID is designed to recover from an interruption, including `SIGKILL`, at an - Never run `make check` because it's too expansive - Run `./snapraid test` for the regression test +- The `test` and `selftest` suites (in `cmdline/selftest.c` and `./snapraid test`) must be strictly memory-only without creating files or performing filesystem I/O - If running the speed benchmark (`./snapraid -T`), always use `--speed-test-period 100` (e.g. `./snapraid -T --speed-test-period 100`) to run it faster - The Windows build needs to be tested only when modifying Windows-specific code - Verify error paths and recovery scenarios (the test suite covers aborted sync, UUID changes, disk failures) diff --git a/cmdline/check.c b/cmdline/check.c index d6fb0f79..f9b08ca4 100644 --- a/cmdline/check.c +++ b/cmdline/check.c @@ -2534,7 +2534,18 @@ int state_check(struct snapraid_state* state, int fix, block_off_t blockstart, b /* LCOV_EXCL_STOP */ } - ret = parity_chsize(parity_ptr[l], &state->parity[l], 0, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder); + /* + * Fix must preserve the split layout stored in the content file. + * + * Unlike sync, fix does not persist parity split boundary changes. Allowing a + * partially restored elastic split to become shorter would move the remaining + * logical parity into following splits, and repaired parity could then be + * written using a transient mapping that is lost on the next invocation. + * + * An exception is made for new parity levels not yet present in the content + * file (PARITY_SIZE_INVALID), which are being created from scratch. + */ + ret = parity_chsize(parity_ptr[l], &state->parity[l], 0, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder, state->parity[l].split_map[0].size == PARITY_SIZE_INVALID); if (ret == -1) { /* LCOV_EXCL_START */ log_tag("parity_%s:%u:%s: Create error. %s.\n", es(errno), 0, lev_config_name(l), strerror(errno)); diff --git a/cmdline/parity.c b/cmdline/parity.c index 20fe844f..f2f2e559 100644 --- a/cmdline/parity.c +++ b/cmdline/parity.c @@ -594,7 +594,7 @@ static int parity_split_is_fixed(struct snapraid_parity_handle* handle, unsigned return 1; } -int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder) +int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder, int allow_split_realloc) { int ret; unsigned s; @@ -655,7 +655,18 @@ int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* log_fatal(errno, "Unexpected over resizing parity file '%s' to size %" PRIu64 " resulting in size %" PRIu64 ".\n", split->path, run, (uint64_t)split->st.st_size); return -1; /* LCOV_EXCL_STOP */ - } else if (is_fixed && split->st.st_size < run) { + } else if ((is_fixed || !allow_split_realloc) && split->st.st_size < run) { + /* + * A fixed split must always reach its logical boundary because changing + * it would shift the logical offsets of data already present in following + * splits. + * + * Some callers also require the existing split layout to remain unchanged + * even when this split would normally be elastic. In particular, fix does + * not persist a changed split mapping. Accepting a partial allocation here + * would make the remaining size move into a following split and fix could + * then write parity using a transient layout that is lost on the next run. + */ /* LCOV_EXCL_START */ errno = ENXIO; log_fatal(errno, "Failed restoring parity file '%s' to size %" PRIu64 " resulting in size %" PRIu64 ".\n", split->path, run, (uint64_t)split->st.st_size); diff --git a/cmdline/parity.h b/cmdline/parity.h index 439ab9e5..cda3ede0 100644 --- a/cmdline/parity.h +++ b/cmdline/parity.h @@ -93,9 +93,16 @@ int parity_create(struct snapraid_parity_handle* handle, const struct snapraid_p /** * Change the parity size. - * \param out_size Return the size of the parity file. The out_size is set also on error to reflect a partial resize. + * + * If allow_split_realloc is nonzero, an elastic split that cannot reach the + * requested size may keep the size actually allocated and leave the remaining + * space to following splits. + * + * If allow_split_realloc is zero, a partial allocation must fail instead of + * moving a logical split boundary. This is required by callers that cannot + * persist a changed split layout. */ -int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder); +int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder, int allow_split_realloc); /** * Get the size of the parity. diff --git a/cmdline/sync.c b/cmdline/sync.c index d349c524..ecedd552 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -2040,11 +2040,10 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t int is_modified; /* - * Change the size of the parity file, truncating or extending it - * from this point all the DELETED blocks after the end of the parity are invalid - * and they are automatically removed when we save the new content file + * Sync may reallocate an elastic split into following splits. Any resulting + * logical split mapping is persisted by the sync state-update protocol. */ - ret = parity_chsize(&parity_handle[l], &state->parity[l], &is_modified, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder); + ret = parity_chsize(&parity_handle[l], &state->parity[l], &is_modified, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder, 1); if (ret == -1) { /* LCOV_EXCL_START */ data_off_t out_size;