[PATCH 7/6 RFC] nvme: test per-command retry delay
Sagi Grimberg <[email protected]>
| Newsgroups | org.infradead.lists.linux-nvme |
|---|---|
| Message-ID | <[email protected]> |
Add tests to exercise host command retry delays handling.
070: check that basic command RETRY disposition works and respect ctrl
crd
071: check that basic command FAILOVER disposition works and respects
ctrl crd
072: check that different commands completed with different crd levels
are retried independently, each respecting its paired completion
crd level
073: check that different commands completed with different crd levels
are failed-over independently, each respecting its paired completion
crd level
These tests rely on nvmet support for subsystem crdt attributes
(_require_nvmet_crdt) and nvme host crd error injection support.
In addition we add some common nvme helpers to set nvmet attributes,
inject errors, and leverage nvme diags to count retries/failovers.
Signed-off-by: Sagi Grimberg <[email protected]>
---
common/nvme | 281 +++++++++++++++++++++++++++++++++++++++++++++
tests/nvme/070 | 95 +++++++++++++++
tests/nvme/070.out | 3 +
tests/nvme/071 | 185 +++++++++++++++++++++++++++++
tests/nvme/071.out | 3 +
tests/nvme/072 | 118 +++++++++++++++++++
tests/nvme/072.out | 8 ++
tests/nvme/073 | 157 +++++++++++++++++++++++++
tests/nvme/073.out | 8 ++
tests/nvme/rc | 24 +++-
10 files changed, 881 insertions(+), 1 deletion(-)
create mode 100755 tests/nvme/070
create mode 100644 tests/nvme/070.out
create mode 100755 tests/nvme/071
create mode 100644 tests/nvme/071.out
create mode 100755 tests/nvme/072
create mode 100644 tests/nvme/072.out
create mode 100755 tests/nvme/073
create mode 100644 tests/nvme/073.out
diff --git a/common/nvme b/common/nvme
index f3999378db2d..a224dca61840 100644
--- a/common/nvme
+++ b/common/nvme
@@ -1527,3 +1527,284 @@ _nvme_requires() {
return 0
}
+
+_require_nvmet_crdt() {
+ local subsysnqn="${1:-$def_subsysnqn}"
+ local crdt_attr="${NVMET_CFS}/subsystems/${subsysnqn}/attr_crdt1"
+ local tmp="blktests-crdt-probe-$$"
+
+ _have_driver nvmet || return $?
+ _have_configfs || return $?
+
+ if [[ -e "${crdt_attr}" ]]; then
+ return 0
+ fi
+
+ if [[ ! -d "${NVMET_CFS}/subsystems" ]]; then
+ SKIP_REASONS+=("nvmet configfs is not available")
+ return 1
+ fi
+
+ mkdir "${NVMET_CFS}/subsystems/${tmp}" || {
+ SKIP_REASONS+=("unable to create nvmet subsystem for CRDT probe")
+ return 1
+ }
+ if [[ ! -e "${NVMET_CFS}/subsystems/${tmp}/attr_crdt1" ]]; then
+ rmdir "${NVMET_CFS}/subsystems/${tmp}"
+ SKIP_REASONS+=("nvmet does not support attr_crdt1/2/3")
+ return 1
+ fi
+ rmdir "${NVMET_CFS}/subsystems/${tmp}"
+ return 0
+}
+
+# Set Identify Controller CRDT values (units of 100ms). Call before connect.
+_nvmet_set_crdt() {
+ local subsysnqn="${def_subsysnqn}"
+ local crdt1=0
+ local crdt2=0
+ local crdt3=0
+ local cfs
+
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --subsysnqn)
+ subsysnqn="$2"
+ shift 2
+ ;;
+ *)
+ crdt1="$1"
+ crdt2="${2:-0}"
+ crdt3="${3:-0}"
+ shift $#
+ ;;
+ esac
+ done
+
+ cfs="${NVMET_CFS}/subsystems/${subsysnqn}"
+ _set_attr "${crdt1}" "${cfs}/attr_crdt1"
+ _set_attr "${crdt2}" "${cfs}/attr_crdt2"
+ _set_attr "${crdt3}" "${cfs}/attr_crdt3"
+}
+
+# List hidden multipath path namespaces sharing uuid with head ns.
+_nvme_path_ns_devs() {
+ local head_ns="$1"
+ local uuid
+ local ns
+ local found=0
+
+ uuid="$(cat "/sys/block/${head_ns}/uuid" 2>/dev/null)" || return 1
+
+ shopt -s nullglob
+ for ns in /sys/block/nvme*c*n*; do
+ [[ -e "${ns}/uuid" ]] || continue
+ if [[ "$(cat "${ns}/uuid")" == "${uuid}" ]]; then
+ basename "${ns}"
+ found=1
+ fi
+ done
+ shopt -u nullglob
+
+ # Fallback: controllers may expose path ns under /sys/class/nvme.
+ if (( found == 0 )); then
+ shopt -s nullglob
+ for ns in /sys/class/nvme/nvme*/nvme*c*n*; do
+ [[ -e "${ns}/uuid" ]] || continue
+ if [[ "$(cat "${ns}/uuid")" == "${uuid}" ]]; then
+ basename "${ns}"
+ found=1
+ fi
+ done
+ shopt -u nullglob
+ fi
+
+ (( found == 1 ))
+}
+
+_nvme_ns_diag_path() {
+ local ns="$1"
+ local attr="$2"
+
+ echo "/sys/block/${ns}/diag/${attr}"
+}
+
+_nvme_get_ns_diag() {
+ local path
+
+ path="$(_nvme_ns_diag_path "$1" "$2")"
+ [[ -e "${path}" ]] || { echo 0; return 1; }
+ cat "${path}"
+}
+
+_nvme_set_ns_diag() {
+ local path
+
+ path="$(_nvme_ns_diag_path "$1" "$2")"
+ [[ -e "${path}" ]] || return 1
+ _set_attr "$3" "${path}"
+}
+
+_nvme_sum_path_diag() {
+ local head_ns="$1"
+ local attr="$2"
+ local path_ns
+ local sum=0
+ local val
+
+ while read -r path_ns; do
+ [[ -z "${path_ns}" ]] && continue
+ val="$(_nvme_get_ns_diag "${path_ns}" "${attr}")"
+ sum=$((sum + val))
+ done < <(_nvme_path_ns_devs "${head_ns}")
+ echo "${sum}"
+}
+
+_nvme_reset_path_diag() {
+ local head_ns="$1"
+ local attr="$2"
+ local path_ns
+
+ while read -r path_ns; do
+ [[ -z "${path_ns}" ]] && continue
+ _nvme_set_ns_diag "${path_ns}" "${attr}" 0 || true
+ done < <(_nvme_path_ns_devs "${head_ns}")
+}
+
+# Fault-inject targets for a namespace: path ns devices if multipath, else the ns.
+_nvme_fault_inject_devs() {
+ local ns="$1"
+ local paths
+
+ paths="$(_nvme_path_ns_devs "${ns}")"
+ if [[ -n "${paths}" ]]; then
+ echo "${paths}"
+ else
+ echo "${ns}"
+ fi
+}
+
+# Resolve the nvme controller sysfs/debugfs name for a namespace device.
+_nvme_ctrl_dev_from_ns() {
+ local ns="$1"
+ local ctrl
+
+ if [[ "${ns}" =~ ^nvme[0-9]+c[0-9]+n[0-9]+$ ]]; then
+ ctrl="$(basename "$(readlink -f "/sys/block/${ns}/device")")"
+ echo "${ctrl}"
+ return 0
+ fi
+ echo "${ns%n*}"
+}
+
+_nvme_now_ms() {
+ echo $(($(date +%s%N) / 1000000))
+}
+
+# Arm host fault inject on a namespace/path device.
+# Args: <dev> <dont_retry> <status> <crd> <times> [probability=100] [verbose=1]
+_nvme_arm_crd_inject() {
+ local dev=$1
+ local dont_retry=$2
+ local status=$3
+ local crd=$4
+ local times=$5
+ local probability=${6:-100}
+ local verbose=${7:-1}
+ local fi="/sys/kernel/debug/${dev}/fault_inject"
+
+ if [[ ! -d "${fi}" ]]; then
+ echo "FAIL: missing ${fi}"
+ return 1
+ fi
+ if [[ ! -e "${fi}/crd" ]]; then
+ echo "FAIL: missing ${fi}/crd"
+ return 1
+ fi
+
+ _set_attr "${verbose}" "${fi}/verbose"
+ _set_attr "${dont_retry}" "${fi}/dont_retry"
+ _set_attr "${status}" "${fi}/status"
+ _set_attr "${crd}" "${fi}/crd"
+ _set_attr "${times}" "${fi}/times"
+ _set_attr "${probability}" "${fi}/probability"
+ return 0
+}
+
+_nvme_disarm_crd_inject() {
+ local fi="/sys/kernel/debug/$1/fault_inject"
+
+ [[ -d "${fi}" ]] || return 0
+ _set_attr 0 "${fi}/probability"
+ _set_attr 0 "${fi}/times"
+ _set_attr 0 "${fi}/crd"
+}
+
+# Timed direct write; prints elapsed milliseconds on stdout.
+_nvme_timed_direct_write() {
+ local dev=$1
+ local start end
+
+ start="$(_nvme_now_ms)"
+ dd if=/dev/zero of="${dev}" bs=4k count=1 oflag=direct status=none \
+ conv=notrunc 2>>"$FULL" || return 1
+ end="$(_nvme_now_ms)"
+ echo $((end - start))
+}
+
+# Background timed direct write. Sets nvme_bg_timed_pid (do NOT call from $()).
+# Writes elapsed ms to <result_file>, or FAIL on I/O error.
+_nvme_bg_timed_direct_write() {
+ local dev=$1
+ local result=$2
+
+ rm -f "${result}"
+ (
+ local start end
+ start="$(_nvme_now_ms)"
+ if dd if=/dev/zero of="${dev}" bs=4k count=1 oflag=direct \
+ status=none conv=notrunc 2>>"$FULL"; then
+ end="$(_nvme_now_ms)"
+ echo $((end - start)) >"${result}"
+ else
+ echo FAIL >"${result}"
+ fi
+ ) &
+ nvme_bg_timed_pid=$!
+}
+
+# Wait until fault_inject times reaches 0 (inject consumed).
+_nvme_wait_inject_consumed() {
+ local fi="/sys/kernel/debug/$1/fault_inject/times"
+ local timeout_ms=${2:-2000}
+ local start
+
+ [[ -e "${fi}" ]] || return 1
+ start="$(_nvme_now_ms)"
+ while (( $(cat "${fi}") > 0 )); do
+ if (( $(_nvme_now_ms) - start > timeout_ms )); then
+ return 1
+ fi
+ sleep 0.01
+ done
+ return 0
+}
+
+# Validate elapsed ms against an expected CRD delay.
+# Args: <label> <elapsed_ms> <expected_ms> [max_ms=expected*2]
+_nvme_check_crd_elapsed() {
+ local label=$1
+ local elapsed=$2
+ local expected=$3
+ local max_ms=${4:-$((expected * 2))}
+
+ if (( elapsed < expected / 2 )); then
+ echo "FAIL: ${label} too fast (${elapsed}ms), expected ~${expected}ms"
+ return 1
+ fi
+ if (( elapsed >= max_ms )); then
+ echo "FAIL: ${label} too slow (${elapsed}ms), expected ~${expected}ms (max ${max_ms}ms)"
+ return 1
+ fi
+ return 0
+}
diff --git a/tests/nvme/070 b/tests/nvme/070
new file mode 100755
index 000000000000..14d7160664a0
--- /dev/null
+++ b/tests/nvme/070
@@ -0,0 +1,95 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Sagi Grimberg <[email protected]>
+#
+# Test NVMe command retry delay (CRD) with the per-request retry timer.
+# Requires nvmet attr_crdt* and host fault_inject/crd.
+
+. tests/nvme/rc
+
+DESCRIPTION="test NVMe CRD per-request retry under fio"
+QUICK=1
+
+# NVME_SC_INTERNAL
+NVME_SC_INTERNAL=0x6
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _have_fio
+ _have_kernel_options FAULT_INJECTION FAULT_INJECTION_DEBUG_FS
+ _require_nvme_trtype_is_fabrics
+ _require_nvmet_crdt
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+inject_crd_retries() {
+ local -a inject_devs
+ local dev
+ local ctrl
+ local i
+
+ mapfile -t inject_devs < <(_nvme_fault_inject_devs "$1")
+ for ((i = 0; i < 5; i++)); do
+ for dev in "${inject_devs[@]}"; do
+ ctrl="$(_nvme_ctrl_dev_from_ns "${dev}")"
+ _nvme_err_inject_setup "${dev}" "${ctrl}"
+ # verbose=1 probability=100 dont_retry=0 status=INTERNAL crd=1 times=8
+ _nvme_enable_crd_err_inject "${dev}" 1 100 0 \
+ "${NVME_SC_INTERNAL}" 1 8
+ done
+ sleep 1
+ for dev in "${inject_devs[@]}"; do
+ ctrl="$(_nvme_ctrl_dev_from_ns "${dev}")"
+ _nvme_disable_err_inject "${dev}"
+ _nvme_err_inject_cleanup "${dev}" "${ctrl}"
+ done
+ sleep 1
+ done
+}
+
+test() {
+ local fio_pid
+ local ns
+ local retries_before
+ local retries_after
+ local inject_dev
+
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+ _nvmet_target_setup
+ # CRDT1 = 5 * 100ms = 500ms
+ _nvmet_set_crdt 5 0 0
+
+ _nvme_connect_subsys
+ ns=$(_find_nvme_ns "${def_subsys_uuid}")
+
+ inject_dev=$(_nvme_fault_inject_devs "${ns}" | head -1)
+ _nvme_set_ns_diag "${inject_dev}" command_retries_count 0 || true
+ retries_before=$(_nvme_get_ns_diag "${inject_dev}" command_retries_count)
+
+ _run_fio_verify_io --filename="/dev/${ns}" \
+ --group_reporting --ramp_time=2 \
+ --time_based --runtime=20 &> "$FULL" &
+ fio_pid=$!
+ sleep 3
+
+ echo "Injecting CRD retries"
+ inject_crd_retries "${ns}"
+
+ wait "${fio_pid}" || echo "FAIL: fio exited with errors (see $FULL)"
+
+ retries_after=$(_nvme_get_ns_diag "${inject_dev}" command_retries_count)
+ if (( retries_after <= retries_before )); then
+ echo "command_retries_count did not increase (${retries_before} -> ${retries_after})"
+ fi
+
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/070.out b/tests/nvme/070.out
new file mode 100644
index 000000000000..bf52d7b13a5e
--- /dev/null
+++ b/tests/nvme/070.out
@@ -0,0 +1,3 @@
+Running nvme/070
+Injecting CRD retries
+Test complete
diff --git a/tests/nvme/071 b/tests/nvme/071
new file mode 100755
index 000000000000..897a88302b75
--- /dev/null
+++ b/tests/nvme/071
@@ -0,0 +1,185 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Sagi Grimberg <[email protected]>
+#
+# Test NVMe multipath failover respects controller CRD. Requires nvmet
+# attr_crdt*, host fault_inject/crd, and nvme_core.multipath=Y.
+
+. tests/nvme/rc
+
+DESCRIPTION="test NVMe CRD multipath failover under fio"
+QUICK=1
+
+# NVME_SC_INTERNAL_PATH_ERROR
+NVME_SC_INTERNAL_PATH_ERROR=0x300
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _have_fio
+ _have_module_param_value nvme_core multipath Y
+ _have_kernel_options FAULT_INJECTION FAULT_INJECTION_DEBUG_FS
+ _require_nvme_trtype_is_fabrics
+ _require_nvmet_crdt
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+dump_fault_inject() {
+ local fi="/sys/kernel/debug/$1/fault_inject"
+ local f
+
+ echo "fault_inject $1:" >> "$FULL"
+ if [[ ! -d "${fi}" ]]; then
+ echo " MISSING ${fi}" >> "$FULL"
+ return
+ fi
+ for f in "${fi}"/*; do
+ echo " $(basename "$f")=$(cat "$f" 2>/dev/null)" >> "$FULL"
+ done
+}
+
+# Arm path-error+CRD inject on a single path; leave the peer path clean.
+arm_crd_failover_inject() {
+ local dev=$1
+ local fi="/sys/kernel/debug/${dev}/fault_inject"
+ local status_val dnr_val crd_val prob_val
+
+ if [[ ! -d "${fi}" ]]; then
+ echo "FAIL: missing ${fi}"
+ dump_fault_inject "${dev}"
+ return 1
+ fi
+ if [[ ! -e "${fi}/crd" ]]; then
+ echo "FAIL: missing ${fi}/crd (rebuild/install nvme-core with CRD fault inject)"
+ dump_fault_inject "${dev}"
+ return 1
+ fi
+
+ # Avoid ctrl-side setup: only the path ns debugfs matters for data I/O.
+ # status/dont_retry/crd before probability. verbose=1 logs each injection.
+ _set_attr 1 "${fi}/verbose"
+ _set_attr 0 "${fi}/dont_retry"
+ _set_attr "${NVME_SC_INTERNAL_PATH_ERROR}" "${fi}/status"
+ _set_attr 1 "${fi}/crd"
+ _set_attr 100 "${fi}/times"
+ _set_attr 100 "${fi}/probability"
+
+ status_val="$(cat "${fi}/status")"
+ dnr_val="$(cat "${fi}/dont_retry")"
+ crd_val="$(cat "${fi}/crd")"
+ prob_val="$(cat "${fi}/probability")"
+ {
+ echo "armed ${dev}: status=${status_val} dont_retry=${dnr_val} crd=${crd_val} probability=${prob_val} times=$(cat "${fi}/times")"
+ } >> "$FULL"
+
+ # debugfs x16 prints as 0x0300
+ if [[ "${status_val}" != "0x0300" && "${status_val}" != "0x300" &&
+ "$((status_val))" -ne "$((NVME_SC_INTERNAL_PATH_ERROR))" ]]; then
+ echo "FAIL: status not set (got ${status_val})"
+ dump_fault_inject "${dev}"
+ return 1
+ fi
+ if [[ "${dnr_val}" != "N" && "${dnr_val}" != "0" ]]; then
+ echo "FAIL: dont_retry not cleared (got ${dnr_val})"
+ dump_fault_inject "${dev}"
+ return 1
+ fi
+ if [[ "${crd_val}" != "1" ]]; then
+ echo "FAIL: crd not set (got ${crd_val})"
+ dump_fault_inject "${dev}"
+ return 1
+ fi
+ if [[ "${prob_val}" != "100" ]]; then
+ echo "FAIL: probability not set (got ${prob_val})"
+ dump_fault_inject "${dev}"
+ return 1
+ fi
+ return 0
+}
+
+disarm_crd_failover_inject() {
+ local fi="/sys/kernel/debug/$1/fault_inject"
+
+ _set_attr 0 "${fi}/probability"
+ _set_attr 0 "${fi}/times"
+ _set_attr 0 "${fi}/crd"
+}
+
+test() {
+ local fio_pid
+ local ns
+ local port
+ local -a ports
+ local -a path_devs
+ local inject_dev
+ local peer_dev
+ local inject_before
+ local inject_after
+
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+ _nvmet_target_setup --ports 2
+ # CRDT1 = 2 * 100ms = 200ms; must be set before connect.
+ _nvmet_set_crdt 2 0 0
+
+ _get_nvmet_ports "${def_subsysnqn}" ports
+ for port in "${ports[@]}"; do
+ _setup_nvmet_port_ana "${port}" 1 "optimized"
+ _nvme_connect_subsys --port "${port}" --no-wait-ns
+ done
+ sleep 1
+
+ ns=$(_find_nvme_ns "${def_subsys_uuid}")
+ mapfile -t path_devs < <(_nvme_path_ns_devs "${ns}")
+ if ((${#path_devs[@]} < 2)); then
+ echo "FAIL: need >=2 path namespaces, found ${#path_devs[@]} (${path_devs[*]})"
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+
+ inject_dev=${path_devs[0]}
+ peer_dev=${path_devs[1]}
+ echo "inject=${inject_dev} peer=${peer_dev}" >> "$FULL"
+ dump_fault_inject "${inject_dev}"
+
+ _nvme_set_ns_diag "${inject_dev}" multipath_failover_count 0 || true
+ _nvme_set_ns_diag "${peer_dev}" multipath_failover_count 0 || true
+ inject_before=$(_nvme_get_ns_diag "${inject_dev}" multipath_failover_count)
+
+ # Arm before fio so we know inject is configured; I/O should failover
+ # to the peer path (no EIO if CRD failover works).
+ echo "Injecting CRD failovers"
+ if ! arm_crd_failover_inject "${inject_dev}"; then
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+
+ # Append fio output; do not truncate diagnostics already in $FULL.
+ _run_fio --name=crd-failover --filename="/dev/${ns}" \
+ --rw=randwrite --direct=1 --ioengine=libaio --bs=4k \
+ --iodepth=16 --time_based --runtime=10 \
+ --group_reporting >>"$FULL" 2>&1 &
+ fio_pid=$!
+
+ wait "${fio_pid}" || echo "FAIL: fio exited with errors (see $FULL)"
+
+ disarm_crd_failover_inject "${inject_dev}"
+
+ inject_after=$(_nvme_get_ns_diag "${inject_dev}" multipath_failover_count)
+ echo "failovers ${inject_dev}: ${inject_before} -> ${inject_after}" >> "$FULL"
+ if (( inject_after <= inject_before )); then
+ echo "FAIL: multipath_failover_count on ${inject_dev} did not increase (${inject_before} -> ${inject_after})"
+ dump_fault_inject "${inject_dev}"
+ fi
+
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/071.out b/tests/nvme/071.out
new file mode 100644
index 000000000000..2fca7041e467
--- /dev/null
+++ b/tests/nvme/071.out
@@ -0,0 +1,3 @@
+Running nvme/071
+Injecting CRD failovers
+Test complete
diff --git a/tests/nvme/072 b/tests/nvme/072
new file mode 100755
index 000000000000..2acda72cdd44
--- /dev/null
+++ b/tests/nvme/072
@@ -0,0 +1,118 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Sagi Grimberg <[email protected]>
+#
+# Verify per-request CRD retry timers are independent: arm CRD2 then CRD1 on
+# two overlapping commands to the same namespace (fault_inject only holds one
+# crd at a time, but once latched on a request the timers run concurrently).
+# Each command must complete near its own CRDT, not a shared controller delay.
+
+. tests/nvme/rc
+
+DESCRIPTION="test NVMe CRD per-request retry timer independence"
+QUICK=1
+
+NVME_SC_INTERNAL=0x6
+# CRDT units are 100ms: CRD1=1s, CRD2=10s
+CRDT1=10
+CRDT2=100
+CRD1_MS=$((CRDT1 * 100))
+CRD2_MS=$((CRDT2 * 100))
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _have_kernel_options FAULT_INJECTION FAULT_INJECTION_DEBUG_FS
+ _require_nvme_trtype_is_fabrics
+ _require_nvmet_crdt
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+test() {
+ local ns
+ local inject_dev
+ local crd2_pid crd1_pid
+ local crd2_result crd1_result
+ local crd2_elapsed crd1_elapsed
+
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+ _nvmet_target_setup
+ _nvmet_set_crdt "${CRDT1}" "${CRDT2}" 0
+
+ _nvme_connect_subsys
+ ns=$(_find_nvme_ns "${def_subsys_uuid}")
+ inject_dev=$(_nvme_fault_inject_devs "${ns}" | head -1)
+
+ echo "ns=${ns} inject=${inject_dev}" >>"$FULL"
+ echo "CRDT CRD1=${CRD1_MS}ms CRD2=${CRD2_MS}ms"
+
+ if [[ ! -e /sys/kernel/debug/${inject_dev}/fault_inject/crd ]]; then
+ echo "FAIL: missing fault_inject/crd on ${inject_dev}"
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+
+ crd2_result="${TMPDIR}/crd2_elapsed"
+ crd1_result="${TMPDIR}/crd1_elapsed"
+
+ echo "Arming CRD2 and starting first write"
+ _nvme_arm_crd_inject "${inject_dev}" 0 "${NVME_SC_INTERNAL}" 2 1 || return 1
+ echo "inject: status=${NVME_SC_INTERNAL} crd=2 times=1" >>"$FULL"
+ _nvme_bg_timed_direct_write "/dev/${ns}" "${crd2_result}"
+ crd2_pid=$nvme_bg_timed_pid
+ echo "CRD2 write pid=${crd2_pid}" >>"$FULL"
+
+ if ! _nvme_wait_inject_consumed "${inject_dev}"; then
+ echo "FAIL: CRD2 inject was not consumed"
+ _nvme_disarm_crd_inject "${inject_dev}"
+ kill "${crd2_pid}" 2>/dev/null || true
+ wait "${crd2_pid}" 2>/dev/null || true
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+ echo "CRD2 latched; arming CRD1 while CRD2 retry is pending" >>"$FULL"
+
+ echo "Arming CRD1 and starting second write (CRD2 still pending)"
+ _nvme_arm_crd_inject "${inject_dev}" 0 "${NVME_SC_INTERNAL}" 1 1 || return 1
+ echo "inject: status=${NVME_SC_INTERNAL} crd=1 times=1" >>"$FULL"
+ _nvme_bg_timed_direct_write "/dev/${ns}" "${crd1_result}"
+ crd1_pid=$nvme_bg_timed_pid
+ echo "CRD1 write pid=${crd1_pid}" >>"$FULL"
+
+ echo "Waiting for both writes (expect CRD1~${CRD1_MS}ms then CRD2~${CRD2_MS}ms)"
+ wait "${crd1_pid}" || true
+ wait "${crd2_pid}" || true
+ _nvme_disarm_crd_inject "${inject_dev}"
+ udevadm settle >/dev/null 2>&1 || true
+
+ crd1_elapsed="$(cat "${crd1_result}" 2>/dev/null || echo FAIL)"
+ crd2_elapsed="$(cat "${crd2_result}" 2>/dev/null || echo FAIL)"
+ echo "CRD1 write elapsed ${crd1_elapsed}ms (expected ~${CRD1_MS}ms)" >>"$FULL"
+ echo "CRD2 write elapsed ${crd2_elapsed}ms (expected ~${CRD2_MS}ms)" >>"$FULL"
+
+ if [[ "${crd1_elapsed}" == "FAIL" || -z "${crd1_elapsed}" ]]; then
+ echo "FAIL: CRD1 write did not complete"
+ else
+ echo "CRD1 write elapsed ${crd1_elapsed}ms" | sed -E 's/(elapsed )[0-9]+/\1NUM/'
+ _nvme_check_crd_elapsed "CRD1 write" "${crd1_elapsed}" "${CRD1_MS}" "$((CRD2_MS / 2))"
+ fi
+ if [[ "${crd2_elapsed}" == "FAIL" || -z "${crd2_elapsed}" ]]; then
+ echo "FAIL: CRD2 write did not complete"
+ else
+ echo "CRD2 write elapsed ${crd2_elapsed}ms" | sed -E 's/(elapsed )[0-9]+/\1NUM/'
+ _nvme_check_crd_elapsed "CRD2 write" "${crd2_elapsed}" "${CRD2_MS}"
+ fi
+
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ udevadm settle >/dev/null 2>&1 || true
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/072.out b/tests/nvme/072.out
new file mode 100644
index 000000000000..d9f4520c8d23
--- /dev/null
+++ b/tests/nvme/072.out
@@ -0,0 +1,8 @@
+Running nvme/072
+CRDT CRD1=1000ms CRD2=10000ms
+Arming CRD2 and starting first write
+Arming CRD1 and starting second write (CRD2 still pending)
+Waiting for both writes (expect CRD1~1000ms then CRD2~10000ms)
+CRD1 write elapsed NUMms
+CRD2 write elapsed NUMms
+Test complete
diff --git a/tests/nvme/073 b/tests/nvme/073
new file mode 100755
index 000000000000..34911392437a
--- /dev/null
+++ b/tests/nvme/073
@@ -0,0 +1,157 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Sagi Grimberg <[email protected]>
+#
+# Verify per-failover CRD timers are independent on the same ns head.
+#
+# path0 stays usable after a path-error+CRD (only current_path is cleared), and
+# with NUMA/default selection the next head I/O typically picks path0 again.
+# So both commands are failed on path0 with different CRDs while their failover
+# timers overlap: CRD2 (~10s) then CRD1 (~1s). path1 is left clean so each
+# request can complete once its own CRDT elapses.
+
+. tests/nvme/rc
+
+DESCRIPTION="test NVMe CRD multipath failover timer independence"
+QUICK=1
+
+NVME_SC_INTERNAL_PATH_ERROR=0x300
+# CRDT units are 100ms: CRD1=1s, CRD2=10s
+CRDT1=10
+CRDT2=100
+CRD1_MS=$((CRDT1 * 100))
+CRD2_MS=$((CRDT2 * 100))
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _have_module_param_value nvme_core multipath Y
+ _have_kernel_options FAULT_INJECTION FAULT_INJECTION_DEBUG_FS
+ _require_nvme_trtype_is_fabrics
+ _require_nvmet_crdt
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+test() {
+ local ns
+ local port
+ local -a ports
+ local -a path_devs
+ local path0
+ local path1
+ local fo_before fo_after
+ local crd2_pid crd1_pid
+ local crd2_result crd1_result
+ local crd2_elapsed crd1_elapsed
+ local sync_start
+
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+ _nvmet_target_setup --ports 2
+ _nvmet_set_crdt "${CRDT1}" "${CRDT2}" 0
+
+ _get_nvmet_ports "${def_subsysnqn}" ports
+ for port in "${ports[@]}"; do
+ _setup_nvmet_port_ana "${port}" 1 "optimized"
+ _nvme_connect_subsys --port "${port}" --no-wait-ns
+ done
+ sleep 1
+
+ ns=$(_find_nvme_ns "${def_subsys_uuid}")
+ mapfile -t path_devs < <(_nvme_path_ns_devs "${ns}")
+ if ((${#path_devs[@]} < 2)); then
+ echo "FAIL: need >=2 path namespaces, found ${#path_devs[@]} (${path_devs[*]})"
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+
+ path0=${path_devs[0]}
+ path1=${path_devs[1]}
+ echo "ns=${ns} path0=${path0} path1=${path1}" >>"$FULL"
+ echo "CRDT CRD1=${CRD1_MS}ms CRD2=${CRD2_MS}ms"
+
+ if [[ ! -e /sys/kernel/debug/${path0}/fault_inject/crd ]]; then
+ echo "FAIL: missing fault_inject/crd on ${path0}"
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+
+ _nvme_set_ns_diag "${path0}" multipath_failover_count 0 || true
+ _nvme_set_ns_diag "${path1}" multipath_failover_count 0 || true
+ fo_before=$(_nvme_get_ns_diag "${path0}" multipath_failover_count)
+
+ crd2_result="${TMPDIR}/crd2_elapsed"
+ crd1_result="${TMPDIR}/crd1_elapsed"
+
+ echo "Arming CRD2 on path0 and starting first write"
+ _nvme_arm_crd_inject "${path0}" 0 "${NVME_SC_INTERNAL_PATH_ERROR}" 2 1 || return 1
+ echo "path0 inject: path_error crd=2 times=1" >>"$FULL"
+ _nvme_bg_timed_direct_write "/dev/${ns}" "${crd2_result}"
+ crd2_pid=$nvme_bg_timed_pid
+ echo "CRD2 write pid=${crd2_pid}" >>"$FULL"
+
+ sync_start="$(_nvme_now_ms)"
+ while (( $(_nvme_get_ns_diag "${path0}" multipath_failover_count) <= fo_before )); do
+ if (( $(_nvme_now_ms) - sync_start > 2000 )); then
+ echo "FAIL: CRD2 failover was not scheduled on ${path0}"
+ _nvme_disarm_crd_inject "${path0}"
+ kill "${crd2_pid}" 2>/dev/null || true
+ wait "${crd2_pid}" 2>/dev/null || true
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ return 1
+ fi
+ sleep 0.01
+ done
+ echo "CRD2 failover pending; path0 still selectable (NUMA/current)" >>"$FULL"
+
+ # Same path again: latch CRD1 while the CRD2 fot is still pending.
+ echo "Arming CRD1 on path0 and starting second write (CRD2 still pending)"
+ _nvme_arm_crd_inject "${path0}" 0 "${NVME_SC_INTERNAL_PATH_ERROR}" 1 1 || return 1
+ echo "path0 inject: path_error crd=1 times=1" >>"$FULL"
+ _nvme_bg_timed_direct_write "/dev/${ns}" "${crd1_result}"
+ crd1_pid=$nvme_bg_timed_pid
+ echo "CRD1 write pid=${crd1_pid}" >>"$FULL"
+
+ echo "Waiting for both writes (expect CRD1~${CRD1_MS}ms then CRD2~${CRD2_MS}ms)"
+ wait "${crd1_pid}" || true
+ wait "${crd2_pid}" || true
+ _nvme_disarm_crd_inject "${path0}"
+ udevadm settle >/dev/null 2>&1 || true
+
+ crd1_elapsed="$(cat "${crd1_result}" 2>/dev/null || echo FAIL)"
+ crd2_elapsed="$(cat "${crd2_result}" 2>/dev/null || echo FAIL)"
+ fo_after=$(_nvme_get_ns_diag "${path0}" multipath_failover_count)
+ echo "CRD1 failover elapsed ${crd1_elapsed}ms (expected ~${CRD1_MS}ms)" >>"$FULL"
+ echo "CRD2 failover elapsed ${crd2_elapsed}ms (expected ~${CRD2_MS}ms)" >>"$FULL"
+ echo "path0 failover count ${fo_before} -> ${fo_after}" >>"$FULL"
+
+ if (( fo_after < fo_before + 2 )); then
+ echo "FAIL: expected two failovers on path0, got ${fo_before} -> ${fo_after}"
+ fi
+
+ if [[ "${crd1_elapsed}" == "FAIL" || -z "${crd1_elapsed}" ]]; then
+ echo "FAIL: CRD1 failover write did not complete"
+ else
+ echo "CRD1 failover elapsed ${crd1_elapsed}ms" | sed -E 's/(elapsed )[0-9]+/\1NUM/'
+ _nvme_check_crd_elapsed "CRD1 failover" "${crd1_elapsed}" "${CRD1_MS}" "$((CRD2_MS / 2))"
+ fi
+ if [[ "${crd2_elapsed}" == "FAIL" || -z "${crd2_elapsed}" ]]; then
+ echo "FAIL: CRD2 failover write did not complete"
+ else
+ echo "CRD2 failover elapsed ${crd2_elapsed}ms" | sed -E 's/(elapsed )[0-9]+/\1NUM/'
+ _nvme_check_crd_elapsed "CRD2 failover" "${crd2_elapsed}" "${CRD2_MS}"
+ fi
+
+ _nvme_disconnect_subsys
+ _nvmet_target_cleanup
+ udevadm settle >/dev/null 2>&1 || true
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/073.out b/tests/nvme/073.out
new file mode 100644
index 000000000000..e771abb14874
--- /dev/null
+++ b/tests/nvme/073.out
@@ -0,0 +1,8 @@
+Running nvme/073
+CRDT CRD1=1000ms CRD2=10000ms
+Arming CRD2 on path0 and starting first write
+Arming CRD1 on path0 and starting second write (CRD2 still pending)
+Waiting for both writes (expect CRD1~1000ms then CRD2~10000ms)
+CRD1 failover elapsed NUMms
+CRD2 failover elapsed NUMms
+Test complete
diff --git a/tests/nvme/rc b/tests/nvme/rc
index 31a0fc59ff4b..f286d9a95cce 100644
--- a/tests/nvme/rc
+++ b/tests/nvme/rc
@@ -505,17 +505,39 @@ _nvme_err_inject_cleanup()
_nvme_enable_err_inject()
{
+ # Set status/dont_retry[/crd] before arming probability/times so concurrent
+ # I/O cannot observe the debugfs defaults (INVALID_OPCODE + DNR).
_set_attr "$2" /sys/kernel/debug/"$1"/fault_inject/verbose
- _set_attr "$3" /sys/kernel/debug/"$1"/fault_inject/probability
_set_attr "$4" /sys/kernel/debug/"$1"/fault_inject/dont_retry
_set_attr "$5" /sys/kernel/debug/"$1"/fault_inject/status
+ if [[ -n "${7:-}" && -e /sys/kernel/debug/"$1"/fault_inject/crd ]]; then
+ _set_attr "$7" /sys/kernel/debug/"$1"/fault_inject/crd
+ fi
_set_attr "$6" /sys/kernel/debug/"$1"/fault_inject/times
+ _set_attr "$3" /sys/kernel/debug/"$1"/fault_inject/probability
+}
+
+# Enable fault injection with a Command Retry Delay (CRD) level (1-3).
+# Args: <dev> <verbose> <probability> <dont_retry> <status> <crd> <times>
+_nvme_enable_crd_err_inject()
+{
+ local crd_file="/sys/kernel/debug/$1/fault_inject/crd"
+
+ if [[ ! -e "${crd_file}" ]]; then
+ echo "FAIL: fault_inject crd attribute missing on $1"
+ return 1
+ fi
+ # Map to _nvme_enable_err_inject args: times then crd.
+ _nvme_enable_err_inject "$1" "$2" "$3" "$4" "$5" "$7" "$6"
}
_nvme_disable_err_inject()
{
_set_attr 0 /sys/kernel/debug/"$1"/fault_inject/probability
_set_attr 0 /sys/kernel/debug/"$1"/fault_inject/times
+ if [[ -e /sys/kernel/debug/"$1"/fault_inject/crd ]]; then
+ _set_attr 0 /sys/kernel/debug/"$1"/fault_inject/crd
+ fi
}
_nvme_enable_passthru_admin_error_logging()
--
2.43.0