[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.