#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2026 Peter Lamberg
#
# FS QA Test 355
#
# Test that a resumed snapshot deletion keeps its saved drop progress.
#
# If the drop progress is reset to zero on resume (a commit or unmount before
# the resumed run drops its first reference), the next run restarts from the
# first slot and tries to free blocks still shared with a snapshot again: the
# transaction aborts with "unable to find ref byte nr".
#
. ./common/preamble
_begin_fstest auto quick snapshot subvol

. ./common/attr
. ./common/filter

_fixed_by_kernel_commit xxxxxxxxxxxx \
	"btrfs: keep drop_progress when resuming snapshot deletion"

_require_scratch
_require_attrs user
_require_btrfs_command inspect-internal dump-tree
_require_btrfs_command inspect-internal rootid
_require_command "$TIMEOUT_PROG" timeout

# One big user xattr per file gives roughly one 4K leaf per file. A level 2
# node covers at most ~14600 leaves, so 30000 files make a level 3 tree with a
# few root children of several thousand leaves each.
# The resumed drop has to walk a whole such child before it drops anything,
# which is the window the second unmount must land in.
nfiles=30000
per_dir=1000
pad=$(printf '%3000s' '' | tr ' ' x)

drop_caches()
{
	sync
	echo 3 > /proc/sys/vm/drop_caches
}

# Print the S1 root item's "drop_progress objectid" and "drop_level", or
# "ABSENT" if the root item is gone (deletion finished).
drop_state()
{
	$BTRFS_UTIL_PROG inspect-internal dump-tree -t root $SCRATCH_DEV \
		2>/dev/null | $AWK_PROG -v want=$s1_id '
		/item [0-9]+ key \(/ {
			split($0, a, "("); split(a[2], b, " "); cur = b[1]; next
		}
		cur == want && /drop_progress key/ {
			match($0, /key \([0-9]+/); obj = substr($0, RSTART + 5, RLENGTH - 5)
			match($0, /drop_level [0-9]+/); lvl = substr($0, RSTART + 11, RLENGTH - 11)
			print obj, lvl; found = 1
		}
		END { if (!found) print "ABSENT" }'
}

root_level()
{
	$BTRFS_UTIL_PROG inspect-internal dump-tree -t root $SCRATCH_DEV \
		2>/dev/null | $AWK_PROG -v want=$s1_id '
		/item [0-9]+ key \(/ {
			split($0, a, "("); split(a[2], b, " "); cur = b[1]; next
		}
		cur == want && /^[[:space:]]*level [0-9]+ generation_v2/ {
			match($0, /level [0-9]+/); print substr($0, RSTART + 6, RLENGTH - 6); exit
		}'
}

s2_digest()
{
	$GETFATTR_PROG --absolute-names -R -d -m user.pad $SCRATCH_MNT/S2 2>&1 | \
		_filter_scratch | md5sum | cut -d' ' -f1
}

_scratch_mkfs -n 4096 >> $seqres.full 2>&1 || \
	_notrun "mkfs with nodesize 4096 failed"
_scratch_mount

$BTRFS_UTIL_PROG subvolume create $SCRATCH_MNT/S1 >> $seqres.full
for ((d = 0; d < nfiles / per_dir; d++)); do
	dir=$SCRATCH_MNT/S1/d$d
	mkdir $dir
	(cd $dir && seq -f "f%04.0f" 0 $((per_dir - 1)) | xargs touch &&
	 seq -f "f%04.0f" 0 $((per_dir - 1)) | \
		xargs $SETFATTR_PROG -n user.pad -v "$pad") || \
		_fail "populating S1 failed"
done
s1_id=$($BTRFS_UTIL_PROG inspect-internal rootid $SCRATCH_MNT/S1)

# S2 shares every block of S1, S1 owns them.
$BTRFS_UTIL_PROG subvolume snapshot -r $SCRATCH_MNT/S1 $SCRATCH_MNT/S2 \
	>> $seqres.full
$BTRFS_UTIL_PROG filesystem sync $SCRATCH_MNT
digest_before=$(s2_digest)
level=$(root_level)
echo "S1 id $s1_id root level $level files $nfiles" >> $seqres.full
[ -n "$level" ] && [ $level -ge 3 ] || \
	_notrun "S1 tree too shallow (level $level)"

$BTRFS_UTIL_PROG subvolume delete $SCRATCH_MNT/S1 >> $seqres.full
drop_caches

# First drop run: unmount once at least one root child has been dropped, so
# the saved drop progress is non-zero and at the root level.
for ((i = 0; i < 600; i++)); do
	$BTRFS_UTIL_PROG filesystem sync $SCRATCH_MNT
	read -r obj lvl < <(drop_state)
	[ "$obj" = ABSENT ] && \
		_notrun "subvolume deletion finished before it was interrupted"
	[ "$obj" != 0 ] && [ "$lvl" = "$level" ] && break
	# dump-tree reads through the block device page cache, which goes
	# stale while the fs is mounted
	drop_caches
	sleep 0.1
done
[ "$obj" != 0 ] || _notrun "subvolume deletion made no progress"
_scratch_unmount
echo "first run interrupted at drop key ($obj) level $lvl" >> $seqres.full

# Resumed runs, each interrupted right away. The saved progress must never go
# back to zero.
for ((i = 0; i < 5; i++)); do
	drop_caches
	_scratch_mount
	_scratch_unmount
	read -r obj lvl < <(drop_state)
	echo "resume $i: drop key ($obj) level $lvl" >> $seqres.full
	[ "$obj" = ABSENT ] && break
	if [ "$obj" = 0 ]; then
		echo "drop progress of the deleted subvolume reset to zero on resume"
		break
	fi
done

# Let the deletion finish. With a reset drop progress this tries to free blocks
# still used by S2 again and the transaction aborts.
_scratch_mount
$TIMEOUT_PROG 600 $BTRFS_UTIL_PROG subvolume sync $SCRATCH_MNT \
	>> $seqres.full 2>&1
drop_caches
digest_after=$(s2_digest)
[ "$digest_before" = "$digest_after" ] || \
	echo "S2 content changed after deleting S1"
_scratch_unmount
[ "$(drop_state)" = ABSENT ] || echo "S1 deletion did not finish"

echo "Silence is golden"
status=0
exit
