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