fix(btrfs): allocate the swapfile with fallocate instead of dd

On a btrfs root the swapfile was written with dd from /dev/zero, so a
swapfile the size of the RAM cost tens of GiB of writes and stalled
every other writer on the filesystem. A caller that bounds the run, such
as an async Ansible task, killed dd halfway, and the next run found the
wrong size and started over. btrfs accepts a preallocated NOCOW
swapfile and the swap directory already carries +C, so every filesystem
now allocates with fallocate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-23 10:36:04 +02:00
1 parent 43cdce5fba
commit 91e1eff8a6
4 files changed
+19 -34

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ case "${SWAPFORGE}" in
esac
echo "ok: resolved from the installed package, not the source tree"
for binary in findmnt swapoff chmod mkswap swapon fallocate chattr btrfs dd; do
for binary in findmnt swapoff fallocate chmod mkswap swapon chattr btrfs; do
command -v "${binary}" >/dev/null || fail "${binary} is missing from the image"
done
echo "ok: every declared binary is present"
+8 -9
View File
@@ -7,7 +7,6 @@ from swap_forge.core import (
BTRFS_BINARIES,
BTRFS_SWAP_PATH,
DEFAULT_SWAP_PATH,
NON_BTRFS_BINARIES,
REQUIRED_BINARIES,
MissingBinaryError,
create_swap,
@@ -22,28 +21,24 @@ from swap_forge.core import (
class TestRequiredBinaries(unittest.TestCase):
def test_declares_every_external_command_the_package_calls(self):
declared = (
set(REQUIRED_BINARIES) | set(BTRFS_BINARIES) | set(NON_BTRFS_BINARIES)
)
declared = set(REQUIRED_BINARIES) | set(BTRFS_BINARIES)
self.assertEqual(
declared,
{
"findmnt",
"swapoff",
"fallocate",
"chmod",
"mkswap",
"swapon",
"chattr",
"btrfs",
"dd",
"fallocate",
},
)
def test_filesystem_specific_binaries_are_not_declared_as_always_required(self):
self.assertFalse(set(REQUIRED_BINARIES) & set(BTRFS_BINARIES))
self.assertFalse(set(REQUIRED_BINARIES) & set(NON_BTRFS_BINARIES))
def test_missing_binary_is_reported_with_its_name(self):
with mock.patch("swap_forge.core.subprocess.run") as subprocess_run:
@@ -153,7 +148,7 @@ class TestCreateSwap(unittest.TestCase):
self.assertEqual(commands, ["fallocate", "chmod", "mkswap", "swapon"])
self.assertEqual(fstab.read_text(), f"{swapfile} none swap sw 0 0\n")
def test_btrfs_disables_copy_on_write_and_uses_dd(self):
def test_btrfs_disables_copy_on_write_before_fallocate(self):
with tempfile.TemporaryDirectory() as base:
swapfile = Path(base) / "swap" / "swapfile"
fstab = Path(base) / "fstab"
@@ -164,7 +159,11 @@ class TestCreateSwap(unittest.TestCase):
commands = [call.args[0][0] for call in run.call_args_list]
self.assertEqual(
commands, ["chattr", "btrfs", "dd", "chmod", "mkswap", "swapon"]
commands, ["chattr", "btrfs", "fallocate", "chmod", "mkswap", "swapon"]
)
self.assertEqual(
run.call_args_list[2].args[0],
["fallocate", "-l", "1024M", str(swapfile)],
)
self.assertTrue(swapfile.parent.is_dir())