#! /bin/bash
# SPDX-License-Identifier: GPL-2.0-only
# Copyright 2021 Google LLC
#
# FS QA Test No. 622
#
# Test that when the lazytime mount option is enabled, updates to atime, mtime,
# and ctime are persisted in (at least) the four cases when they should be:
#
# - The inode needs to be updated for some change unrelated to file timestamps
# - Userspace calls fsync(), syncfs(), or sync()
# - The inode is evicted from memory
# - More than dirtytime_expire_seconds have elapsed
#
# This is in part a regression test for kernel commit 1e249cb5b7fc
# ("fs: fix lazytime expiration handling in __writeback_single_inode()").
# This test failed on XFS without that commit.
#
. ./common/preamble
_begin_fstest auto shutdown metadata atime

DIRTY_EXPIRE_CENTISECS_ORIG=$(</proc/sys/vm/dirty_expire_centisecs)
DIRTY_WRITEBACK_CENTISECS_ORIG=$(</proc/sys/vm/dirty_writeback_centisecs)
DIRTYTIME_EXPIRE_SECONDS_ORIG=$(</proc/sys/vm/dirtytime_expire_seconds)

restore_expiration_settings()
{
	echo "$DIRTY_EXPIRE_CENTISECS_ORIG" > /proc/sys/vm/dirty_expire_centisecs
	echo "$DIRTY_WRITEBACK_CENTISECS_ORIG" > /proc/sys/vm/dirty_writeback_centisecs
	echo "$DIRTYTIME_EXPIRE_SECONDS_ORIG" > /proc/sys/vm/dirtytime_expire_seconds
}

# Enable continuous writeback of dirty inodes, so that we don't have to wait
# for the typical 30 seconds default.
__expire_inodes()
{
	echo 1 > /proc/sys/vm/dirty_expire_centisecs
	echo 1 > /proc/sys/vm/dirty_writeback_centisecs
}

# Trigger and wait for writeback of any dirty inodes (not dirtytime inodes).
expire_inodes()
{
	__expire_inodes
	# Userspace doesn't have direct visibility into when inodes are dirty,
	# so the best we can do is sleep for a couple seconds.
	sleep 2
	restore_expiration_settings
}

# Trigger and wait for writeback of any dirtytime inodes.
expire_timestamps()
{
	# Enable immediate expiration of lazytime timestamps, so that we don't
	# have to wait for the typical 24 hours default.  This should quickly
	# turn dirtytime inodes into regular dirty inodes.
	echo 1 > /proc/sys/vm/dirtytime_expire_seconds

	# Enable continuous writeback of dirty inodes.
	__expire_inodes

	# Userspace doesn't have direct visibility into when inodes are dirty,
	# so the best we can do is sleep for a couple seconds.
	sleep 2
	restore_expiration_settings
}

# Override the default cleanup function.
_cleanup()
{
	restore_expiration_settings
	rm -f $tmp.*
}

. ./common/filter

# This test uses the shutdown command, so it has to use the scratch filesystem
# rather than the test filesystem.
_require_scratch
_require_scratch_shutdown
_require_xfs_io_command "pwrite"
_require_xfs_io_command "fsync"
_require_xfs_io_command "syncfs"
# Note that this test doesn't have to check that the filesystem supports
# "lazytime", since "lazytime" is a VFS-level option, and at worst it just will
# be ignored.  This test will still pass if lazytime is ignored, as it only
# tests that timestamp updates are persisted when they should be; it doesn't
# test that timestamp updates aren't persisted when they shouldn't be.

_scratch_mkfs &>> $seqres.full
_scratch_mount

# Create the test file for which we'll update and check the timestamps.
file=$SCRATCH_MNT/file
$XFS_IO_PROG -f $file -c "pwrite 0 100" > /dev/null

# Get the specified timestamp of $file in nanoseconds since the epoch.
get_timestamp()
{
	local timestamp_type=$1

	local arg
	case $timestamp_type in
	atime)	arg=X ;;
	mtime)	arg=Y ;;
	ctime)	arg=Z ;;
	*)	_fail "Unhandled timestamp_type: $timestamp_type" ;;
	esac
	stat -c "%.9${arg}" $file | tr -d '.'
}

do_test()
{
	local timestamp_type=$1
	local persist_method=$2

	echo -e "\n# Testing that lazytime $timestamp_type update is persisted by $persist_method"

	# exfat does not support last metadata change timestamp
	[ "${FSTYP}" == "exfat" -a "${timestamp_type}" == "ctime" ] && return 0

	# Mount the filesystem with lazytime.  If atime is being tested, then
	# also use strictatime, since otherwise the filesystem may default to
	# relatime and not do the atime updates.
	if [[ $timestamp_type == atime ]]; then
		_scratch_cycle_mount lazytime,strictatime
	else
		_scratch_cycle_mount lazytime
	fi

	# Update the specified timestamp on the file.
	local orig_time=$(get_timestamp $timestamp_type)
	sleep 0.1

	# exfat's timestamp for access_time has double seconds granularity
	[ "${FSTYP}" == "exfat" -a "${timestamp_type}" == "atime" ] && sleep 2

	case $timestamp_type in
	atime)
		# Read from the file to update its atime.
		cat $file > /dev/null
		;;
	mtime)
		# Write to the file to update its mtime.  Make sure to not write
		# past the end of the file, as that would change i_size, which
		# would be an inode change which would cause the timestamp to
		# always be written -- thus making the test not detect bugs
		# where the timestamp doesn't get written.
		#
		# Also do the write twice, since XFS updates i_version the first
		# time, which likewise causes mtime to be written.  We want the
		# last thing done to just update mtime.
		$XFS_IO_PROG -f $file -c "pwrite 0 100" > /dev/null
		$XFS_IO_PROG -f $file -c "pwrite 0 100" > /dev/null
		;;
	ctime)
		# It isn't possible to update just ctime, so use 'touch -a'
		# to update both atime and ctime.
		touch -a $file
		;;
	esac
	local expected_time=$(get_timestamp $timestamp_type)
	if (( expected_time <= orig_time )); then
		echo "FAIL: $timestamp_type didn't increase after updating it (in-memory)"
	fi

	# Do something that should cause the timestamp to be persisted.
	case $persist_method in
	other_inode_change)
		# Make a non-timestamp-related change to the inode.
		chmod 777 $file
		if [[ $timestamp_type == ctime ]]; then
			# The inode change will have updated ctime again.
			expected_time=$(get_timestamp ctime)
		fi
		# The inode may have been marked dirty but not actually written
		# yet.  Expire it by tweaking the VM settings and waiting.
		expire_inodes
		;;
	sync)
		# Execute the sync() system call.
		sync
		;;
	fsync)
		# Execute the fsync() system call on the file.
		$XFS_IO_PROG -r $file -c fsync
		;;
	syncfs)
		# Execute the syncfs() system call on the filesystem.
		$XFS_IO_PROG $SCRATCH_MNT -c syncfs
		;;
	eviction)
		# Evict the inode from memory.  In theory, drop_caches should do
		# the trick by itself.  But that actually just dirties the
		# inodes that need a lazytime update.  So we still need to wait
		# for inode writeback too.
		echo 2 > /proc/sys/vm/drop_caches
		expire_inodes
		;;
	expiration)
		# Expire the lazy timestamps via dirtytime_expire_seconds.
		expire_timestamps
		;;
	*)
		_fail "Unhandled persist_method: $persist_method"
	esac

	# Use the shutdown ioctl to abort the filesystem.
	#
	# The timestamp might have just been written to the log and not *fully*
	# persisted yet, so use -f to ensure the log gets flushed.
	_scratch_shutdown -f

	# Now remount the filesystem and verify that the timestamp really got
	# updated as expected.
	_scratch_cycle_mount
	local ondisk_time=$(get_timestamp $timestamp_type)
	if (( ondisk_time != expected_time )); then
		# Fail the test by printing unexpected output rather than by
		# calling _fail(), since we can still run the other test cases.
		echo "FAIL: lazytime $timestamp_type wasn't persisted by $persist_method"
		echo "ondisk_time ($ondisk_time) != expected_time ($expected_time)"
	fi
}

for timestamp_type in atime mtime ctime; do
	do_test $timestamp_type other_inode_change
	do_test $timestamp_type sync
	do_test $timestamp_type fsync
	do_test $timestamp_type syncfs
	do_test $timestamp_type eviction
	do_test $timestamp_type expiration
done

# success, all done
status=0
exit
