-
Notifications
You must be signed in to change notification settings - Fork 1.4k
FileTarget - Fix archive cleanup when only single old file and using MaxArchiveDays #6028
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
Conversation
WalkthroughIntroduces ResolveMaxArchiveFiles(bool) in BaseFileArchiveHandler and replaces inline max-archive resolution with it; adds a guard path when resolved max ≥ matched files and MaxArchiveDays ≤ 0 in DeleteOldFilesBeforeArchive; tightens CleanupFiles single-file/exclusion behavior; adds an extra FixWindowsFileSystemTunneling call in RollingArchiveFileHandler when the initial target file exists and old files were deleted; adds a new unit test and removes a couple of access-time updates in tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Caller
participant BaseHandler
participant FileSystem
Note over BaseHandler: BaseFileArchiveHandler — deletion resolution
Caller->>BaseHandler: DeleteOldFilesBeforeArchive(fileInfos, initialFileOpen)
BaseHandler->>BaseHandler: max = ResolveMaxArchiveFiles(initialFileOpen)
BaseHandler->>FileSystem: match & sort candidate files
alt max >= matchedCount and MaxArchiveDays <= 0
BaseHandler-->>Caller: return (no deletions)
else
BaseHandler->>BaseHandler: CleanupFiles(candidates, max, MaxArchiveDays)
BaseHandler->>FileSystem: evaluate exclusions & ages, yield deletions
BaseHandler-->>Caller: return deletions
end
sequenceDiagram
autonumber
participant Caller
participant RollingHandler
participant BaseHandler
participant FileSystem
Note over RollingHandler: RollingArchiveFileHandler — tunneling invocation for existing target
Caller->>RollingHandler: ArchiveBeforeOpenFile(oldPath, newPath, initialFileOpen)
RollingHandler->>RollingHandler: initialFileExists = initialFileOpen && File.Exists(newFilePath)
RollingHandler->>BaseHandler: DeleteOldFilesBeforeArchive(..., initialFileOpen)
BaseHandler-->>RollingHandler: deletedOldFiles (true/false)
alt initialFileExists and deletedOldFiles
RollingHandler->>FileSystem: FixWindowsFileSystemTunneling(newFilePath) %% added call %%
end
Note over RollingHandler: existing tunneling calls remain unchanged
RollingHandler->>RollingHandler: continue archive flow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs(2 hunks)
🔇 Additional comments (2)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (2)
90-91: LGTM: Valid optimization for early exit.The early exit correctly avoids unnecessary processing when there's sufficient capacity for all files and no days-based constraint is active.
52-117: PR title mentions "weekday-format ddd" but changes appear optimization-focused.The PR title suggests adding support for weekday format "ddd", but the visible changes only add early-exit optimizations. Are there additional changes in other files that implement the weekday format feature, or should the PR title be updated to reflect the optimization focus?
a36ad85 to
7673576
Compare
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs(3 hunks)
🔇 Additional comments (2)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (2)
84-86: LGTM: Clean refactoring with sound optimization.Extracting
maxArchiveFilescalculation to a dedicated helper improves readability. The early-return condition correctly skips cleanup when we have file-count capacity and no time-based constraints are configured.
114-122: Rewrite: Original concern is unfounded after code analysis.The
maxArchiveFiles=0behavior inResolveMaxArchiveFilesonly applies during the initial file open (initialFileOpen=true). After startup, normalMaxArchiveFilesrules resume. Additionally,ShouldDeleteFileappliesmaxArchiveDaysindependently via OR logic—files are deleted if either the count threshold or age threshold is exceeded. The two settings don't conflict; they're independent filters. No issue to address.
7673576 to
837d3be
Compare
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
198-210: Critical: maxArchiveFiles == 0 logic still contradicts NLog specification.The condition at line 205 (
maxArchiveFiles == 0) yields the file for deletion, but according to NLog specification,MaxArchiveFiles=0means "infinite archives" (do not delete based on count). The current logic is inverted:
- Incorrect semantic: When users set
MaxArchiveFiles=0, they expect infinite archives, but the code deletes the file- Age constraint bypass: The code yields without checking
MaxArchiveDays(line 207), so even files newer than the configured age threshold are deletedThis issue was identified in the previous review and remains unaddressed.
Apply this fix to respect both the specification and age constraints:
- else if (maxArchiveFiles == 0) + else if (maxArchiveFiles == 0 && _fileTarget.MaxArchiveDays > 0) { - yield return fileInfos[0]; - yield break; + // Single file: check age constraint before yielding + var fileCreatedTimeUtc = FileInfoHelper.LookupValidFileCreationTimeUtc(fileInfos[0]); + var fileInfoDate = new FileInfoDateTime(fileInfos[0], fileCreatedTimeUtc); + if (ShouldDeleteFile(fileInfoDate, 1, maxArchiveFiles, _fileTarget.MaxArchiveDays)) + { + yield return fileInfos[0]; + } + yield break; }Note: This fix handles the
maxArchiveFiles == 0case whenMaxArchiveDays > 0. You should also verify the overall logic: ifmaxArchiveFiles == 0is intended as a sentinel for "delete all" (viaDeleteOldFileOnStartup), the relationship between user-configuredMaxArchiveFilesvalues and the resolvedmaxArchiveFilesneeds clearer separation to avoid conflating "infinite archives" with "delete all".
🧹 Nitpick comments (2)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
60-61: Refactor for clarity: variable name and logic flow are confusing.The variable
deletedOldFilesis first assignedFile.Exists(newFilePath)(line 60), then updated with an AND operation (line 61). This is logically correct but confusing because:
- The name "deletedOldFiles" suggests it tracks whether files were deleted, but it initially tracks whether the target file exists
- The AND operation
DeleteOldFilesBeforeArchive(...) && deletedOldFilesis backwards for readabilityConsider this clearer alternative:
- bool deletedOldFiles = File.Exists(newFilePath); - deletedOldFiles = DeleteOldFilesBeforeArchive(newFilePath, initialFileOpen, parseArchiveSequenceNo) && deletedOldFiles; + bool targetFileExists = File.Exists(newFilePath); + bool deletedOldFiles = DeleteOldFilesBeforeArchive(newFilePath, initialFileOpen, parseArchiveSequenceNo); - if (deletedOldFiles && initialFileOpen) + if (deletedOldFiles && targetFileExists && initialFileOpen) { FixWindowsFileSystemTunneling(newFilePath); }src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
114-122: Simplify ResolveMaxArchiveFiles for readability.The conditional logic at lines 117-118 is difficult to parse:
- Line 117: The condition
(!_fileTarget.ArchiveOldFileOnStartup || _fileTarget.DeleteOldFileOnStartup)requires careful boolean logic analysis- Line 118: The ternary
_fileTarget.DeleteOldFileOnStartup ? 0 : maxArchiveFilesmay assignmaxArchiveFilesto itself (no-op) whenDeleteOldFileOnStartupis falseConsider restructuring for clarity:
private int ResolveMaxArchiveFiles(bool initialFileOpen) { var maxArchiveFiles = _fileTarget.MaxArchiveFiles; - if (initialFileOpen && (!_fileTarget.ArchiveOldFileOnStartup || _fileTarget.DeleteOldFileOnStartup)) - maxArchiveFiles = _fileTarget.DeleteOldFileOnStartup ? 0 : maxArchiveFiles; + if (initialFileOpen && _fileTarget.DeleteOldFileOnStartup) + { + maxArchiveFiles = 0; // Delete all files on startup + } + else if (initialFileOpen && !_fileTarget.ArchiveOldFileOnStartup) + { + // Keep maxArchiveFiles as-is, don't decrement + } else if (maxArchiveFiles > 0) + { maxArchiveFiles -= 1; + } return maxArchiveFiles; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs(3 hunks)src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (3)
DeleteOldFilesBeforeArchive(52-60)DeleteOldFilesBeforeArchive(62-112)FixWindowsFileSystemTunneling(406-421)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: task-list-completed
🔇 Additional comments (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
84-86: No issues found with early-exit logic.The early-exit at lines 84-86 handles all scenarios correctly:
- DeleteOldFileOnStartup case:
ResolveMaxArchiveFilesreturns 0 (line 118). The condition0 >= fileInfos.Lengthis false if files exist (caught by line 82), so no early-exit occurs—cleanup proceeds as intended.- Normal cleanup case: Early-exit skips unnecessary cleanup when we have room for all files and no age-based constraint.
The code distinguishes between two "zero" semantics:
MaxArchiveFiles=0(property, triggersZeroFileArchiveHandler) versusmaxArchiveFiles=0(local variable, signals "delete all files" duringDeleteOldFileOnStartup). The logic correctly navigates both scenarios.
d602d5a to
d5f7a55
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
60-60: LGTM - Correctly handles edge case for weekday-format archive cleanup.The new
initialFileExistscheck and subsequentFixWindowsFileSystemTunnelingcall correctly address scenarios where files with weekday patterns (e.g., "app_Mon.log") might be both the current file and an old archive requiring cleanup. SinceFixWindowsFileSystemTunnelinginternally checks!File.Exists(newFilePath)before acting, this is safe even when the file wasn't actually deleted.Optional micro-optimization: If profiling shows this matters, you could check
File.Exists(newFilePath)again before callingFixWindowsFileSystemTunnelingto avoid the method call overhead when the file still exists after cleanup:if (initialFileExists && deletedOldFiles) { - FixWindowsFileSystemTunneling(newFilePath); + if (!File.Exists(newFilePath)) + { + FixWindowsFileSystemTunneling(newFilePath); + } }However, since
FixWindowsFileSystemTunnelingalready performs this check and the method is lightweight, the current implementation prioritizes code simplicity.Also applies to: 72-75
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs(3 hunks)src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
FixWindowsFileSystemTunneling(406-421)
d5f7a55 to
b1fb06d
Compare
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
198-210: Single-file fast‑path still bypasses MaxArchiveDays; guard or drop the special‑case.With maxArchiveFiles == 0 the file is yielded unconditionally, ignoring MaxArchiveDays. If the intent is that age should still gate deletion, either guard by days or let the general path handle it. This mirrors prior feedback on this block.
Option A (keep fast‑path but respect days):
-else if (maxArchiveFiles == 0) +else if (maxArchiveFiles == 0) { - yield return fileInfos[0]; - yield break; + if (maxArchiveDays <= 0) + { + yield return fileInfos[0]; + } + else + { + var createdUtc = FileInfoHelper.LookupValidFileCreationTimeUtc(fileInfos[0]); + var candidate = new FileInfoDateTime(fileInfos[0], createdUtc); + // Disable count-based deletion here so age decides + if (ShouldDeleteFile(candidate, 1, -1, maxArchiveDays)) + yield return fileInfos[0]; + } + yield break; }Option B (simpler, rely on common logic and avoid surprises):
-else if (maxArchiveFiles == 0) -{ - yield return fileInfos[0]; - yield break; -} +// No single-file shortcut; let common path apply count/age rules uniformly
🧹 Nitpick comments (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
84-87: Add missing fast-path for “unlimited archives and no age policy”.If MaxArchiveFiles < 0 and MaxArchiveDays ≤ 0 there’s nothing to delete; skip enumeration to reduce I/O and log noise.
int maxArchiveFiles = ResolveMaxArchiveFiles(initialFileOpen); -if (maxArchiveFiles >= fileInfos.Length && _fileTarget.MaxArchiveDays <= 0) +// Nothing to delete when count already within limit and no age policy +if (maxArchiveFiles >= fileInfos.Length && _fileTarget.MaxArchiveDays <= 0) return false; +// Also nothing to delete when unlimited archives and no age policy +if (maxArchiveFiles < 0 && _fileTarget.MaxArchiveDays <= 0) + return false;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs(4 hunks)src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
src/NLog/Common/InternalLogger.cs (1)
InternalLogger(51-511)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: task-list-completed
🔇 Additional comments (2)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (2)
268-268: LGTM: clearer internal log line.
114-122: ResolveMaxArchiveFiles logic needs clarification; confirm behavioral intent before refactoring.The review comment raises a valid semantic concern. Your current code unconditionally coerces the resolved max to
0wheneverDeleteOldFileOnStartup = trueon initial file open—even ifMaxArchiveFilesis configured as negative (infinite). Web search and test results confirm:
- MaxArchiveFiles < 0 means "disabled/unlimited"; positive values keep at most N archive files
- DeleteOldFileOnStartup applies to the active log file at startup and is separate from periodic archive cleanup
The ambiguity: The existing tests do not cover the edge case of
MaxArchiveFiles < 0combined withDeleteOldFileOnStartup = true. The code at line 118 forces cleanup to0, but it's unclear whether this is intentional (e.g., "DeleteOldFileOnStartup means clean all archives at startup") or an unintended side-effect.Before accepting the refactor:
- Confirm with maintainers whether infinite archives should be coerced to
0during DeleteOldFileOnStartup- If yes, the refactoring improves clarity—add a comment explaining why:
DeleteOldFileOnStartup = true forces maxArchiveFiles = 0 to trigger full cleanup, regardless of configured limit- If no (infinite should stay infinite), adjust the logic:
if (DeleteOldFileOnStartup) return initialFileOpen ? 0 : maxArchiveFiles;or similarThe suggested refactor does improve readability, but the behavior change (or confirmation) is the key decision point.
781c236 to
28bbbcd
Compare
28bbbcd to
9b8561f
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
72-75: Avoid duplicate FixWindowsFileSystemTunneling call paths.You now call FixWindowsFileSystemTunneling both inside the early-return branch (Lines 65–68) and here. Consider consolidating into a single post-cleanup condition to reduce duplication, e.g., invoke once when deletedOldFiles is true and initialFileOpen is true; the method itself safely no-ops if the file still exists.
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
114-122: Clarify ResolveMaxArchiveFiles semantics.Helper improves readability. Add a brief comment summarizing the adjustments for initialFileOpen, ArchiveOldFileOnStartup, DeleteOldFileOnStartup, and the “-1/0/>0” cases to prevent regressions and future confusion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs(4 hunks)src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs(2 hunks)tests/NLog.UnitTests/Targets/FileTargetTests.cs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/NLog.UnitTests/Targets/FileTargetTests.cs (2)
tests/NLog.UnitTests/NLogTestBase.cs (3)
IsLinux(369-372)AssertFileContents(121-124)AssertFileContents(126-170)src/NLog/Internal/FileInfoHelper.cs (2)
DateTime(43-46)DateTime(48-62)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
src/NLog/Common/InternalLogger.cs (1)
InternalLogger(51-511)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (1)
FixWindowsFileSystemTunneling(407-422)
🔇 Additional comments (5)
src/NLog/Targets/FileArchiveHandlers/RollingArchiveFileHandler.cs (1)
60-61: Good pre-check for initial existence.Capturing initialFileExists before cleanup correctly detects whether the active file may have been removed by DeleteOldFilesBeforeArchive. LGTM.
tests/NLog.UnitTests/Targets/FileTargetTests.cs (1)
2714-2784: Solid coverage for weekday “ddd” with MaxArchiveDays + size rolling.Test reliably seeds 8 prior days, exercises cleanup and rolling, and validates expected sequences and counts. LGTM.
src/NLog/Targets/FileArchiveHandlers/BaseFileArchiveHandler.cs (3)
84-87: Early-exit guard is correct and reduces I/O.Skipping cleanup when maxArchiveFiles ≥ file count and no MaxArchiveDays avoids unnecessary enumeration. LGTM.
198-210: Single-file path: early-yield with yield break is correct.The yield + yield break prevents duplicate enumeration and keeps behavior deterministic. LGTM.
265-269: Use of TimeSource.Current for age checks is appropriate.Converting from system creation time via FromSystemTime keeps tests and production in sync with custom time sources. LGTM.
|



Fixed bug in archive-cleanup where it skipped cleanup when wildcard only finds single file. Fixed resetting of Windows Tunneling File-Timestamp when recycling old FileName.
Adding support for: