-
Notifications
You must be signed in to change notification settings - Fork 8
Fix issue with progress directory not found in progress sync #37
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
Changes from all commits
6a13373
62da93a
c041e3d
2b57e8d
9839695
fbe295c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,6 +69,17 @@ def on() -> None: | |
|
|
||
| clone_with_custom_name(f"{username}/{fork_name}", PROGRESS_LOCAL_FOLDER_NAME) | ||
|
|
||
| # Verify clone succeeded, else restore local progress before failing | ||
| if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In theory, isn't it still possible for this to work even though the deletion still trails?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If rmtree keeps failing after 20 retries, we raise an exception at rmtree already and won't proceed. This check here is more of a safety check to check that clone succeeds rather than for rmtree. It's more of a safety net, as if clone fails, we can still restore the existing local progress.json file for the user (otherwise if it fails, it will leave users in unexpected state without progress folder). But I'm also ok with leaving this check out to be honest, as i don't feel its entirely necessary. |
||
| os.makedirs(os.path.dirname(local_progress_filepath), exist_ok=True) | ||
| with open(local_progress_filepath, "w") as file: | ||
| file.write(json.dumps(local_progress, indent=2)) | ||
| raise RuntimeError( | ||
| f"Clone failed for {PROGRESS_LOCAL_FOLDER_NAME}. " | ||
| "Your local progress has been restored." | ||
jovnc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "Re-run the command `gitmastery progress sync on` to try again." | ||
| ) | ||
|
|
||
| # To reconcile the difference between local and remote progress, we merge by | ||
| # (exercise_name, start_time) which should be unique | ||
| remote_progress = [] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,35 @@ | ||
| import os | ||
| import shutil | ||
| import stat | ||
| import time | ||
| from pathlib import Path | ||
| from typing import Union | ||
|
|
||
| MAX_DELETE_RETRIES = 20 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we might stall for 4 seconds in total? Seems like a pretty high upper bound. Do we want to lower it for now? I think a max retry of 5 is somewhat reasonable unless we hit an obvious issue
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like we should be safer with a higher number, I increased it from 10 initially. Rationale is if we set too low a threshold, there is a higher likelihood of failure and we will need to update the app again for students, which we want to avoid. |
||
| MAX_RETRY_INTERVAL = 0.2 | ||
|
|
||
|
|
||
| def rmtree(folder_name: Union[str, Path]) -> None: | ||
| """ | ||
| Remove a directory tree. | ||
|
|
||
| Raises RuntimeError if the folder still exists after max retries. | ||
| """ | ||
| if not os.path.exists(folder_name): | ||
| return | ||
|
|
||
| def force_remove_readonly(func, path, _): | ||
| os.chmod(path, stat.S_IWRITE) | ||
| func(path) | ||
|
|
||
| shutil.rmtree(folder_name, onerror=force_remove_readonly) | ||
|
|
||
| # Wait for folder to be fully deleted (Windows can be slow with permissions) | ||
| max_retries = MAX_DELETE_RETRIES | ||
| for _ in range(max_retries): | ||
| if not os.path.exists(folder_name): | ||
| return | ||
| time.sleep(MAX_RETRY_INTERVAL) | ||
|
|
||
| # If folder still exists after retries, raise error | ||
| raise RuntimeError(f"Failed to delete {folder_name} after {max_retries} retries") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can I understand the rationale behind this? Is it because
rmtreemight not fully delete it on Windows, and as a result, we need to re-create the folder explicitly, rather than allowingopen(..., "a")to create it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is to address an existing bug for all OSes. It's more of a separate issue, but I just placed it here for convenience, since it's sort of related.
Idea is because we do a
rmtreeat the top deletes the progress folder, but below we instantly open itThis line throws a
FileNotFoundError. The problem is that open with "a" mode creates the file if it doesn't exist, but does NOT create the parent directory.