From 62ae6c3552d8eb720f0216f7c1202f0ba1b40421 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Wed, 9 Sep 2026 09:58:56 +0200 Subject: [PATCH] Replace physical reach tracking with the natural file and parity EOF --- cmdline/check.c | 41 +++---- cmdline/handle.c | 24 ++-- cmdline/handle.h | 13 +- cmdline/parity.c | 291 +++++++++++++++++++++------------------------ cmdline/parity.h | 72 ++++------- cmdline/selftest.c | 8 +- cmdline/sync.c | 24 ++-- 7 files changed, 195 insertions(+), 278 deletions(-) diff --git a/cmdline/check.c b/cmdline/check.c index bd98c622..6ad02b9c 100644 --- a/cmdline/check.c +++ b/cmdline/check.c @@ -2442,9 +2442,8 @@ static int state_check_process(struct snapraid_state* state, int fix, struct sna * If you check/fix after a partial sync, you do not want to fix parity * for blocks that are going to have it computed in the sync completion. * - * For unused parity there is no need to write it, because when fixing - * we already have allocated space for it on parity file creation, - * and its content doesn't matter. + * For unused parity there is no need to write or allocate it because no live + * file depends on that position and its physical contents do not matter. */ if (used_parity && valid_parity) { /* update the parity */ @@ -2456,7 +2455,7 @@ static int state_check_process(struct snapraid_state* state, int fix, struct sna /* and the parity is not excluded */ && !state->parity[l].is_excluded_by_filter ) { - ret = parity_write(parity[l], i, buffer[diskmax + l], state->block_size); + ret = parity_write(parity[l], i, buffer[diskmax + l], state->block_size, state->opt.skip_fallocate); if (ret == -1) { /* LCOV_EXCL_START */ log_tag("%s:%" PRIu64 ":%s: Write error. %s.\n", es(errno), i, lev_config_name(l), strerror(errno)); @@ -3148,17 +3147,21 @@ int state_check(struct snapraid_state* state, int fix, block_off_t blockstart, b } /* - * Fix must preserve the split layout stored in the content file. + * Fix restores the physical state described by 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. + * For an existing parity level, preserve every persisted split boundary + * while limiting the physical layout to the parity extent actually required + * by the content. Preceding splits needed to reach an existing later split + * are restored immediately; the remaining tail grows lazily as repaired + * parity blocks are written. * - * An exception is made for new parity levels not yet present in the content - * file (PARITY_SIZE_INVALID), which are being created from scratch. + * A new parity level has no persisted layout yet (PARITY_SIZE_INVALID), so + * its split layout must instead be allocated 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 (state->parity[l].split_map[0].size == PARITY_SIZE_INVALID) + ret = parity_chsize(parity_ptr[l], &state->parity[l], 0, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder); + else + ret = parity_restore(parity_ptr[l], size, state->block_size, state->opt.skip_fallocate); 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)); @@ -3211,20 +3214,6 @@ int state_check(struct snapraid_state* state, int fix, block_off_t blockstart, b /* try to close only if opened */ for (l = 0; l < state->level; ++l) { if (parity_ptr[l]) { - /* if fixing and not excluded, truncate parity to physical_reach_size */ - if (fix && !state->parity[l].is_excluded_by_filter) { - ret = parity_truncate(parity_ptr[l]); - if (ret == -1) { - /* LCOV_EXCL_START */ - log_tag("parity_%s:%" PRIu64 ":%s: Truncate error. %s.\n", es(errno), blockmax, lev_config_name(l), strerror(errno)); - log_fatal_errno(errno, lev_config_name(l)); - - ++process_error; - /* continue, as we are already exiting */ - /* LCOV_EXCL_STOP */ - } - } - ret = parity_close(parity_ptr[l]); if (ret == -1) { /* LCOV_EXCL_START */ diff --git a/cmdline/handle.c b/cmdline/handle.c index 7c9a368f..a9620ff5 100644 --- a/cmdline/handle.c +++ b/cmdline/handle.c @@ -129,9 +129,6 @@ int handle_create(struct snapraid_handle* handle, struct snapraid_file* file, in /* LCOV_EXCL_STOP */ } - /* get the size of the existing data */ - handle->physical_reach_size = handle->st.st_size; - ret = advise_open(&handle->advise, handle->f); if (ret != 0) { /* LCOV_EXCL_START */ @@ -259,8 +256,8 @@ int handle_truncate(struct snapraid_handle* handle, struct snapraid_file* file) /* LCOV_EXCL_STOP */ } - /* adjust the size to the truncated size */ - handle->physical_reach_size = file->size; + /* keep the cached EOF current; the other stat fields remain the open snapshot */ + handle->st.st_size = file->size; return 0; } @@ -318,9 +315,6 @@ int handle_open(struct snapraid_handle* handle, struct snapraid_file* file, int /* LCOV_EXCL_STOP */ } - /* get the size of the existing data */ - handle->physical_reach_size = handle->st.st_size; - ret = advise_open(&handle->advise, handle->f); if (ret != 0) { /* LCOV_EXCL_START */ @@ -349,7 +343,6 @@ int handle_close(struct snapraid_handle* handle) /* invalidate for error */ handle->file = 0; handle->f = -1; - handle->physical_reach_size = 0; return -1; /* LCOV_EXCL_STOP */ } @@ -358,7 +351,6 @@ int handle_close(struct snapraid_handle* handle) /* reset the descriptor */ handle->file = 0; handle->f = -1; - handle->physical_reach_size = 0; handle->readonly_errno = 0; return 0; @@ -378,9 +370,9 @@ ssize_t handle_read(struct snapraid_handle* handle, block_off_t file_pos, unsign out_missing = log_error; /* check if we are going to read only not initialized data */ - if (offset >= handle->physical_reach_size) { + if (offset >= handle->st.st_size) { /* if the file is missing, it's at 0 size, or it's rebuilt while reading */ - if (offset == handle->physical_reach_size || handle->physical_reach_size == 0) { + if (offset == handle->st.st_size || handle->st.st_size == 0) { errno = ENOENT; if (offset == 0) { out_missing(errno, "Missing file '%s'.\n", handle->path); @@ -482,10 +474,9 @@ int handle_write(struct snapraid_handle* handle, block_off_t file_pos, unsigned count += write_ret; } while (count < write_size); - /* adjust the size of the written/retained data */ - if (handle->physical_reach_size < offset + write_size) { - handle->physical_reach_size = offset + write_size; - } + /* keep the cached EOF current; the other stat fields remain the open snapshot */ + if (handle->st.st_size < offset + write_size) + handle->st.st_size = offset + write_size; ret = advise_write(&handle->advise, handle->f, offset, block_size); if (ret != 0) { @@ -551,7 +542,6 @@ struct snapraid_handle* handle_mapping(struct snapraid_state* state, unsigned* h handle[j].disk = 0; handle[j].file = 0; handle[j].f = -1; - handle[j].physical_reach_size = 0; handle[j].readonly_errno = 0; handle[j].bw = 0; } diff --git a/cmdline/handle.h b/cmdline/handle.h index 6b3b675b..4a6e5ac0 100644 --- a/cmdline/handle.h +++ b/cmdline/handle.h @@ -16,19 +16,8 @@ struct snapraid_handle { struct snapraid_disk* disk; /**< Disk of the file. */ struct snapraid_file* file; /**< File opened. When the file is closed, it's set to 0. */ int f; /**< Handle of the file. */ - struct stat st; /**< Stat info of the opened file. */ + struct stat st; /**< Stat info of the opened file. st_size tracks the current EOF after recovery writes. */ struct advise_struct advise; /**< Advise information. */ - - /** - * High-water mark size of the written/retained data. - * - * High-water size does not indicate that data below this offset is valid; - * it only marks the physical boundary beyond which data is known to be invalid - * and can be discarded upon truncation. Data within the physical_reach_size may - * or may not be valid. Final verification is determined independently by - * block states and hashes. - */ - data_off_t physical_reach_size; int created; /**< If the file was created, otherwise it was already existing. */ int readonly_errno; /**< Non-zero if opened read-only as fallback. */ struct snapraid_bw* bw; /**< Context for bandwidth limiting. */ diff --git a/cmdline/parity.c b/cmdline/parity.c index b509e837..27521b2d 100644 --- a/cmdline/parity.c +++ b/cmdline/parity.c @@ -127,29 +127,24 @@ void parity_size(struct snapraid_parity_handle* handle, data_off_t* out_size) *out_size = size; } -void parity_physical_reach_size(struct snapraid_parity_handle* handle, data_off_t* out_size) +void parity_physical_size(struct snapraid_parity_handle* handle, data_off_t* out_size) { unsigned s; data_off_t size; /* - * Compute the physical reach of the logical parity layout across splits. + * Compute the physical size of the logical parity layout across splits. * - * physical_reach_size represents physical file presence, not parity validity - * or contiguity. Invalid, stale, unwritten, or sparse stripes may exist below - * this boundary, for example when fix cannot recover a stripe but continues - * with later ones. Such holes are allowed and need not be represented in the - * content state. - * - * For split parity, splits concatenate linearly. If an earlier split is - * physically shorter than its logical size, logical offsets beyond that - * missing region cannot be addressed, so subsequent splits are not counted. + * Physical presence is determined directly from EOF and is independent of + * parity validity. For split parity, if an earlier split is physically shorter + * than its logical size, subsequent splits cannot extend the logical extent + * past that missing region. */ size = 0; for (s = 0; s < handle->split_mac; ++s) { struct snapraid_split_handle* split = &handle->split_map[s]; - data_off_t run = split->physical_reach_size; + data_off_t run = split->st.st_size; /* don't count physical data outside the logical split */ if (run > split->size) @@ -212,27 +207,6 @@ int parity_create(struct snapraid_parity_handle* handle, const struct snapraid_p /* LCOV_EXCL_STOP */ } - /* - * Initialize the physical read/truncation boundary from the current EOF. - * - * physical_reach_size does NOT imply that parity below this offset is valid. - * Parity may be invalid, stale, unwritten, or sparse in any region below - * this boundary. It only records the physical EOF boundary for I/O and - * truncation. The physical contents below this boundary are not necessarily - * described by the content state. Recovery correctness is validated - * independently using data hashes and recomputation. - * - * While a handle is open, physical_reach_size can intentionally differ from EOF. - * In particular, preallocation may extend EOF without extending physical_reach_size. - * Writable fix paths normally truncate the file back to physical_reach_size before - * closing. - * - * Across close/reopen there is no separate persistent physical_reach value, so EOF - * is used to reconstruct this physical boundary. This reconstruction must not - * be interpreted as validation of the parity contained below EOF. - */ - split->physical_reach_size = split->st.st_size; - /** * If the parity size is not yet set, set it now. * This happens when expanding the number of parities, @@ -389,6 +363,29 @@ static int parity_handle_shrink(struct snapraid_split_handle* split, data_off_t return ret; } +/** + * Set an exact physical split size without attempting a partial allocation. + */ +static int parity_setsize(struct snapraid_split_handle* split, data_off_t size, int skip_fallocate) +{ + int ret; + + if (split->st.st_size < size) + ret = parity_handle_grow(split, split->st.st_size, size, skip_fallocate); + else if (split->st.st_size > size) + ret = parity_handle_shrink(split, size); + else + return 0; + + if (ret != 0) + return -1; + + /* the exact resize succeeded, so keep the cached EOF current */ + split->st.st_size = size; + + return 0; +} + /** * Get the highest bit set. */ @@ -566,23 +563,73 @@ static int parity_handle_chsize(struct snapraid_split_handle* split, data_off_t /* LCOV_EXCL_STOP */ } + return 0; +} + +int parity_restore(struct snapraid_parity_handle* handle, data_off_t size, uint32_t block_size, int skip_fallocate) +{ + int ret; + unsigned s; + unsigned physical_split; + data_off_t block_mask; + + /* mask of bits used by the block size */ + block_mask = ((data_off_t)block_size) - 1; + + if (size < 0) { + /* LCOV_EXCL_START */ + errno = ENXIO; + return -1; + /* LCOV_EXCL_STOP */ + } + + physical_split = 0; + for (s = 0; s < handle->split_mac; ++s) { + struct snapraid_split_handle* split = &handle->split_map[s]; + data_off_t run = split->size; + + /* preserve persisted split boundaries and cut only the logical tail */ + if (run > size) + run = size; + + if ((run & block_mask) != 0) { + /* LCOV_EXCL_START */ + errno = ENXIO; + log_fatal(EINTERNAL, "Internal inconsistency in split '%s' size with extra '%" PRIu64 "' bytes.\n", split->path, run & block_mask); + return -1; + /* LCOV_EXCL_STOP */ + } + + /* defer growth until a parity block is actually written */ + if (split->st.st_size > run) { + ret = parity_setsize(split, run, 0); + if (ret != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + } + + if (split->st.st_size != 0) + physical_split = s; + + size -= run; + } + /* - * Keep the physical read/truncation boundary consistent with resizing. - * - * Shrinking reduces physical_reach_size because bytes beyond the new EOF no longer - * exist physically and therefore cannot be read. - * - * Growing leaves physical_reach_size unchanged. Allocation or preallocation alone - * does not make the newly created physical extent readable as parity; the - * boundary is advanced only after a parity write has completed. - * - * This must not be confused with parity validity. Advancing physical_reach_size - * after a completed write only records the physical offset reached by writes. - * It does not certify preceding bytes as correct parity; parity may remain - * invalid or unwritten in any region below this boundary. + * After capping the tail, a non-empty later split belongs to the required + * layout and requires every preceding persisted boundary to exist exactly. */ - if (split->physical_reach_size > split->st.st_size) - split->physical_reach_size = split->st.st_size; + for (s = 0; s < physical_split; ++s) { + struct snapraid_split_handle* split = &handle->split_map[s]; + + ret = parity_setsize(split, split->size, skip_fallocate); + if (ret != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + } return 0; } @@ -616,7 +663,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 allow_split_realloc) +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 ret; unsigned s; @@ -677,21 +724,15 @@ 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 || !allow_split_realloc) && split->st.st_size < run) { + } else if (is_fixed && 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); + log_fatal(errno, "Failed 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 { @@ -805,27 +846,6 @@ int parity_open(struct snapraid_parity_handle* handle, const struct snapraid_par /* LCOV_EXCL_STOP */ } - /* - * Initialize the physical read/truncation boundary from the current EOF. - * - * physical_reach_size does NOT imply that parity below this offset is valid. - * Parity may be invalid, stale, unwritten, or sparse in any region below - * this boundary. It only records the physical EOF boundary for I/O and - * truncation. The physical contents below this boundary are not necessarily - * described by the content state. Recovery correctness is validated - * independently using data hashes and recomputation. - * - * While a handle is open, physical_reach_size can intentionally differ from EOF. - * In particular, preallocation may extend EOF without extending physical_reach_size. - * Writable fix paths normally truncate the file back to physical_reach_size before - * closing. - * - * Across close/reopen there is no separate persistent physical_reach value, so EOF - * is used to reconstruct this physical boundary. This reconstruction must not - * be interpreted as validation of the parity contained below EOF. - */ - split->physical_reach_size = split->st.st_size; - /** * If the parity size is not yet set, set it now. * This happens when expanding the number of parities, @@ -977,67 +997,6 @@ int parity_sync(struct snapraid_parity_handle* handle) return 0; } -/* - * Truncate parity splits from the logical tail backwards. - * - * This removes preallocated or otherwise disposable data beyond the reached extent. - * - * A split can only be truncated to physical_reach_size if it represents the logical - * tail of the parity (i.e. all following splits are empty). Intermediate splits - * followed by non-empty splits must maintain their allocated logical boundary - * (split->size) so that logical parity offsets in subsequent splits are not shifted, - * and physical file lengths match the split boundaries expected on subsequent reopen. - */ -int parity_truncate(struct snapraid_parity_handle* handle) -{ - unsigned s; - int f_ret = 0; - int following_is_zero = 1; - - for (s = handle->split_mac; s > 0; --s) { - struct snapraid_split_handle* split = &handle->split_map[s - 1]; - data_off_t trunc_size; - int ret; - - /* - * A split can only be truncated to its physical_reach_size if it is the - * logical tail of the parity file (i.e. all following splits are zero-sized). - * - * Once a following split contains data, any preceding split is an internal - * segment whose allocated boundary must be preserved at split->size. - * Truncating an internal split would shift logical offsets in all subsequent - * splits, invalidating their parity positions, and if split boundaries are not - * yet stored in the content file, subsequent reopenings would infer an incorrect - * layout from physical EOF. - */ - if (following_is_zero) { - trunc_size = split->physical_reach_size; - } else { - trunc_size = split->size; - } - - ret = ftruncate(split->f, trunc_size); - if (ret != 0) { - /* LCOV_EXCL_START */ - log_fatal(errno, "Error truncating the parity file '%s' to size %" PRIu64 ". %s.\n", split->path, trunc_size, strerror(errno)); - f_ret = -1; - - /* - * On error, assume this split might not be zero-sized physically, - * so preceding splits must preserve their full allocated size. - */ - following_is_zero = 0; - - /* continue to truncate the others */ - /* LCOV_EXCL_STOP */ - } else if (trunc_size != 0) { - following_is_zero = 0; - } - } - - return f_ret; -} - int parity_close(struct snapraid_parity_handle* handle) { unsigned s; @@ -1090,11 +1049,13 @@ struct snapraid_split_handle* parity_split_find(struct snapraid_parity_handle* h return 0; } -int parity_write(struct snapraid_parity_handle* handle, block_off_t pos, unsigned char* block_buffer, unsigned block_size) +int parity_write(struct snapraid_parity_handle* handle, block_off_t pos, unsigned char* block_buffer, unsigned block_size, int skip_fallocate) { ssize_t write_ret; data_off_t offset; struct snapraid_split_handle* split; + unsigned long split_idx; + unsigned s; unsigned count; int ret; @@ -1108,12 +1069,39 @@ int parity_write(struct snapraid_parity_handle* handle, block_off_t pos, unsigne return -1; /* LCOV_EXCL_STOP */ } + split_idx = split - handle->split_map; /* * Parity blocks cannot cross split boundaries: offsets and split sizes * are block-aligned, so the entire block belongs to this split. */ + /* + * Grow parity only as writes reach it. A write in a later split requires + * every preceding split to reach its persisted logical boundary, otherwise + * the physical files would not represent a contiguous logical high-water mark. + * Allocation still says nothing about parity validity, which is tracked and + * verified independently. + */ + for (s = 0; s <= split_idx; ++s) { + struct snapraid_split_handle* grow_split = &handle->split_map[s]; + data_off_t grow_size; + + if (s < split_idx) + grow_size = grow_split->size; + else + grow_size = offset; + + if (grow_split->st.st_size < grow_size) { + ret = parity_setsize(grow_split, grow_size, skip_fallocate); + if (ret != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + } + } + bw_limit(handle->bw, block_size); count = 0; @@ -1143,17 +1131,8 @@ int parity_write(struct snapraid_parity_handle* handle, block_off_t pos, unsigne count += write_ret; } while (count < block_size); - /* - * Extend the physical read/truncation limit only after the complete block - * has been written. - * - * This is not a validity marker. In particular, writes may occur non-contiguously, - * so bytes between the old boundary and this block are not implied to contain - * valid parity. Parity correctness is tracked independently per block in the - * content state. - */ - if (split->physical_reach_size < offset + block_size) - split->physical_reach_size = offset + block_size; + if (split->st.st_size < offset + block_size) + split->st.st_size = offset + block_size; ret = advise_write(&split->advise, split->f, offset, block_size); if (ret != 0) { @@ -1191,12 +1170,11 @@ int parity_read(struct snapraid_parity_handle* handle, block_off_t pos, unsigned */ /* - * physical_reach_size only limits the physical offset that this handle considers - * readable. Passing this check does not mean that the parity bytes are valid. - * A readable region may contain stale, sparse, unwritten, or corrupted parity, - * which higher-level check/recovery logic must validate independently. + * Physical EOF only limits whether bytes can be read. It does not imply that + * the parity is valid, synchronized, or initialized; validity is determined + * independently by the higher-level check/recovery logic. */ - if (offset >= split->physical_reach_size) { + if (offset >= split->st.st_size) { /* LCOV_EXCL_START */ errno = ENXIO; log_error(errno, "Reading over the end from parity file '%s' at offset %" PRIu64 " for size %u.\n", split->path, offset, block_size); @@ -1239,4 +1217,3 @@ int parity_read(struct snapraid_parity_handle* handle, block_off_t pos, unsigned return block_size; } - diff --git a/cmdline/parity.h b/cmdline/parity.h index e2851f86..0fb3c9f9 100644 --- a/cmdline/parity.h +++ b/cmdline/parity.h @@ -25,31 +25,6 @@ struct snapraid_split_handle { */ data_off_t size; - /** - * High-water mark size of the parity split. - * - * This is the physical truncation and read limit of the split. - * - * physical_reach_size does NOT indicate that data below this offset is - * valid or contiguous parity. Parity may be invalid, stale, unwritten, - * or sparse at any point below this boundary. In particular, fix may - * leave an unrecoverable stripe unfixed and continue repairing later - * stripes, intentionally leaving holes inside the physical reach. - * - * Such holes are allowed and are not necessarily represented in the - * content state. Recovery correctness is validated independently using - * the stored data hashes or the available recovery validation. - * - * Data beyond physical_reach_size is disposable/preallocated and discarded by - * parity_truncate(). A completed write advances physical_reach_size to the end of - * the written block so it is not truncated away. - * - * Before closing writable parity handles during fix operations, parity_truncate() - * persists this boundary into the physical EOF. On the next open, physical_reach_size can - * therefore be initialized from st_size. - */ - data_off_t physical_reach_size; - /** * Artificial size limit for testing. * 0 means unlimited. @@ -99,15 +74,22 @@ int parity_create(struct snapraid_parity_handle* handle, const struct snapraid_p /** * Change the parity size. * - * 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. + * An elastic split that cannot reach the requested size may keep the size + * actually allocated and leave the remaining space to following splits. + * The resulting split layout is returned in parity. */ -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 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); + +/** + * Restore the parity split layout already stored in the handle, limited to size. + * + * Persisted split boundaries are preserved. If size ends inside the persisted + * layout, only its logical tail is removed: the containing split is shortened + * and all following splits are truncated to zero. If a split is physically + * non-empty, all preceding splits are restored to their exact logical size. + * The remaining physical tail is grown lazily by parity_write(). + */ +int parity_restore(struct snapraid_parity_handle* handle, data_off_t size, uint32_t block_size, int skip_fallocate); /** * Get the size of the parity. @@ -117,21 +99,16 @@ int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* void parity_size(struct snapraid_parity_handle* handle, data_off_t* out_size); /** - * Get the physical reach of the logical parity file across splits. + * Get the physical size of the logical parity file across splits. * - * This composes the physical_reach_size boundaries of consecutive splits. - * If the physical_reach_size of an earlier split is smaller than its logical size, + * This composes the physical EOF of consecutive splits within their logical + * boundaries. If an earlier split is physically shorter than its logical size, * later splits cannot extend the logical offset past that missing region. * - * Parity within this range is not necessarily valid, synchronized, or contiguous. - * In particular, fix may leave an unrecoverable stripe unfixed and continue with - * later stripes, so holes inside this range are allowed and are not necessarily - * represented in the content state. - * - * For example, with two 100 GiB splits, if the first has physical_reach_size 90 GiB - * and the second has physical_reach_size 100 GiB, the result is 90 GiB, not 190 GiB. + * Physical presence does not imply parity validity. Parity may be invalid, + * stale, unwritten, sparse, or contain holes independently of its physical EOF. */ -void parity_physical_reach_size(struct snapraid_parity_handle* handle, data_off_t* out_size); +void parity_physical_size(struct snapraid_parity_handle* handle, data_off_t* out_size); /** * Open an already existing parity file. @@ -143,11 +120,6 @@ int parity_open(struct snapraid_parity_handle* handle, const struct snapraid_par */ int parity_sync(struct snapraid_parity_handle* handle); -/** - * Truncate parity splits from the logical tail backwards. - */ -int parity_truncate(struct snapraid_parity_handle* handle); - /** * Close the parity file. */ @@ -161,7 +133,7 @@ int parity_read(struct snapraid_parity_handle* handle, block_off_t pos, unsigned /** * Write a block in the parity file. */ -int parity_write(struct snapraid_parity_handle* handle, block_off_t pos, unsigned char* block_buffer, unsigned block_size); +int parity_write(struct snapraid_parity_handle* handle, block_off_t pos, unsigned char* block_buffer, unsigned block_size, int skip_fallocate); /** * Complete all pending I/O and sync the parity files. diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 773f1e01..89fc2f21 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -303,7 +303,7 @@ static void test_parity(void) static const struct { unsigned split_mac; data_off_t size[2]; - data_off_t physical_reach_size[2]; + data_off_t physical_size[2]; data_off_t expected; } test[] = { { 1, { 100, 0 }, { 100, 0 }, 100 }, @@ -326,13 +326,13 @@ static void test_parity(void) handle.split_mac = test[i].split_mac; for (s = 0; s < handle.split_mac; ++s) { handle.split_map[s].size = test[i].size[s]; - handle.split_map[s].physical_reach_size = test[i].physical_reach_size[s]; + handle.split_map[s].st.st_size = test[i].physical_size[s]; } - parity_physical_reach_size(&handle, &size); + parity_physical_size(&handle, &size); if (size != test[i].expected) { /* LCOV_EXCL_START */ - log_fatal(EINTERNAL, "Failed parity physical reach size test\n"); + log_fatal(EINTERNAL, "Failed parity physical size test\n"); exit(EXIT_FAILURE); /* LCOV_EXCL_STOP */ } diff --git a/cmdline/sync.c b/cmdline/sync.c index 6b512e70..7bfe2e44 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -911,7 +911,7 @@ static void sync_parity_writer(struct snapraid_worker* worker, struct snapraid_t int ret; /* write parity */ - ret = parity_write(parity_handle, blockcur, buffer, state->block_size); + ret = parity_write(parity_handle, blockcur, buffer, state->block_size, state->opt.skip_fallocate); if (ret == -1) { /* LCOV_EXCL_START */ log_tag("parity_%s:%" PRIu64 ":%s: Write error. %s.\n", es(errno), blockcur, lev_config_name(level), strerror(errno)); @@ -2011,16 +2011,16 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t * discard parity positions that are still referenced by the previously * persisted content file. * - * This comparison deliberately does not use physical_reach_size or physical EOF. - * Those describe physical I/O extent, not the logical parity layout whose - * crash-consistency relationship with the content file must be preserved. + * This comparison deliberately does not use physical EOF. It describes + * physical I/O extent, not the logical parity layout whose crash-consistency + * relationship with the content file must be preserved. */ parity_size(&parity_handle[l], ¤t_size); if (size < current_size) parity_shrink = 1; /* number of block in the parity file */ - parity_physical_reach_size(&parity_handle[l], &out_size); + parity_physical_size(&parity_handle[l], &out_size); parityblocks = out_size / state->block_size; /* if the file is too small */ @@ -2041,9 +2041,9 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t * truncation before parity_tail remains fatal. * * New levels need the same physical coverage of valid blocks, not an exact - * match with another level or the allocated extent. After an interrupted - * sync, fix leaves pending parity to sync and may truncate its unwritten - * tail. Sync will resize and regenerate that tail if it is still allocated. + * match with another level or the allocated extent. Physical EOF only records + * presence; pending or invalid parity is identified independently by block + * state and regenerated by sync when required. */ if (!state->opt.force_full && !(state->opt.force_realloc && state->opt.parity_tail == 0)) { /* if the parities are too small */ @@ -2103,9 +2103,9 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t * existing post-resize content write persists the resulting split sizes before * parity synchronization begins. * - * This decision uses the logical parity size, not physical_reach_size or physical - * EOF. physical_reach_size is only a physical I/O extent and does not represent - * parity validity or the persisted logical mapping. + * This decision uses the logical parity size, not physical EOF. Physical EOF + * is only an I/O extent and does not represent parity validity or the persisted + * logical mapping. */ if (state->opt.force_full) state_sync_mark_rebuild(state); @@ -2132,7 +2132,7 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t * 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, 1); + ret = parity_chsize(&parity_handle[l], &state->parity[l], &is_modified, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder); if (ret == -1) { /* LCOV_EXCL_START */ data_off_t out_size;