Please acknowledge the following before creating a ticket
Description of the bug:
rw=trimwrite issues each block as two consecutive io_us: a DDIR_TRIM
followed by a DDIR_WRITE to the same offset/length (the HOWTO documents it
as "Blocks will be trimmed first, then the same blocks will be written to").
The main loop in do_io() checks its termination conditions
(runtime_exceeded(), and the bytes_issued >= total_bytes budget) once per
io_u, with no awareness of the trim/write pairing (set_rw_ddir() alternates
based on f->last_start[DDIR_WRITE] == f->last_start[DDIR_TRIM], so the trim
and its paired write are two independent trips through the loop). When the
job terminates after a trim has been issued but before its paired write is
generated, the run ends with a "dangling trim": the final block is left
deallocated and its paired write is never issued.
Impact:
- The trimwrite contract ("every trimmed block is written back") is violated
for the final block; its data is deallocated, not rewritten. The job itself
exits 0, so this is silent.
- Any verification workflow that replays the job's
write_iolog (e.g.
read_iolog + replay_skip=trim on a time_based run that looped over the
region more than once) fails with verify: bad magic header 0 on the
dangling range: the log's earlier writes to that block were wiped by the
final trim, and no write follows.
Proposed fix (validated locally: 0/10 dangling trims after, vs 10/10 before;
a loops-bounded run still issues the same op counts, so runs that already
terminated on pair boundaries are unaffected): defer termination while a pair
is open, so the loop issues exactly one more io_u (the paired write) and
terminates on the next iteration.
--- a/backend.c
+++ b/backend.c
@@ -1080,6 +1080,25 @@
*
* Returns number of bytes written and trimmed.
*/
+/*
+ * In trimwrite mode a block is trimmed and then written back by two
+ * consecutive io_us. Terminating between the two leaves the block
+ * deallocated with its paired write never issued (a "dangling trim"),
+ * which breaks replay-based verification of the write log. The pair
+ * state lives in the per-file last_start markers: unequal means a trim
+ * was issued and its paired write is still pending.
+ */
+static bool trimwrite_mid_pair(struct thread_data *td)
+{
+ struct fio_file *f;
+
+ if (!td_trimwrite(td) || td->o.nr_files != 1)
+ return false;
+
+ f = td->files[0];
+ return f->last_start[DDIR_WRITE] != f->last_start[DDIR_TRIM];
+}
+
static void do_io(struct thread_data *td, uint64_t *bytes_done)
{
unsigned int i;
@@ -1144,7 +1163,8 @@
if (runtime_exceeded(td, &td->ts_cache)) {
__update_ts_cache(td);
- if (runtime_exceeded(td, &td->ts_cache)) {
+ if (runtime_exceeded(td, &td->ts_cache) &&
+ !trimwrite_mid_pair(td)) {
fio_mark_td_terminate(td);
break;
}
@@ -1161,6 +1181,7 @@
*/
if (bytes_issued >= total_bytes &&
!td->o.read_iolog_file &&
+ !trimwrite_mid_pair(td) &&
(!td->o.time_based ||
(td->o.time_based && td->o.verify != VERIFY_NONE)))
break;
The helper conservatively bails for nr_files != 1; the pairing markers are
per-file, so a multi-file generalization would just loop over the files.
Environment: Amazon Linux 2, x86_64, kernel loop device over a sparse file
on ext4
fio version: fio-3.42 (release tarball). The termination logic in
do_io() is unchanged at current master, so the bug should be present there
as well.
Reproduction steps
Any discard-capable block device works; a loop device is the easiest:
truncate -s 1G /tmp/backing.img
sudo losetup -f --show /tmp/backing.img # assume /dev/loop0
repro.fio:
[trimwrite]
filename=/dev/loop0
rw=trimwrite
direct=1
ioengine=libaio
iodepth=8
bsrange=4k-128k
time_based=1
runtime=4
write_iolog=/tmp/trimwrite.iolog
serialize_overlap=1
size=64m
Run it, then compare trim and write op counts in the iolog:
sudo fio repro.fio
awk '$3=="trim"{t++;last="trim"} $3=="write"{w++;last="write"} \
END{print "trims="t, "writes="w, "last_op="last}' /tmp/trimwrite.iolog
Observed (3 consecutive runs):
trims=3412 writes=3411 last_op=trim
trims=2412 writes=2411 last_op=trim
trims=2373 writes=2372 last_op=trim
Expected: trims == writes, with the last op for every block pair being the
write. One extra trim with last_op=trim means the final block was
deallocated and never rewritten.
Please acknowledge the following before creating a ticket
Description of the bug:
rw=trimwriteissues each block as two consecutive io_us: aDDIR_TRIMfollowed by a
DDIR_WRITEto the same offset/length (the HOWTO documents itas "Blocks will be trimmed first, then the same blocks will be written to").
The main loop in
do_io()checks its termination conditions(
runtime_exceeded(), and thebytes_issued >= total_bytesbudget) once perio_u, with no awareness of the trim/write pairing (
set_rw_ddir()alternatesbased on
f->last_start[DDIR_WRITE] == f->last_start[DDIR_TRIM], so the trimand its paired write are two independent trips through the loop). When the
job terminates after a trim has been issued but before its paired write is
generated, the run ends with a "dangling trim": the final block is left
deallocated and its paired write is never issued.
Impact:
for the final block; its data is deallocated, not rewritten. The job itself
exits 0, so this is silent.
write_iolog(e.g.read_iolog+replay_skip=trimon atime_basedrun that looped over theregion more than once) fails with
verify: bad magic header 0on thedangling range: the log's earlier writes to that block were wiped by the
final trim, and no write follows.
Proposed fix (validated locally: 0/10 dangling trims after, vs 10/10 before;
a loops-bounded run still issues the same op counts, so runs that already
terminated on pair boundaries are unaffected): defer termination while a pair
is open, so the loop issues exactly one more io_u (the paired write) and
terminates on the next iteration.
The helper conservatively bails for
nr_files != 1; the pairing markers areper-file, so a multi-file generalization would just loop over the files.
Environment: Amazon Linux 2, x86_64, kernel loop device over a sparse file
on ext4
fio version: fio-3.42 (release tarball). The termination logic in
do_io()is unchanged at current master, so the bug should be present thereas well.
Reproduction steps
Any discard-capable block device works; a loop device is the easiest:
truncate -s 1G /tmp/backing.img sudo losetup -f --show /tmp/backing.img # assume /dev/loop0repro.fio:Run it, then compare trim and write op counts in the iolog:
Observed (3 consecutive runs):
Expected:
trims == writes, with the last op for every block pair being thewrite. One extra trim with
last_op=trimmeans the final block wasdeallocated and never rewritten.