fix: Stop deleting estimation files on transient errors#1721
Conversation
Fixes #1543 Changes: - Remove destructive File.Delete call that was triggered by any exception - Validate file name format before parsing (check for '-Sub-' presence) - Only catch IOException for transient errors (file locked, etc.) - Skip gracefully on any error instead of deleting potentially valid files The previous behavior would delete timing estimation files for transient errors like file locks, causing permanent loss of timing data. Files should only be overwritten by SaveSubModuleTimeAsync with correct data, not deleted on read errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryImproves error handling in FileSystemModuleEstimatedTimeProvider by replacing Split with IndexOf, adding explicit pattern validation, and removing automatic file deletion on errors. Critical IssuesThe catch block changed from catch-all to catching only
Recommendation: Either revert to catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException)
{
// Transient or permission error - skip gracefully without deleting
return null;
}The pattern validation improvement (lines 43-50) is excellent and prevents the original IndexOutOfRangeException. Suggestions💡 File cleanup behavior changed: The old code deleted files on any error, the new code doesn't delete them at all. This means corrupted/malformed files will persist and be processed every time. This might be intentional (safer), but worth confirming this is the desired behavior. If a file truly cannot be parsed, it will be retried indefinitely. Verdict |
Address review feedback - catch additional file access exceptions: - IOException (file locked, path too long, etc.) - UnauthorizedAccessException (access denied) - SecurityException (missing permissions) Uses exception filter pattern for clean multi-exception handling.
SummaryThis PR fixes a critical bug where timing estimation files were being deleted on any exception, including transient errors like file locks. Critical IssuesNone found ✅ The fix is well-reasoned:
The exception handling is appropriate because:
SuggestionsNone - the implementation is solid and addresses the issue described in #1543. Verdict✅ APPROVE - No critical issues. This is a good bug fix that prevents data loss. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical issue where timing estimation files were being deleted on any exception, including transient errors. The fix prevents data loss by validating file name format before parsing and removing the destructive File.Delete call.
Key Changes:
- Replace
Split()[1]with safeIndexOfpattern matching to avoid exceptions on invalid file names - Remove destructive
File.Deletecall that destroyed timing data - Catch only
IOExceptioninstead of all exceptions to handle transient I/O errors
| return new SubModuleEstimation(name, time); | ||
| } | ||
| catch | ||
| catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) |
There was a problem hiding this comment.
Catching only IOException is too narrow and may cause unhandled exceptions. The GetEstimatedTimeAsync method on line 53 can throw FormatException (from TimeSpan.Parse on line 83), and File.ReadAllTextAsync can throw other exceptions beyond IOException such as UnauthorizedAccessException, NotSupportedException, ArgumentException, etc. Since the SafeModuleEstimatedTimeProvider wrapper already catches all exceptions and logs them, it's safer to catch Exception here to prevent any parsing or I/O error from propagating.
| try | ||
| { | ||
| var name = Path.GetFileNameWithoutExtension(file.FullName).Split("-Sub-")[1]; | ||
| var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.FullName); | ||
| var subIndex = fileNameWithoutExtension.IndexOf("-Sub-", StringComparison.Ordinal); | ||
|
|
||
| if (subIndex < 0) | ||
| { | ||
| // File doesn't match expected naming pattern - skip gracefully | ||
| return null; | ||
| } | ||
|
|
||
| var name = fileNameWithoutExtension[(subIndex + 5)..]; // 5 = length of "-Sub-" | ||
| var time = await GetEstimatedTimeAsync(file.FullName).ConfigureAwait(false); | ||
| return new SubModuleEstimation(name, time); | ||
| } | ||
| catch | ||
| catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) | ||
| { | ||
| File.Delete(file.FullName); | ||
| // File access error (locked, permissions, etc.) - skip gracefully without deleting | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The new error handling behavior lacks test coverage. Consider adding unit tests to verify that files are preserved when IOException occurs, when the file name format is invalid (no "-Sub-" pattern), and when GetEstimatedTimeAsync throws FormatException. This ensures the fix works as intended and prevents regression.
Summary
Fixes #1543
File.Deletecall that was triggered by any exception-Sub-presence)IOExceptionfor transient errors (file locked, etc.)Problem
The previous implementation would:
Split()[1]which throws if pattern not foundSolution
-Sub-pattern before parsing usingIndexOfIOExceptionfor transient I/O errorsnulland skip instead of deletingGetEstimatedTimeAsynchandle format errors (it already returns a default value)Files will only be updated by
SaveSubModuleTimeAsyncwith correct data, never deleted on read errors.Test plan
🤖 Generated with Claude Code