Skip to content

Commit 95d0a19

Browse files
committed
Handle broken pipe when destination is full
1 parent d312737 commit 95d0a19

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

rsync_time_machine.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,14 +694,27 @@ def deal_with_no_space_left(
694694
auto_expire: bool,
695695
) -> bool:
696696
"""Deal with no space left on device."""
697-
with open(log_file) as f:
697+
with open(log_file, encoding="utf-8", errors="replace") as f:
698698
log_data = f.read()
699699

700700
no_space_left = re.search(
701701
r"No space left on device \(28\)|Result too large \(34\)",
702702
log_data,
703703
)
704704

705+
if not no_space_left and re.search(r"rsync: \[sender\] write error: Broken pipe \(32\)", log_data):
706+
df_result = run_cmd(f"df -Pk '{dest_folder}'", dest_is_ssh(ssh))
707+
if df_result.returncode == 0:
708+
lines = df_result.stdout.splitlines()
709+
if len(lines) > 1:
710+
try:
711+
available_blocks = int(lines[1].split()[3])
712+
except (IndexError, ValueError):
713+
pass
714+
else:
715+
if available_blocks <= 0:
716+
no_space_left = True
717+
705718
if no_space_left:
706719
if not auto_expire:
707720
log_error(
@@ -727,7 +740,7 @@ def check_rsync_errors(
727740
auto_delete_log: bool, # noqa: FBT001
728741
) -> None:
729742
"""Check rsync errors."""
730-
with open(log_file) as f:
743+
with open(log_file, encoding="utf-8", errors="replace") as f:
731744
log_data = f.read()
732745
if "rsync error:" in log_data:
733746
log_error(

tests/test_app.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
backup,
1919
backup_marker_path,
2020
check_dest_is_backup_folder,
21+
deal_with_no_space_left,
2122
expire_backups,
2223
find,
2324
find_backup_marker,
@@ -199,6 +200,82 @@ def test_find_backup_marker(tmp_path: Path) -> None:
199200
assert find_backup_marker(str(tmp_path), None) == marker_path
200201

201202

203+
def test_deal_with_no_space_left_handles_broken_pipe_when_dest_full(
204+
tmp_path: Path,
205+
monkeypatch: pytest.MonkeyPatch,
206+
) -> None:
207+
"""Broken pipe with a full destination should trigger expiration."""
208+
log_file = tmp_path / "2025-10-12-212400.log"
209+
log_file.write_text("rsync: [sender] write error: Broken pipe (32)\n", encoding="utf-8")
210+
211+
backups = [
212+
"/dest/2025-10-11-120000",
213+
"/dest/2025-10-12-120000",
214+
]
215+
216+
monkeypatch.setattr(rsync_time_machine, "find_backups", Mock(return_value=backups))
217+
218+
expired: list[str] = []
219+
monkeypatch.setattr(
220+
rsync_time_machine,
221+
"expire_backup",
222+
lambda path, ssh: expired.append(path),
223+
)
224+
225+
def fake_run_cmd(cmd: str, ssh: rsync_time_machine.SSH | None = None) -> rsync_time_machine.CmdResult:
226+
if cmd.startswith("df -Pk"):
227+
df_output = (
228+
"Filesystem 1024-blocks Used Available Capacity Mounted on\n"
229+
"/dev/sda1 100 100 0 100% /dest\n"
230+
)
231+
return rsync_time_machine.CmdResult(df_output, "", 0)
232+
return rsync_time_machine.CmdResult("", "", 0)
233+
234+
monkeypatch.setattr(rsync_time_machine, "run_cmd", fake_run_cmd)
235+
236+
should_retry = deal_with_no_space_left(
237+
str(log_file),
238+
"/dest",
239+
ssh=None,
240+
auto_expire=True,
241+
)
242+
243+
assert should_retry is True
244+
assert expired == [sorted(backups)[-1]]
245+
246+
247+
def test_deal_with_no_space_left_ignores_broken_pipe_when_space_available(
248+
tmp_path: Path,
249+
monkeypatch: pytest.MonkeyPatch,
250+
) -> None:
251+
"""Broken pipe without the destination filling up should not expire backups."""
252+
log_file = tmp_path / "2025-10-12-212400.log"
253+
log_file.write_text("rsync: [sender] write error: Broken pipe (32)\n", encoding="utf-8")
254+
255+
monkeypatch.setattr(rsync_time_machine, "find_backups", Mock(return_value=["/dest/2025-10-12-120000", "/dest/2025-10-11-120000"]))
256+
monkeypatch.setattr(rsync_time_machine, "expire_backup", Mock())
257+
258+
def fake_run_cmd(cmd: str, ssh: rsync_time_machine.SSH | None = None) -> rsync_time_machine.CmdResult:
259+
if cmd.startswith("df -Pk"):
260+
df_output = (
261+
"Filesystem 1024-blocks Used Available Capacity Mounted on\n"
262+
"/dev/sda1 100 58 42 58% /dest\n"
263+
)
264+
return rsync_time_machine.CmdResult(df_output, "", 0)
265+
return rsync_time_machine.CmdResult("", "", 0)
266+
267+
monkeypatch.setattr(rsync_time_machine, "run_cmd", fake_run_cmd)
268+
269+
should_retry = deal_with_no_space_left(
270+
str(log_file),
271+
"/dest",
272+
ssh=None,
273+
auto_expire=True,
274+
)
275+
276+
assert should_retry is False
277+
278+
202279
def test_run_cmd() -> None:
203280
"""Test the run_cmd function."""
204281
result = run_cmd("echo 'Hello, World!'")

0 commit comments

Comments
 (0)