Linux XFS filesystem development
 help / color / mirror / Atom feed
From: Brandon Allard <brandon@redpanda.com>
To: fstests@vger.kernel.org
Cc: zlang@kernel.org, linux-xfs@vger.kernel.org,
	Brandon Allard <brandon@redpanda.com>
Subject: [PATCH] xfs: test NOWAIT DIO writes that span written/unwritten boundaries
Date: Fri, 29 May 2026 15:27:14 -0400	[thread overview]
Message-ID: <20260529192714.21602-1-brandon@redpanda.com> (raw)

Add a regression test exercising IOMAP_NOWAIT direct-IO writes around
the written/unwritten extent boundary that arises from preallocated
sequential append workloads.

Commit 883a790a8440 ("xfs: don't allow NOWAIT DIO across extent
boundaries") added a guard in xfs_direct_write_iomap_begin that returns
-EAGAIN under IOMAP_NOWAIT when the imap returned by xfs_bmapi_read
does not cover the entire requested range. A subsequent kernel patch
coalesces a (NORM, UNWRITTEN) pair into a single in-memory iomap when
the two records are physically contiguous on disk, restoring the
success path for the workload above.

The test covers five scenarios:
  A. NOWAIT DIO inside a single NORM extent must succeed.
  B. NOWAIT DIO inside a single UNWRITTEN extent must succeed.
  C. NOWAIT DIO spans NORM->UNWRITTEN, contiguous must succeed
     (the new code path).
  D. NOWAIT DIO spans UNWRITTEN->NORM, contiguous must return
     -EAGAIN (asymmetric guard; only N->U is safe to merge).
  E. NOWAIT DIO spans NORM->HOLE must return -EAGAIN (hole guard
     from 883a790a8440 preserved).

The scratch filesystem is mounted with -o lazytime so the NOWAIT path
does not bail in the mtime-update transaction (a separate -EAGAIN
source unrelated to extent-boundary handling). It is mkfs'd with
-b size=4096 so byte offsets in the golden output are stable across
host architectures.

Signed-off-by: Brandon Allard <brandon@redpanda.com>

This test exercises behavior introduced by the accompanying kernel
patch "xfs: coalesce contiguous unwritten suffix into iomap for NOWAIT
writes" posted to linux-xfs:
  https://lore.kernel.org/linux-xfs/20260529191100.4142371-1-brandon@redpanda.com/

Scenario C will fail on kernels that do not include that patch. If the
kernel patch is accepted, the XXXXXXXXXXXXX placeholder in
_fixed_by_kernel_commit should be replaced with the real commit SHA.
---
 tests/xfs/842     | 152 ++++++++++++++++++++++++++++++++++++++++++++++
 tests/xfs/842.out |  42 +++++++++++++
 2 files changed, 194 insertions(+)
 create mode 100755 tests/xfs/842
 create mode 100644 tests/xfs/842.out

diff --git a/tests/xfs/842 b/tests/xfs/842
new file mode 100755
index 0000000..ab93dc3
--- /dev/null
+++ b/tests/xfs/842
@@ -0,0 +1,152 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Redpanda Data, Inc.
+#
+# FS QA Test No. 842
+#
+# Test several scenarios for RWF_NOWAIT direct-IO writes on XFS, focused on
+# the WRITTEN<->UNWRITTEN extent boundary case.
+#
+# Background:
+#   Commit 883a790a8440 ("xfs: don't allow NOWAIT DIO across extent
+#   boundaries") added a guard in xfs_direct_write_iomap_begin that returns
+#   -EAGAIN under IOMAP_NOWAIT when the imap returned by xfs_bmapi_read does
+#   not span the entire requested range. The guard prevents NOWAIT callers
+#   from observing short writes when the range needs multiple bios.
+#
+#   For sequential append-DIO workloads with adaptive preallocation, requests
+#   regularly straddle a NORM extent (just-written head) and a physically
+#   contiguous UNWRITTEN extent (preallocated tail). A single bio covers both
+#   safely; no short-write hazard exists. A subsequent fix coalesces the two
+#   records in the in-memory iomap and reports the merged range as UNWRITTEN
+#   so the completion handler converts the suffix normally.
+#
+# Scenarios:
+#   A. NOWAIT DIO inside a single NORM extent          -- must succeed
+#   B. NOWAIT DIO inside a single UNWRITTEN extent     -- must succeed
+#   C. NOWAIT DIO spans NORM -> UNWRITTEN, physically  -- must succeed
+#      contiguous (the fix)
+#   D. NOWAIT DIO spans UNWRITTEN -> NORM, physically  -- must fail (-EAGAIN)
+#      contiguous (coalesce path is intentionally
+#      asymmetric: only N->U is safe to merge)
+#   E. NOWAIT DIO spans NORM -> HOLE                   -- must fail (-EAGAIN)
+#      (hole guard from 883a790a8440 preserved)
+#
+# The scratch filesystem MUST be mounted with -o lazytime; without it the
+# NOWAIT path bails earlier in the mtime-update transaction (separate
+# -EAGAIN source unrelated to extent-boundary handling) and scenarios B/C
+# would return -EAGAIN for the wrong reason.
+#
+# The scratch filesystem is mkfs'd with -b size=4096 so byte offsets in the
+# golden output are stable across host architectures.
+#
+. ./common/preamble
+_begin_fstest auto quick rw prealloc
+
+# Override the default cleanup function.
+_cleanup()
+{
+	cd /
+	rm -f $tmp.*
+	_scratch_unmount 2>/dev/null
+}
+
+# Import common functions.
+. ./common/filter
+
+_require_scratch
+_require_xfs_io_command "falloc"
+_require_xfs_io_command "fiemap"
+_require_xfs_io_command "pwrite" "-N"
+
+_fixed_by_kernel_commit XXXXXXXXXXXXX \
+	"xfs: coalesce contiguous unwritten suffix into iomap for NOWAIT writes"
+
+_scratch_mkfs_xfs -b size=4096 >> $seqres.full 2>&1
+_scratch_mount "-o lazytime"
+
+# Defensive: confirm mkfs honored the requested block size. The offsets in
+# the golden output assume bsize == 4096.
+bsize=$(_get_file_block_size $SCRATCH_MNT)
+[ "$bsize" -eq 4096 ] || _notrun "fs block size is $bsize, expected 4096"
+
+# All offsets/sizes are multiples of the 4096-byte block size enforced above.
+HEAD_BYTES=4096       # 1 block (NORM head after split)
+TAIL_BYTES=32768      # 8 blocks (UNWRITTEN tail / NORM tail)
+ALLOC_BYTES=36864     # 9 blocks (HEAD + TAIL, one allocation)
+SPAN_BYTES=8192       # 2 blocks (spans a single extent boundary)
+
+# Return 0 if the first two extents in $1 are physically adjacent.
+# _filter_xfs_io_fiemap emits one space-separated record per extent:
+#   first_logical last_logical first_physical last_physical (512-byte sectors).
+_extents_are_contiguous()
+{
+	$XFS_IO_PROG -c "fiemap" "$1" 2>/dev/null \
+		| _filter_xfs_io_fiemap \
+		| awk 'NR==1 { end=$4 } NR==2 { exit !(end + 1 == $3) }'
+}
+
+# Scenario A: NOWAIT DIO inside a single NORM extent. Must succeed.
+echo
+echo "== A: NOWAIT DIO inside a single NORM extent (must succeed) =="
+fa=$SCRATCH_MNT/A
+$XFS_IO_PROG -fd -c "pwrite -S 0xaa 0 $ALLOC_BYTES" $fa | _filter_xfs_io
+$XFS_IO_PROG -d -c "pwrite -N -V 1 -S 0xbb -b $SPAN_BYTES 0 $SPAN_BYTES" $fa \
+	| _filter_xfs_io
+od -A d -t x1 -N $SPAN_BYTES $fa
+
+# Scenario B: NOWAIT DIO inside a single UNWRITTEN extent. Must succeed.
+echo
+echo "== B: NOWAIT DIO inside a single UNWRITTEN extent (must succeed) =="
+fb=$SCRATCH_MNT/B
+$XFS_IO_PROG -fdt -c "falloc 0 $ALLOC_BYTES" $fb | _filter_xfs_io
+$XFS_IO_PROG -d -c "pwrite -N -V 1 -S 0xcc -b $SPAN_BYTES 0 $SPAN_BYTES" $fb \
+	| _filter_xfs_io
+od -A d -t x1 -N $SPAN_BYTES $fb
+
+# Scenario C: NOWAIT DIO spans NORM -> UNWRITTEN, physically contiguous.
+echo
+echo "== C: NOWAIT DIO spans NORM->UNWRITTEN, contiguous (must succeed) =="
+fc=$SCRATCH_MNT/C
+# The allocator preserves physical contiguity across the split, which is
+# what we need to exercise the coalesce path.
+$XFS_IO_PROG -fdt -c "falloc 0 $ALLOC_BYTES" $fc | _filter_xfs_io
+$XFS_IO_PROG -d -c "pwrite -S 0xdd 0 $HEAD_BYTES" $fc | _filter_xfs_io
+
+if ! _extents_are_contiguous $fc; then
+	_notrun "C: allocator placed split extents non-contiguously, cannot exercise W<->U coalesce on this run"
+fi
+
+$XFS_IO_PROG -d -c "pwrite -N -V 1 -S 0xee -b $SPAN_BYTES 0 $SPAN_BYTES" $fc \
+	| _filter_xfs_io
+od -A d -t x1 -N $SPAN_BYTES $fc
+
+# Scenario D: NOWAIT DIO spans UNWRITTEN -> NORM (physically contiguous).
+echo
+echo "== D: NOWAIT DIO spans UNWRITTEN->NORM, contiguous (must EAGAIN) =="
+fd=$SCRATCH_MNT/D
+$XFS_IO_PROG -fdt -c "falloc 0 $ALLOC_BYTES" $fd | _filter_xfs_io
+$XFS_IO_PROG -d -c "pwrite -S 0x11 $TAIL_BYTES $HEAD_BYTES" $fd | _filter_xfs_io
+
+if ! _extents_are_contiguous $fd; then
+	_notrun "D: allocator placed split extents non-contiguously, cannot exercise U<->W asymmetric guard on this run"
+fi
+
+# Span ends at offset (TAIL_BYTES + HEAD_BYTES), crosses the U->N boundary.
+span_d_off=$((TAIL_BYTES - HEAD_BYTES))
+$XFS_IO_PROG -d -c "pwrite -N -V 1 -S 0x22 -b $SPAN_BYTES $span_d_off $SPAN_BYTES" $fd
+# Confirm the would-be data was not written: tail block should still be 0x11.
+od -A d -t x1 -j $TAIL_BYTES -N $HEAD_BYTES $fd
+
+# Scenario E: NOWAIT DIO spans NORM -> HOLE.
+echo
+echo "== E: NOWAIT DIO spans NORM->HOLE (must EAGAIN) =="
+fe=$SCRATCH_MNT/E
+$XFS_IO_PROG -fd -c "pwrite -S 0x33 0 $HEAD_BYTES" $fe | _filter_xfs_io
+$XFS_IO_PROG -d -c "pwrite -N -V 1 -S 0x44 -b $SPAN_BYTES 0 $SPAN_BYTES" $fe
+# Confirm: head block must still hold the original 0x33 pattern; if the
+# NOWAIT write had partial-committed, we would see 0x44 here instead.
+od -A d -t x1 -N $HEAD_BYTES $fe
+
+status=0
+exit
diff --git a/tests/xfs/842.out b/tests/xfs/842.out
new file mode 100644
index 0000000..6cbfc2a
--- /dev/null
+++ b/tests/xfs/842.out
@@ -0,0 +1,42 @@
+QA output created by 842
+
+== A: NOWAIT DIO inside a single NORM extent (must succeed) ==
+wrote 36864/36864 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 8192/8192 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+0000000 bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb
+*
+0008192
+
+== B: NOWAIT DIO inside a single UNWRITTEN extent (must succeed) ==
+wrote 8192/8192 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+0000000 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc
+*
+0008192
+
+== C: NOWAIT DIO spans NORM->UNWRITTEN, contiguous (must succeed) ==
+wrote 4096/4096 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 8192/8192 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+0000000 ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee
+*
+0008192
+
+== D: NOWAIT DIO spans UNWRITTEN->NORM, contiguous (must EAGAIN) ==
+wrote 4096/4096 bytes at offset 32768
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+pwrite: Resource temporarily unavailable
+0032768 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11
+*
+0036864
+
+== E: NOWAIT DIO spans NORM->HOLE (must EAGAIN) ==
+wrote 4096/4096 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+pwrite: Resource temporarily unavailable
+0000000 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33
+*
+0004096
-- 
2.52.0


                 reply	other threads:[~2026-05-29 19:27 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260529192714.21602-1-brandon@redpanda.com \
    --to=brandon@redpanda.com \
    --cc=fstests@vger.kernel.org \
    --cc=linux-xfs@vger.kernel.org \
    --cc=zlang@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox