Skip to content

fix: handle missing files gracefully in remove_real_and_linked_file (#297) - #337

Open
bhumitschaudhry wants to merge 3 commits into
lyogavin:mainfrom
bhumitschaudhry:fix/delete-original-crash
Open

bhumitschaudhry wants to merge 3 commits into
lyogavin:mainfrom
bhumitschaudhry:fix/delete-original-crash

Conversation

@bhumitschaudhry

Copy link
Copy Markdown

Summary

Fixes #297

The remove_real_and_linked_file() helper had two bugs that caused crashes when delete_original=True was used during model splitting:

  1. targetpath uninitializedtargetpath was only assigned inside an if block, causing UnboundLocalError when the input was a regular file (not a symlink).
  2. Missing file crashFileNotFoundError was not caught, so deleting an already-removed file would crash mid-split, leaving users with partial splits and missing source shards.
  3. Type mismatchos.path.realpath() returns str but callers pass Path objects, breaking the equality comparison.

Changes

air_llm/airllm/utils.py:

  • Normalize to_delete via os.fspath() so Path objects compare correctly against str realpath
  • Initialize targetpath = None before the conditional
  • Catch FileNotFoundError on os.remove() and return gracefully
  • Add os.path.exists() guard before removing the symlink target
  • Add docstring explaining the function's behavior

air_llm/tests/test_delete_original.py (new):

  • Unit tests for regular files, Path inputs, missing files, and (Linux/macOS) symlinks

Root Cause

The original code:

def remove_real_and_linked_file(to_delete):
    if (os.path.realpath(to_delete) != to_delete):
        targetpath = os.path.realpath(to_delete)
    os.remove(to_delete)
    if (targetpath):
         os.remove(targetpath)

Had three bugs:

  • targetpath was undefined when to_delete was a regular file → UnboundLocalError
  • Path objects compared against str realpath → symlink detection broken on Path inputs
  • No error handling for missing files → crash mid-split

Test Results

Ran 8 tests in 0.038s — OK (skipped=3, symlink tests skip on Windows)

Why this matters

delete_original=True is specifically for low-disk environments. A crash after deleting original shards leaves users with a partial split, missing source files, and no clear recovery path except re-downloading very large model artifacts.

Fixes lyogavin#297

The remove_real_and_linked_file() helper had two bugs that caused crashes
when delete_original=True was used during model splitting:

1. targetpath was only assigned inside an if-block, causing
   UnboundLocalError when the input was a regular file (not a symlink).
2. A FileNotFoundError was not caught, so deleting an already-removed
   file would crash mid-split, leaving users with partial splits and
   missing source shards.

Changes:
- Normalize to_delete via os.fspath() so Path objects compare correctly
  against str realpath.
- Initialize targetpath = None before the conditional.
- Catch FileNotFoundError on os.remove() and return gracefully.
- Add os.path.exists() guard before removing the symlink target.
- Add docstring explaining the function's behavior.
- Add tests for regular files, Path inputs, missing files, and
  (Linux/macOS) symlinks.

Co-authored-by: Claude <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 7, 2026 09:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses crashes when delete_original=True triggers remove_real_and_linked_file() during model splitting, making deletion resilient to Path inputs and missing files, and adding unit coverage for the helper.

Changes:

  • Harden remove_real_and_linked_file() against Path vs str comparisons and missing-file deletions.
  • Add a unittest module covering regular files, Path inputs, missing files, and symlink scenarios.
  • Add an additional “standalone” test script under air_llm/tests/.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
air_llm/airllm/utils.py Makes deletion helper safer for Path inputs and missing files; adds docstring describing behavior.
air_llm/tests/test_delete_original.py Adds unit tests for regular files, missing files, and symlink behavior.
air_llm/tests/test_delete_original_standalone.py Adds a standalone script/test module duplicating the helper and validating behavior in isolation.
Suppressed comments (4)

air_llm/tests/test_delete_original.py:81

  • This symlink test should be skipped on Windows for the same reason as the other symlink test (symlink creation often fails without elevated privileges).

    def test_symlink_with_path_object(self):
        """Symlink removal works with Path input too."""

air_llm/tests/test_delete_original.py:103

  • Creating a broken symlink is also Windows-hostile (privilege/dev-mode dependent). Skipping this test on Windows avoids CI failures unrelated to the actual behavior under test.

    def test_broken_symlink_does_not_crash(self):
        """Deleting a symlink whose target is already gone should not raise."""
        link = os.path.join(self.tmpdir, "broken-link.bin")

air_llm/tests/test_delete_original_standalone.py:112

  • Same as above: the skip condition should include macOS (Darwin) if the intent is Linux/macOS.
    @unittest.skipUnless(platform.system() == "Linux", "symlink test requires Linux/macOS")
    def test_symlink_with_path_object(self):

air_llm/tests/test_delete_original_standalone.py:123

  • Same as above: include macOS (Darwin) in the skip condition to match the message and intended coverage.
    @unittest.skipUnless(platform.system() == "Linux", "symlink test requires Linux/macOS")
    def test_broken_symlink_does_not_crash(self):

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread air_llm/airllm/utils.py Outdated
Comment thread air_llm/airllm/utils.py
Comment on lines +199 to +203
"""Remove a file, following symlinks to also remove the target if present.

os.remove(to_delete)
if (targetpath):
os.remove(targetpath)
If *to_delete* is a symlink the resolved target is removed after the link
itself. For regular files the function simply deletes the file.

Comment on lines +62 to +64
# --- symlinks --------------------------------------------------------

def test_symlink_removes_link_and_target(self):
Comment on lines +17 to +35
# ── Copy of the fixed function (from airllm/utils.py) ──────────────────

def remove_real_and_linked_file(to_delete):
"""Remove a file, following symlinks to also remove the target if present."""
to_delete = os.fspath(to_delete)
targetpath = None

realpath = os.path.realpath(to_delete)
if realpath != to_delete:
targetpath = realpath

try:
os.remove(to_delete)
except FileNotFoundError:
return

if targetpath is not None and os.path.exists(targetpath):
os.remove(targetpath)

remove_real_and_linked_file(os.path.join(self.tmpdir, name))
self.assertEqual(os.listdir(self.tmpdir), [])

@unittest.skipUnless(platform.system() == "Linux", "symlink test requires Linux/macOS")
bhumitschaudhry and others added 2 commits August 7, 2026 16:17
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…one test

Address Copilot review suggestions on PR lyogavin#337:

1. Use os.path.islink() instead of realpath != to_delete to properly
   detect symlinks (avoids false positives from path normalization).
2. Wrap target removal in try/except FileNotFoundError for TOCTOU race
   safety under concurrent deletions.
3. Add cross-snapshot warning to docstring about shared HF cache blobs.
4. Add @skipUnless(platform != Windows) to symlink tests in
   test_delete_original.py.
5. Move test_delete_original_standalone.py to scripts/ so unittest
   discovery does not pick up the copied function.
6. Fix macOS skip condition: == Linux -> != Windows.

Co-authored-by: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

delete_original can delete a shard and then crash

2 participants