Replace physical reach tracking with the natural file and parity EOF
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-09-09 09:58:56 +02:00
parent 873a5561e4
commit 62ae6c3552
7 changed files with 195 additions and 278 deletions

View file

@ -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 */

View file

@ -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;
}

View file

@ -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. */

View file

@ -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;
}

View file

@ -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.

View file

@ -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 */
}

View file

@ -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], &current_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;