[PATCH] btrfs: send: reproduce preallocated extents with send stream v2
samho <[email protected]>
| Newsgroups | org.kernel.vger.linux-btrfs |
|---|---|
| Message-ID | <[email protected]> |
A preallocated extent is not reproduced by send. For a full send it is
skipped, so the receiver ends up with a hole and none of the space the
sender reserved:
# fallocate -l 16M /mnt/sub/foo
# btrfs subvolume snapshot -r /mnt/sub /mnt/snap
# btrfs send --proto 2 /mnt/snap | btrfs receive /mnt/dst
# du -h /mnt/snap/foo /mnt/dst/snap/foo
16M /mnt/snap/foo
0 /mnt/dst/snap/foo
For an inode that already exists on the receiving side it is worse: the
range is sent as writes full of zeroes, so the receiver spends the stream
and the disk space to turn a preallocated extent into data.
The comment in process_extent() says that the send spec does not have a
prealloc command yet, which stopped being true with send stream v2 - it
has a fallocate command, and btrfs-progs runs fallocate(2) for it since v2
support was added there. Commit 005b0a0c24e1 ("btrfs: send: use fallocate
for hole punching with send stream v2") already uses it for holes with the
same reasoning.
So send a fallocate for a preallocated extent as well. For an inode that
already exists on the receiving side the range is punched first, because
fallocate on its own leaves the current content in place, which would
leave data where the sender has a preallocated extent.
The part of an extent that starts at or beyond the inode's size is still
skipped. A full send could reproduce it, as the truncate we send once we
are done with the inode grows the file on the receiving side and so does
not drop such a range, but an incremental send clips those ranges away
before it gets here, and making the two agree needs the fallocate to be
ordered after the truncate for a file that shrank. Both skip them for now.
For a file that gets 16M preallocated between two snapshots, the
incremental stream goes from 16781375 bytes, 128 writes of zeroes, down to
289 bytes, and the range ends up as a preallocated extent on the receiving
side instead of written zeroes. A full send of a 16M preallocated file
grows by 20 bytes, the single command for it, and the receiver ends up with
the extent instead of nothing at all. Stream v1 is not affected.
The send group of fstests passes with this, including btrfs/284, which
exercises send stream v2 with fsstress and verifies the result with fssum.
Assisted-by: Claude:claude-opus-5
Signed-off-by: samho <[email protected]>
---
fs/btrfs/send.c | 65 +++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 58 insertions(+), 7 deletions(-)
diff --git a/fs/btrfs/send.c b/fs/btrfs/send.c
index dca3570168c7..028bc983ea2d 100644
--- a/fs/btrfs/send.c
+++ b/fs/btrfs/send.c
@@ -6088,6 +6088,26 @@ static int send_write_or_clone(struct send_ctx *sctx,
}
write_data:
+ ei = btrfs_item_ptr(path->nodes[0], path->slots[0],
+ struct btrfs_file_extent_item);
+ if (btrfs_file_extent_type(path->nodes[0], ei) == BTRFS_FILE_EXTENT_PREALLOC &&
+ proto_cmd_ok(sctx, BTRFS_SEND_C_FALLOCATE)) {
+ /*
+ * The inode exists on the receiving side and the range may hold
+ * anything there, so punch it before allocating it - fallocate
+ * on its own leaves the current content in place, which would
+ * turn a preallocated extent into data.
+ */
+ ret = send_fallocate(sctx, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
+ offset, num_bytes);
+ if (ret < 0)
+ return ret;
+
+ ret = send_fallocate(sctx, 0, offset, num_bytes);
+ sctx->cur_inode_next_write_offset = end;
+ return ret;
+ }
+
ret = send_extent_data(sctx, path, offset, num_bytes);
sctx->cur_inode_next_write_offset = end;
return ret;
@@ -6408,6 +6428,43 @@ static int maybe_send_hole(struct send_ctx *sctx, struct btrfs_path *path,
return ret;
}
+/*
+ * Reproduce a preallocated extent on the receiving side.
+ *
+ * Before send stream v2 there was no way to tell a receiver to allocate a range
+ * without writing to it, so a preallocated extent was skipped and the receiver
+ * ended up with a hole - the space the sender reserved is not reserved there.
+ * Since v2 we have a fallocate command, and btrfs-progs runs fallocate(2) for
+ * it, so use it.
+ *
+ * The part of an extent that starts at or beyond the inode's size is not
+ * reproduced. A full send could do it, as the truncate we send once we are done
+ * with the inode grows the file on the receiving side and so does not drop such
+ * a range, but an incremental send clips those ranges away before it gets here.
+ * Reproducing them only for a full send would have the two disagree about the
+ * same subvolume, so both skip them.
+ */
+static int send_prealloc(struct send_ctx *sctx, struct btrfs_path *path,
+ struct btrfs_key *key)
+{
+ const u64 end = min(btrfs_file_extent_end(path), sctx->cur_inode_size);
+ int ret;
+
+ if (!proto_cmd_ok(sctx, BTRFS_SEND_C_FALLOCATE))
+ return 0;
+
+ if (key->offset >= end)
+ return 0;
+
+ ret = send_fallocate(sctx, 0, key->offset, end - key->offset);
+ if (ret < 0)
+ return ret;
+
+ sctx->cur_inode_next_write_offset = end;
+
+ return 0;
+}
+
static int process_extent(struct send_ctx *sctx,
struct btrfs_path *path,
struct btrfs_key *key)
@@ -6433,14 +6490,8 @@ static int process_extent(struct send_ctx *sctx,
type = btrfs_file_extent_type(path->nodes[0], ei);
if (type == BTRFS_FILE_EXTENT_PREALLOC ||
type == BTRFS_FILE_EXTENT_REG) {
- /*
- * The send spec does not have a prealloc command yet,
- * so just leave a hole for prealloc'ed extents until
- * we have enough commands queued up to justify rev'ing
- * the send spec.
- */
if (type == BTRFS_FILE_EXTENT_PREALLOC)
- return 0;
+ return send_prealloc(sctx, path, key);
/* Have a hole, just skip it. */
if (btrfs_file_extent_disk_bytenr(path->nodes[0], ei) == 0)
--
2.34.1
Disclaimer: The contents of this e-mail message and any attachments are confidential and are intended solely for addressee. The information may also be legally privileged. This transmission is sent in trust, for the sole purpose of delivery to the intended recipient. If you have received this transmission in error, any use, reproduction or dissemination of this transmission is strictly prohibited. If you are not the intended recipient, please immediately notify the sender by reply e-mail or phone and delete this message and its attachments, if any.