btrfs: reflink: never block on space reservations while the locked range has delalloc
The previous commit made clone_copy_inline_extent() take its transaction
handle before dirtying a folio in the locked destination range, closing a
flushoncommit deadlock for the single-extent inline clone. Two gaps
remained, and one of them was captured live on a test box within hours:
1) A source file can have an inline extent at offset 0 followed by more
items (e.g. created small, then extended by an append). After the
inline extent's data is copied into a folio - leaving delalloc inside
the locked destination range - the clone loop continues to the next
item and btrfs_replace_file_extents() starts more transactions under
the lock. The captured deadlock (drgn against a live hung kernel):
dedupe: btrfs_replace_file_extents() at [4096,40959], blocked in
__reserve_bytes() on a metadata reservation ticket, while
holding the dst range lock with extent_state [0,4095] =
EXTENT_LOCKED | EXTENT_DELALLOC (dirtied by the inline
copy of the source's first item)
reclaim: flush_space() -> btrfs_commit_current_transaction() ->
btrfs_start_delalloc_flush() -> try_to_writeback_inodes_sb()
flusher: find_lock_delalloc_range() on that inode's [0,4095],
blocked on the dedupe's extent lock
Everything else on the filesystem then queues behind the starved
ticket. Note the blocking point here is a reservation ticket, not
TRANS_STATE_COMMIT_START: ticket servicing commits the transaction,
so with flushoncommit ANY blocking reservation made while the locked
range has delalloc can deadlock.
Fix: once the inline copy's inode update is committed, unlock the
dirtied block before the clone continues. Every byte still locked is
clean, so the commit-time flusher never needs our lock, and i_rwsem
(held for the whole remap) keeps writers away from the unlocked block
until the clone finishes.
2) Delalloc may already exist inside the range when it is first locked:
the pre-lock flush waits for ordered extents, but compressed writeback
is asynchronous and may not have created them yet. Check for
EXTENT_DELALLOC after locking; if present, unlock, flush and retry,
giving up with -EAGAIN after a few attempts.
Fixes: 05a5a7621ce6 ("Btrfs: implement full reflink support for inline extents") Assisted-by: Claude:claude-fable-5