Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download Failure Resilience #956

Merged
merged 7 commits into from
Nov 8, 2022

Conversation

rmartin16
Copy link
Member

@rmartin16 rmartin16 commented Nov 5, 2022

Changes

  • Download to a temporary file
    • A file is now downloaded in to temporary file inside the Briefcase cache directory.
    • If the download succeeds, the temporary file is renamed to the requested filename.
    • As long as Briefcase can exit normally, the temporary file will be deleted if the download fails.

Issues

The other recommendations in #753 are a bit more complicated....so, I didn't implement any of them here so we can at least get this win.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

- Instead of downloading directly to the Briefcase cache, files are
  downloaded to the platform's designated temporary directory.
- If the download succeeds, the temporary file is moved in to the
  Briefcase cache.
- In most cases, the temporary file will be deleted is the download
  fails; one such exception may be Briefcase receiving SIGKILL.
@rmartin16 rmartin16 changed the title Download Failure Robustness/Recovery Download Failure Resilience Nov 5, 2022
@rmartin16 rmartin16 force-pushed the cleanup-failed-downloads branch from 5c2bcb8 to 569b50d Compare November 5, 2022 19:45
Comment on lines 117 to 119
# retrieving the current umask requires changing it...
os.umask(current_umask := os.umask(0o022))
self.tools.os.chmod(filename, 0o664 & ~current_umask)
Copy link
Member Author

@rmartin16 rmartin16 Nov 5, 2022

Choose a reason for hiding this comment

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

The NamedTemporaryFile file is only created with 600 permissions. So, all this umask stuff restores the previous behavior from filename.open("wb").

Copy link
Member

Choose a reason for hiding this comment

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

A longer explanatory comment would be helpful here. The interplay between the 0o022 mask and the and not with 0o644 wan't clear to me.

There's also no test that the resulting file mask is actually followed; there's code coverage, but no real check of the logic.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added (some probably overly verbose) comments about umask. I also tried to implement file permission verification in the tests.....things are really wonky on windows, though...

@rmartin16 rmartin16 marked this pull request as ready for review November 5, 2022 21:24
- By default, temporary files are created in a platform-specific
  temporary directory. This directory may be on a different filesystem
  than the Briefcase cache and make the attempt to move the file to its
  final location a copy/rm operation instead.
- Additionally, some of the downloads are large and if the user has put
  the Briefcase cache on a different filesystem, we should only create
  files in that location.
@rmartin16
Copy link
Member Author

rmartin16 commented Nov 6, 2022

As mentioned in #753:

The only thing to be wary of is the location of the temp file - if we just append ".tmp" to the filename in the final location, we could end up with stray partial download files that are difficult for users to find and are never cleaned up - or, even worse, we could end up with collisions on the temp file name. We either need to be very careful about doing pre/post download cleanup, or make use of temporary directories to ensure that there won't be collisions.

Right now, this PR doesn't address the possibility of *.download files accumulating in the Briefcase cache because of some bizarre unaccounted for problem.

So, should I also make updates to glob the Briefcase cache directory for *.download files at startup and delete them? Seems less than ideal...

Alternatively, perhaps this type of cleanup would be more appropriate as part of "[a] user-accessible mechanism to manually flush the download cache".

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

I'm sure there are edge cases where this will still fail, but they're a lot smaller than the current edge case. 2 testing related comments, and one request for clarification in a docstring, but otherwise this looks really solid.

@@ -86,6 +89,11 @@ def test_new_download_oneshot(mock_tools, url, content_disposition):
# The filename is derived from the URL or header
assert filename == mock_tools.base_path / "downloads" / "something.zip"

# Temporary file was upgraded to intended destination
mock_tools.shutil.move.assert_called_with(mock.ANY, filename)
Copy link
Member

Choose a reason for hiding this comment

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

While this test passes, it's not especially resilient - mock.ANY really does allow any content. This initial "mock.ANY" call is definitely required to verify that a filename was used, but it would be desirable to add checks that the same temporary filename was used for all the subsequent calls.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's fair; I was initially struggling with how to introspect information about the temporary file and I went too far towards "assume it's working if everything else is working." I think my changes now verify the logic is working as designed.

Comment on lines 117 to 119
# retrieving the current umask requires changing it...
os.umask(current_umask := os.umask(0o022))
self.tools.os.chmod(filename, 0o664 & ~current_umask)
Copy link
Member

Choose a reason for hiding this comment

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

A longer explanatory comment would be helpful here. The interplay between the 0o022 mask and the and not with 0o644 wan't clear to me.

There's also no test that the resulting file mask is actually followed; there's code coverage, but no real check of the logic.

- Along with explaining umask usage, the default file permissions were
  updated from 664 to 666 to better mirror `open()`.
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

Awesome - nice work.

Also - Kudos on the first walrus in the codebase :-P

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.

Improve verification and recovery for corrupted downloads
2 participants