#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2021 Oracle.  All Rights Reserved.
#
# FS QA Test No. 177
#
# Functional test for commit:
#
# f38a032b165d ("xfs: fix I_DONTCACHE")
#
# Functional testing for the I_DONTCACHE inode flag, as set by the BULKSTAT
# ioctl.  This flag neuters the inode cache's tendency to try to hang on to
# incore inodes for a while after the last program closes the file, which
# is helpful for filesystem scanners to avoid trashing the inode cache.
#
# However, the inode cache doesn't always honor the DONTCACHE behavior -- the
# only time it really applies is to cache misses from a bulkstat scan.  If
# any other threads accessed the inode before or immediately after the scan,
# the DONTCACHE flag is ignored.  This includes other scans.
#
# Regrettably, there is no way to poke /only/ XFS inode reclamation directly,
# so we're stuck with setting xfssyncd_centisecs to a low value and sleeping
# while watching the internal inode cache counters.

# unreliable_in_parallel: cache residency is affected by external drop caches
# operations. Hence counting inodes "in cache" often does not reflect what the
# test has actually done.

. ./common/preamble
_begin_fstest auto ioctl unreliable_in_parallel

_cleanup()
{
	cd /
	rm -r -f $tmp.*
	test -n "$old_centisecs" && echo "$old_centisecs" > "$xfs_centisecs_file"
}

# Import common functions.
. ./common/filter


# Modify as appropriate.
_fixed_by_kernel_commit f38a032b165d "xfs: fix I_DONTCACHE"

_require_xfs_io_command "bulkstat"
_require_scratch

# We require /sys/fs/xfs/$device/stats/stats to monitor per-filesystem inode
# cache usage.
_require_fs_sysfs stats/stats

count_xfs_inode_objs() {
	_get_fs_sysfs_attr $SCRATCH_DEV stats/stats | awk '/vnodes/ {print $2}'
}

dump_debug_info() {
	echo "round $1 baseline: $baseline_count high: $high_count fresh: $fresh_count post: $post_count end: $end_count" >> $seqres.full
}

# Background reclamation of disused xfs inodes is scheduled for ($xfssyncd / 6)
# centiseconds after the first inode is tagged for reclamation.  It's not great
# to encode this implementation detail in a test like this, but there isn't
# any means to trigger *only* inode cache reclaim -- actual memory pressure
# can trigger the VFS to drop non-DONTCACHE inodes, which is not what we want.
xfs_centisecs_file=/proc/sys/fs/xfs/xfssyncd_centisecs
test -w "$xfs_centisecs_file" || _notrun "Cannot find xfssyncd_centisecs?"

# Set the syncd knob to the minimum value 100cs (aka 1s) so that we don't have
# to wait forever.
old_centisecs="$(cat "$xfs_centisecs_file")"
echo 100 > "$xfs_centisecs_file" || _notrun "Cannot adjust xfssyncd_centisecs?"
delay_centisecs="$(cat "$xfs_centisecs_file")"

# Sleep one second more than the xfssyncd delay to give background inode
# reclaim enough time to run.
sleep_seconds=$(( ( (99 + (delay_centisecs / 6) ) / 100) + 1))
echo "Will sleep $sleep_seconds seconds to expire inodes" >> $seqres.full

# Force a relatively small file system size to keep the number of rtgroups
# and thus metadata inodes low
_scratch_mkfs_sized $((512 * 1024 * 1024)) >> $seqres.full
_scratch_mount >> $seqres.full

junkdir=$SCRATCH_MNT/$seq.junk

# Sample the baseline count of cached inodes after a fresh remount.
_scratch_cycle_mount
baseline_count=$(count_xfs_inode_objs)

# Create a junk directory with about a thousand files.
nr_files=1024
mkdir -p $junkdir
for ((i = 0; i < nr_files; i++)); do
	touch "$junkdir/$i"
done
new_files=$(find $junkdir | wc -l)
echo "created $new_files files" >> $seqres.full
_within_tolerance "new file count" $new_files $nr_files 5 -v

# Sanity check: Make sure that all those new inodes are still in the cache.
# We assume that memory limits are not so low that reclaim started for a bunch
# of empty files.  We hope there will never be more than 100 metadata inodes
# or automatic mount time scanners.
high_count=$(count_xfs_inode_objs)
echo "cached inodes: $high_count" >> $seqres.full
_within_tolerance "inodes after creating files" $high_count $new_files 0 100 -v

################
# Round 1: Check DONTCACHE behavior when it is invoked once.  The inodes should
# be reclaimed if we wait longer than the reclaim interval.
echo "Round 1"

_scratch_cycle_mount
fresh_count=$(count_xfs_inode_objs)
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
post_count=$(count_xfs_inode_objs)
sleep $sleep_seconds
end_count=$(count_xfs_inode_objs)
dump_debug_info 1

# Even with our greatly reduced reclaim timeout, the inodes should still be in
# memory immediately after the bulkstat concludes.
_within_tolerance "inodes after bulkstat" $post_count $high_count 5 -v

# After we've given inode reclaim time to run, the inodes should no longer be
# cached in memory, which means we should have the fresh count again.
_within_tolerance "inodes after expire" $end_count $fresh_count 5 -v

################
# Round 2: Check DONTCACHE behavior when it is invoked multiple times in rapid
# succession.  The inodes should remain in memory even after reclaim because
# the cache notices repeat DONTCACHE hits and ignores them.
echo "Round 2"

_scratch_cycle_mount
fresh_count=$(count_xfs_inode_objs)
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
post_count=$(count_xfs_inode_objs)
sleep $sleep_seconds
end_count=$(count_xfs_inode_objs)
dump_debug_info 2

# Inodes should still be in memory immediately after the second bulkstat
# concludes and after the reclaim interval.
_within_tolerance "inodes after double bulkstat" $post_count $high_count 5 -v
_within_tolerance "inodes after expire" $end_count $high_count 5 -v

################
# Round 3: Check DONTCACHE evictions when it is invoked repeatedly but with
# enough time between scans for inode reclaim to remove the inodes.
echo "Round 3"

_scratch_cycle_mount
fresh_count=$(count_xfs_inode_objs)
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
sleep $sleep_seconds
post_count=$(count_xfs_inode_objs)
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
sleep $sleep_seconds
end_count=$(count_xfs_inode_objs)
dump_debug_info 3

# Inodes should not still be cached after either scan and reclaim interval.
_within_tolerance "inodes after slow bulkstat 1" $post_count $fresh_count 5 -v
_within_tolerance "inodes after slow bulkstat 2" $end_count $fresh_count 5 -v

################
# Round 4: Check that DONTCACHE has no effect when all the files are read
# immediately after a bulkstat.
echo "Round 4"

_scratch_cycle_mount
fresh_count=$(count_xfs_inode_objs)
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
find $junkdir -type f -print0 | xargs -0 cat > /dev/null
post_count=$(count_xfs_inode_objs)
sleep $sleep_seconds
end_count=$(count_xfs_inode_objs)
dump_debug_info 4

# Inodes should still be cached after the scan/read and the reclaim interval.
_within_tolerance "inodes after bulkstat/read" $post_count $high_count 5 -v
_within_tolerance "inodes after reclaim" $end_count $high_count 5 -v

################
# Round 5: Check that DONTCACHE has no effect if the inodes were already in
# cache due to reader programs.
echo "Round 5"

_scratch_cycle_mount
fresh_count=$(count_xfs_inode_objs)
find $junkdir -type f -print0 | xargs -0 cat > /dev/null
$XFS_IO_PROG -c 'bulkstat' $junkdir > /dev/null
post_count=$(count_xfs_inode_objs)
sleep $sleep_seconds
end_count=$(count_xfs_inode_objs)
dump_debug_info 5

# Inodes should still be cached after the read/scan and the reclaim interval.
_within_tolerance "inodes after read/bulkstat" $post_count $high_count 5 -v
_within_tolerance "inodes after reclaim" $end_count $high_count 5 -v

status=0
exit
