From 4922bbaa046a8cd08ba7a388d781b1ec97e4e729 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Wed, 26 Aug 2026 16:34:37 +0200 Subject: [PATCH] dsl_scan: use resilver I/O for DTL-limited scrubs When a scrub finds replacement DTLs, vdev_resilver_needed() narrows the scan to ranges requiring repair, but scrub I/O still reads every child of a replacing vdev. For a split indirect block, its BP-less segment read can compare valid data from the old child with incomplete data from the replacement, discard the valid result, and finish without repair. The scan can then excise the replacement DTL and detach the old child. Record the effective scan as a healing resilver so its I/O skips the DTL-missing child, repairs it, and records failed repair writes. Preserve the original scrub request in the persisted scan flags so status, cancel, pause, wait, and thorough scrub semantics remain unchanged. Add regressions that preserve replacement DTLs with a checkpoint. Verify scrub controls across export/import, target selection, repair, and replacement completion. Exercise split indirect mappings with an incomplete replacement and verify data integrity after the original child has detached and the pool has been exported and imported. Signed-off-by: Kamil Monicz --- cmd/ztest.c | 9 +- include/sys/dsl_scan.h | 2 + module/zfs/dsl_scan.c | 29 +++- module/zfs/spa.c | 8 +- module/zfs/spa_misc.c | 3 +- module/zfs/zfs_ioctl.c | 4 +- tests/runfiles/common.run | 3 +- tests/zfs-tests/tests/Makefile.am | 2 + .../scrub_resilver_classification.ksh | 150 ++++++++++++++++++ .../replacement/scrub_resilver_split.ksh | 142 +++++++++++++++++ 10 files changed, 339 insertions(+), 13 deletions(-) create mode 100755 tests/zfs-tests/tests/functional/replacement/scrub_resilver_classification.ksh create mode 100755 tests/zfs-tests/tests/functional/replacement/scrub_resilver_split.ksh diff --git a/cmd/ztest.c b/cmd/ztest.c index 9c72eaa60664..73e7964cf3ee 100644 --- a/cmd/ztest.c +++ b/cmd/ztest.c @@ -4188,7 +4188,8 @@ ztest_device_removal(ztest_ds_t *zd, uint64_t id) */ error = spa_scan(spa, POOL_SCAN_SCRUB, 0); if (error == 0) { - while (dsl_scan_scrubbing(spa_get_dsl(spa))) + while (dsl_scan_scrubbing(spa_get_dsl(spa)) || + dsl_scan_resilvering(spa_get_dsl(spa))) txg_wait_synced(spa_get_dsl(spa), 0); } @@ -6684,7 +6685,8 @@ ztest_fault_inject(ztest_ds_t *zd, uint64_t id) if (injected && ztest_opts.zo_raid_do_expand) { int error = spa_scan(spa, POOL_SCAN_SCRUB, 0); if (error == 0) { - while (dsl_scan_scrubbing(spa_get_dsl(spa))) + while (dsl_scan_scrubbing(spa_get_dsl(spa)) || + dsl_scan_resilvering(spa_get_dsl(spa))) txg_wait_synced(spa_get_dsl(spa), 0); } } @@ -6719,7 +6721,8 @@ ztest_scrub_impl(spa_t *spa) if (error) return (error); - while (dsl_scan_scrubbing(spa_get_dsl(spa))) + while (dsl_scan_scrubbing(spa_get_dsl(spa)) || + dsl_scan_resilvering(spa_get_dsl(spa))) txg_wait_synced(spa_get_dsl(spa), 0); if (spa_approx_errlog_size(spa) > 0) diff --git a/include/sys/dsl_scan.h b/include/sys/dsl_scan.h index b0d580335a7c..4f19bb2abdbc 100644 --- a/include/sys/dsl_scan.h +++ b/include/sys/dsl_scan.h @@ -67,6 +67,7 @@ typedef enum dsl_scan_flags { DSF_VISIT_DS_AGAIN = 1<<0, DSF_SCRUB_PAUSED = 1<<1, DSF_SCRUB_THOROUGH = 1<<2, + DSF_SCRUB_REQUESTED = 1<<3, /* original request was a scrub */ } dsl_scan_flags_t; typedef struct dsl_errorscrub_phys { @@ -189,6 +190,7 @@ int dsl_scan_cancel(struct dsl_pool *); int dsl_scan(struct dsl_pool *, pool_scan_func_t, uint64_t starttxg, uint64_t txgend, dsl_scan_flags_t flags); void dsl_scan_assess_vdev(struct dsl_pool *dp, vdev_t *vd); +boolean_t dsl_scan_is_scrub_requested(const dsl_scan_t *scn); boolean_t dsl_scan_scrubbing(const struct dsl_pool *dp); boolean_t dsl_errorscrubbing(const struct dsl_pool *dp); boolean_t dsl_errorscrub_active(dsl_scan_t *scn); diff --git a/module/zfs/dsl_scan.c b/module/zfs/dsl_scan.c index 4e561c48edac..8f1e1fce0136 100644 --- a/module/zfs/dsl_scan.c +++ b/module/zfs/dsl_scan.c @@ -777,6 +777,13 @@ dsl_scan_resilver_scheduled(dsl_pool_t *dp) (spa_async_tasks(dp->dp_spa) & SPA_ASYNC_RESILVER)); } +boolean_t +dsl_scan_is_scrub_requested(const dsl_scan_t *scn) +{ + return (scn->scn_phys.scn_func == POOL_SCAN_SCRUB || + scn->scn_phys.scn_flags & DSF_SCRUB_REQUESTED); +} + boolean_t dsl_scan_scrubbing(const dsl_pool_t *dp) { @@ -805,14 +812,16 @@ dsl_errorscrub_is_paused(const dsl_scan_t *scn) boolean_t dsl_scan_is_paused_scrub(const dsl_scan_t *scn) { - return (dsl_scan_scrubbing(scn->scn_dp) && + return (dsl_scan_is_running(scn) && + dsl_scan_is_scrub_requested(scn) && scn->scn_phys.scn_flags & DSF_SCRUB_PAUSED); } static boolean_t dsl_scan_is_thorough_scrub(const dsl_scan_t *scn) { - return (dsl_scan_scrubbing(scn->scn_dp) && + return (dsl_scan_is_running(scn) && + dsl_scan_is_scrub_requested(scn) && scn->scn_phys.scn_flags & DSF_SCRUB_THOROUGH); } @@ -1017,6 +1026,14 @@ dsl_scan_setup_sync(void *arg, dmu_tx_t *tx) spa_event_notify(spa, NULL, aux, ESC_ZFS_RESILVER_START); nvlist_free(aux); + /* + * DTLs limit this scan to replacement ranges. + * Preserve the scrub request while using resilver + * repair semantics. + */ + if (setup_sync_arg->func == POOL_SCAN_SCRUB) + scn->scn_phys.scn_flags |= DSF_SCRUB_REQUESTED; + scn->scn_phys.scn_func = POOL_SCAN_RESILVER; } else { spa_event_notify(spa, NULL, NULL, ESC_ZFS_SCRUB_START); } @@ -1485,7 +1502,8 @@ dsl_scrub_pause_resume_check(void *arg, dmu_tx_t *tx) if (*cmd == POOL_SCRUB_PAUSE) { /* can't pause a scrub when there is no in-progress scrub */ - if (!dsl_scan_scrubbing(dp)) + if (!dsl_scan_is_running(scn) || + !dsl_scan_is_scrub_requested(scn)) return (SET_ERROR(ENOENT)); /* can't pause a paused scrub */ @@ -5050,6 +5068,9 @@ dsl_scan_scrub_done(zio_t *zio) { spa_t *spa = zio->io_spa; dsl_scan_io_queue_t *queue = zio->io_private; + boolean_t scrub_io = (zio->io_flags & ZIO_FLAG_SCRUB) || + (spa->spa_dsl_pool->dp_scan->scn_phys.scn_flags & + DSF_SCRUB_REQUESTED); abd_free(zio->io_abd); @@ -5082,7 +5103,7 @@ dsl_scan_scrub_done(zio_t *zio) */ if (zio->io_error && (zio->io_error != ECKSUM || !(zio->io_flags & ZIO_FLAG_SPECULATIVE)) && - !(zio->io_error == EACCES && (zio->io_flags & ZIO_FLAG_SCRUB) && + !(zio->io_error == EACCES && scrub_io && !(zio->io_flags & ZIO_FLAG_RAW))) { if (dsl_errorscrubbing(spa->spa_dsl_pool) && !dsl_errorscrub_is_paused(spa->spa_dsl_pool->dp_scan)) { diff --git a/module/zfs/spa.c b/module/zfs/spa.c index b4d559954698..610edc368aa1 100644 --- a/module/zfs/spa.c +++ b/module/zfs/spa.c @@ -9831,7 +9831,8 @@ spa_scrub_pause_resume(spa_t *spa, pool_scrub_cmd_t cmd) { ASSERT0(spa_config_held(spa, SCL_ALL, RW_WRITER)); - if (dsl_scan_resilvering(spa->spa_dsl_pool)) + if (dsl_scan_resilvering(spa->spa_dsl_pool) && + !dsl_scan_is_scrub_requested(spa->spa_dsl_pool->dp_scan)) return (SET_ERROR(EBUSY)); return (dsl_scrub_set_pause_resume(spa->spa_dsl_pool, cmd)); @@ -9841,7 +9842,8 @@ int spa_scan_stop(spa_t *spa) { ASSERT0(spa_config_held(spa, SCL_ALL, RW_WRITER)); - if (dsl_scan_resilvering(spa->spa_dsl_pool)) + if (dsl_scan_resilvering(spa->spa_dsl_pool) && + !dsl_scan_is_scrub_requested(spa->spa_dsl_pool->dp_scan)) return (SET_ERROR(EBUSY)); return (dsl_scan_cancel(spa->spa_dsl_pool)); @@ -11755,7 +11757,7 @@ spa_activity_in_progress(spa_t *spa, zpool_wait_activity_t activity, boolean_t scanning, paused, is_scrub; dsl_scan_t *scn = spa->spa_dsl_pool->dp_scan; - is_scrub = (scn->scn_phys.scn_func == POOL_SCAN_SCRUB); + is_scrub = dsl_scan_is_scrub_requested(scn); scanning = (scn->scn_phys.scn_state == DSS_SCANNING); paused = dsl_scan_is_paused_scrub(scn); *in_progress = (scanning && !paused && diff --git a/module/zfs/spa_misc.c b/module/zfs/spa_misc.c index a3d5f1f291d1..766e2a0f715c 100644 --- a/module/zfs/spa_misc.c +++ b/module/zfs/spa_misc.c @@ -2847,7 +2847,8 @@ spa_scan_get_stats(spa_t *spa, pool_scan_stat_t *ps) memset(ps, 0, sizeof (pool_scan_stat_t)); /* data stored on disk */ - ps->pss_func = scn->scn_phys.scn_func; + ps->pss_func = dsl_scan_is_scrub_requested(scn) ? POOL_SCAN_SCRUB : + scn->scn_phys.scn_func; ps->pss_state = scn->scn_phys.scn_state; ps->pss_start_time = scn->scn_phys.scn_start_time; ps->pss_end_time = scn->scn_phys.scn_end_time; diff --git a/module/zfs/zfs_ioctl.c b/module/zfs/zfs_ioctl.c index aea8702feb0a..665450171d14 100644 --- a/module/zfs/zfs_ioctl.c +++ b/module/zfs/zfs_ioctl.c @@ -6702,8 +6702,10 @@ zfs_ioc_pool_reopen(const char *pool, nvlist_t *innvl, nvlist_t *outnvl) * Otherwise, let vdev_open() decided if a resilver is required. */ + dsl_scan_t *scn = spa->spa_dsl_pool->dp_scan; spa->spa_scrub_reopen = (!scrub_restart && - dsl_scan_scrubbing(spa->spa_dsl_pool)); + scn->scn_phys.scn_state == DSS_SCANNING && + dsl_scan_is_scrub_requested(scn)); vdev_reopen(spa->spa_root_vdev); spa->spa_scrub_reopen = B_FALSE; diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 1881ef9690a5..1e817f997f12 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -978,7 +978,8 @@ tests = ['attach_import', 'attach_multiple', 'attach_rebuild', 'rebuild_disabled_feature', 'rebuild_multiple', 'rebuild_raidz', 'replace_import', 'replace_rebuild', 'replace_resilver', 'replace_resilver_sit_out', 'resilver_restart_001', - 'resilver_restart_002', 'scrub_cancel'] + 'resilver_restart_002', 'scrub_cancel', 'scrub_resilver_classification', + 'scrub_resilver_split'] tags = ['functional', 'replacement'] [tests/functional/reservation] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 0cabc9807c74..89f62b6f5494 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -2109,6 +2109,8 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/replacement/resilver_restart_001.ksh \ functional/replacement/resilver_restart_002.ksh \ functional/replacement/scrub_cancel.ksh \ + functional/replacement/scrub_resilver_classification.ksh \ + functional/replacement/scrub_resilver_split.ksh \ functional/replacement/setup.ksh \ functional/reservation/cleanup.ksh \ functional/reservation/reservation_001_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/replacement/scrub_resilver_classification.ksh b/tests/zfs-tests/tests/functional/replacement/scrub_resilver_classification.ksh new file mode 100755 index 000000000000..1a0cf7a3d6be --- /dev/null +++ b/tests/zfs-tests/tests/functional/replacement/scrub_resilver_classification.ksh @@ -0,0 +1,150 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 + +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# https://opensource.org/license/CDDL-1.0. +# + +# shellcheck disable=SC1091 + +. "$STF_SUITE"/include/libtest.shlib +. "$STF_SUITE"/tests/functional/replacement/replacement.cfg + +# +# DESCRIPTION: +# Verify a DTL-limited scrub preserves scrub controls while using healing +# resilver I/O. +# +# STRATEGY: +# 1. Start a replacement with scan progress paused. +# 2. Complete its initial resilver under a checkpoint, preserving the +# replacement DTL, then discard the checkpoint. +# 3. Pause the DTL-limited scrub, export/import the pool, then resume +# and cancel it. +# 4. Run it again with read errors injected on the DTL-missing child. +# 5. Verify scrub status, resilver I/O, repair, detach, and data integrity. +# + +verify_runnable "global" + +function inject_count +{ + zinject | awk ' + /^ *[0-9]/ { + count++ + inject = $NF + } + END { + if (count != 1) + exit 1 + print inject + }' +} + +function cleanup +{ + zinject -c all >/dev/null 2>&1 + set_tunable32 SCAN_SUSPEND_PROGRESS \ + "$ORIG_SCAN_SUSPEND_PROGRESS" >/dev/null 2>&1 + zpool checkpoint -d -w "$TESTPOOL1" >/dev/null 2>&1 + destroy_pool "$TESTPOOL1" + rm -f "${VDEV_FILES[@]}" "$SPARE_VDEV_FILE" +} + +log_assert "DTL-limited scrubs retain scrub controls and use resilver I/O" + +ORIG_SCAN_SUSPEND_PROGRESS=$(get_tunable SCAN_SUSPEND_PROGRESS) + +log_onexit cleanup + +log_must zinject -c all +log_must truncate -s "$VDEV_FILE_SIZE" \ + "${VDEV_FILES[0]}" "$SPARE_VDEV_FILE" +# Keep the replacement DTL available for the scrub instead of restarting a +# deferred resilver when the checkpointed initial scan completes. +log_must zpool create -f -o feature@resilver_defer=disabled \ + "$TESTPOOL1" "${VDEV_FILES[0]}" +log_must zfs create "$TESTPOOL1/$TESTFS" + +mntpnt=$(get_prop mountpoint "$TESTPOOL1/$TESTFS") +log_must dd if=/dev/urandom of="$mntpnt/file" bs=1M count=1 +sync_pool "$TESTPOOL1" + +# A checkpoint prevents the initial resilver from excising the target DTL. +log_must set_tunable32 SCAN_SUSPEND_PROGRESS 1 +log_must zpool replace "$TESTPOOL1" "${VDEV_FILES[0]}" \ + "$SPARE_VDEV_FILE" +log_must is_pool_resilvering "$TESTPOOL1" +log_must zpool checkpoint "$TESTPOOL1" +log_must set_tunable32 SCAN_SUSPEND_PROGRESS 0 +log_must zpool wait -t resilver "$TESTPOOL1" +log_must is_pool_resilvered "$TESTPOOL1" +log_must zpool checkpoint -d -w "$TESTPOOL1" + +pool_status=$(zpool status -P "$TESTPOOL1") || + log_fail "unable to read pool status after checkpoint discard" +[[ "$pool_status" == *replacing-* && + "$pool_status" == *"${VDEV_FILES[0]}"* && + "$pool_status" == *"$SPARE_VDEV_FILE"* ]] || + log_fail "replacement topology was not preserved: $pool_status" + +# The effective resilver must retain the controls of the requested scrub. +log_must set_tunable32 SCAN_SUSPEND_PROGRESS 1 +log_must zpool scrub -t "$TESTPOOL1" +log_must is_pool_scrubbing "$TESTPOOL1" +log_must zpool scrub -p "$TESTPOOL1" +log_must is_pool_scrub_paused "$TESTPOOL1" +log_must zpool export "$TESTPOOL1" +log_must zpool import -d "$TEST_BASE_DIR" "$TESTPOOL1" +log_must is_pool_scrub_paused "$TESTPOOL1" +log_must zpool scrub -t "$TESTPOOL1" +log_must is_pool_scrubbing "$TESTPOOL1" +log_must zpool scrub -s "$TESTPOOL1" +log_must is_pool_scrub_stopped "$TESTPOOL1" + +# Resilver I/O must read the valid original child and repair the missing target. +log_must zinject -d "$SPARE_VDEV_FILE" -e io -T read -f 100 "$TESTPOOL1" +log_must zpool events -c +log_must zpool scrub -t "$TESTPOOL1" +log_must is_pool_scrubbing "$TESTPOOL1" + +log_must set_tunable32 SCAN_SUSPEND_PROGRESS 0 +log_must zpool wait -t scrub "$TESTPOOL1" +target_inject_count=$(inject_count) || + log_fail "zinject did not report exactly one active rule" +log_must zinject -c all +[[ "$target_inject_count" == 0 ]] || + log_fail "scan read the replacement target (inject=$target_inject_count)" +log_must is_pool_scrubbed "$TESTPOOL1" + +log_must zpool wait -t replace "$TESTPOOL1" +pool_status=$(zpool status -P "$TESTPOOL1") || + log_fail "unable to read pool status after scrub" +[[ "$pool_status" != *replacing-* && + "$pool_status" != *"${VDEV_FILES[0]}"* && + "$pool_status" == *"$SPARE_VDEV_FILE"* ]] || + log_fail "replacement did not complete: $pool_status" + +resilver_finish=$(zpool events | \ + awk '/sysevent.fs.zfs.resilver_finish/ { count++ } END { print count + 0 }') +[[ "$resilver_finish" == 1 ]] || + log_fail "expected one resilver finish event, found $resilver_finish" + +last_scrubbed=$(zpool get -H -o value last_scrubbed_txg "$TESTPOOL1") +[[ "$last_scrubbed" == 0 ]] || + log_fail "DTL-limited scrub advanced last_scrubbed_txg to $last_scrubbed" + +log_must check_pool_status "$TESTPOOL1" "scan" \ + "scrub repaired [1-9].*with 0 errors" +log_must check_pool_status "$TESTPOOL1" "errors" "No known data errors" +log_must check_pool_device "$TESTPOOL1" "$SPARE_VDEV_FILE" "0 *0 *0" +log_must zdb -cdui "$TESTPOOL1/$TESTFS" + +log_pass "DTL-limited scrubs retain scrub controls and use resilver I/O" diff --git a/tests/zfs-tests/tests/functional/replacement/scrub_resilver_split.ksh b/tests/zfs-tests/tests/functional/replacement/scrub_resilver_split.ksh new file mode 100755 index 000000000000..0e57ab6de72d --- /dev/null +++ b/tests/zfs-tests/tests/functional/replacement/scrub_resilver_split.ksh @@ -0,0 +1,142 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 + +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# https://opensource.org/license/CDDL-1.0. +# + +# shellcheck disable=SC1091 + +. "$STF_SUITE"/include/libtest.shlib +. "$STF_SUITE"/tests/functional/replacement/replacement.cfg + +# +# DESCRIPTION: +# A DTL-limited scrub repairs split indirect blocks before detaching +# the original replacement child. +# +# STRATEGY: +# 1. Remove a vdev with 128K file blocks using 32K removal segments. +# 2. Replace the remaining vdev, silently dropping writes to the target. +# A checkpoint preserves its DTL when this initial resilver finishes. +# 3. Remove the injection and checkpoint, and verify that a mapped file +# segment is present on the original but missing from the target. +# 4. Scrub, wait for replacement completion, and verify the file's +# contents after exporting/importing the pool. +# + +verify_runnable "global" + +function cleanup +{ + zinject -c all >/dev/null 2>&1 + set_tunable32 SCAN_SUSPEND_PROGRESS \ + "$ORIG_SCAN_SUSPEND_PROGRESS" >/dev/null 2>&1 + set_tunable32 REMOVE_MAX_SEGMENT \ + "$ORIG_REMOVE_MAX_SEGMENT" >/dev/null 2>&1 + zpool checkpoint -d -w "$TESTPOOL1" >/dev/null 2>&1 + destroy_pool "$TESTPOOL1" + rm -f "${VDEV_FILES[0]}" "${VDEV_FILES[1]}" "$SPARE_VDEV_FILE" + rm -rf "$workdir" +} + +log_assert "DTL-limited scrubs repair incomplete split indirect replacements" + +ORIG_SCAN_SUSPEND_PROGRESS=$(get_tunable SCAN_SUSPEND_PROGRESS) +ORIG_REMOVE_MAX_SEGMENT=$(get_tunable REMOVE_MAX_SEGMENT) +workdir=$(mktemp -d "$TEST_BASE_DIR/scrub_resilver_split.XXXXXX") || + log_fail "cannot create test directory" +log_onexit cleanup + +log_must zinject -c all +# Removal needs free space beyond the pool's minimum slop reservation. +log_must truncate -s 512M \ + "${VDEV_FILES[0]}" "${VDEV_FILES[1]}" "$SPARE_VDEV_FILE" +log_must zpool create -f -o feature@resilver_defer=disabled \ + "$TESTPOOL1" "${VDEV_FILES[1]}" +log_must zfs create -o recordsize=128k -o compression=off -o atime=off \ + "$TESTPOOL1/$TESTFS" +mntpnt=$(get_prop mountpoint "$TESTPOOL1/$TESTFS") +log_must dd if=/dev/urandom of="$workdir/expected" bs=128k count=1 +log_must cp "$workdir/expected" "$mntpnt/file" +sync_pool "$TESTPOOL1" +object=$(get_objnum "$mntpnt/file") + +log_must zpool add "$TESTPOOL1" "${VDEV_FILES[0]}" +log_must set_tunable32 REMOVE_MAX_SEGMENT 32768 +log_must zpool remove -w "$TESTPOOL1" "${VDEV_FILES[1]}" +log_must set_tunable32 REMOVE_MAX_SEGMENT "$ORIG_REMOVE_MAX_SEGMENT" + +# Locate the first file segment in the actual indirect mapping. +log_must eval "zdb -dddddd '$TESTPOOL1/$TESTFS' '$object' >'$workdir/block'" +dva=$(awk '$1 == "0" && $2 == "L0" { print $3 }' "$workdir/block") +[[ "$dva" == 0:*:20000 ]] || + log_fail "expected a 128K file block on the removed vdev: $dva" +offset=${dva#0:} +offset=${offset%:*} +typeset -i file_offset="16#$offset" +log_must eval "zdb -mmmm '$TESTPOOL1' >'$workdir/mapping'" +segment_size=0 +while read -r src arrow dst rest; do + [[ "$src" == '<0:'* && "$arrow" == '->' ]] || continue + src=${src#'<0:'} + src=${src%'>'} + typeset -i start="16#${src%:*}" + typeset -i size="16#${src#*:}" + (( file_offset >= start && file_offset < start + size )) || continue + [[ "$dst" == '<1:'* ]] || + log_fail "file segment mapped to an unexpected vdev: $dst" + dst=${dst#'<1:'} + dst=${dst%'>'} + typeset -i target_offset="16#${dst%:*}" + (( target_offset += file_offset - start )) + (( segment_size = size - (file_offset - start) )) + break +done < "$workdir/mapping" +(( segment_size > 0 && segment_size < 131072 )) || + log_fail "file block was not split (first segment size=$segment_size)" +segment=$(printf "%x:%x:r" "$target_offset" "$segment_size") +log_must dd if="$workdir/expected" of="$workdir/expected-segment" \ + bs=512 count=$((segment_size / 512)) + +log_must set_tunable32 SCAN_SUSPEND_PROGRESS 1 +log_must zpool replace "$TESTPOOL1" "${VDEV_FILES[0]}" "$SPARE_VDEV_FILE" +log_must is_pool_resilvering "$TESTPOOL1" +log_must zinject -d "$SPARE_VDEV_FILE" -e noop -T write -f 100 "$TESTPOOL1" +log_must zpool checkpoint "$TESTPOOL1" +log_must set_tunable32 SCAN_SUSPEND_PROGRESS 0 +log_must zpool wait -t resilver "$TESTPOOL1" +log_must zinject -c all +log_must zpool checkpoint -d -w "$TESTPOOL1" +log_must is_pool_replacing "$TESTPOOL1" + +# Raw leaf reads cannot fall back to the other child or heal the target. +log_must eval "zdb -R '$TESTPOOL1' '${VDEV_FILES[0]}:$segment' \ + >'$workdir/original-segment'" +log_must cmp "$workdir/expected-segment" "$workdir/original-segment" +log_must eval "zdb -R '$TESTPOOL1' '$SPARE_VDEV_FILE:$segment' \ + >'$workdir/target-segment'" +log_must test "$(wc -c < "$workdir/target-segment")" -eq "$segment_size" +log_mustnot cmp "$workdir/expected-segment" "$workdir/target-segment" + +log_must zpool scrub "$TESTPOOL1" +log_must zpool wait -t scrub "$TESTPOOL1" +log_must zpool wait -t replace "$TESTPOOL1" +log_mustnot is_pool_replacing "$TESTPOOL1" + +log_must zpool export "$TESTPOOL1" +log_must zpool import -d "$TEST_BASE_DIR" "$TESTPOOL1" +log_must cmp "$workdir/expected" "$mntpnt/file" +log_must check_pool_status "$TESTPOOL1" "scan" \ + "scrub repaired [1-9].*with 0 errors" +log_must check_pool_status "$TESTPOOL1" "errors" "No known data errors" +log_must zdb -cdui "$TESTPOOL1/$TESTFS" + +log_pass "DTL-limited scrubs repair incomplete split indirect replacements"