LogFactory - Flush / FlushAsync asynchronous completion outside Target lock#6101
LogFactory - Flush / FlushAsync asynchronous completion outside Target lock#6101
Conversation
WalkthroughRefactors parallel-flush exception aggregation to use a lazily-initialized, thread-safe exception collection and changes synchronous continuation invocations to asynchronously-started completions to avoid potential deadlocks during parallel flush/continuation flows. (≤50 words) Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant ParallelWorker
participant ExceptionAggregator
participant Continuation
Caller->>ParallelWorker: ForEachItemInParallel(action, continuation)
ParallelWorker->>ParallelWorker: Execute per-item action
alt item throws
ParallelWorker->>ExceptionAggregator: Lazy-init exceptions (Interlocked) and add under lock
end
ParallelWorker->>ParallelWorker: Decrement pending count
alt pending == 0
ParallelWorker->>ExceptionAggregator: GetCombinedException(exceptions ?? empty)
ParallelWorker->>Continuation: Start async task -> invoke continuation(combinedException|null)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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.
🧹 Nitpick comments (2)
src/NLog/Config/LoggingConfiguration.cs (1)
979-995: The async completion pattern correctly avoids potential deadlocks.The
StartAsyncTaskinvocation ensures the flush completion callback executes outside the target lock context, which is the core fix for the deadlock scenario.Similar to the suggestion for
AsyncHelpers.cs, the two branches could be unified since invoking withlastExceptionworks for both null and non-null cases.,
♻️ Optional simplification
if (completed) { // The flush callback happens while holding target lock, so execute the flush completion asynchronously to avoid potential deadlocks. + var flushException = lastException; if (lastException != null) - { InternalLogger.Warn("Flush completed with errors"); - AsyncHelpers.StartAsyncTask(s => - { - ((AsyncContinuation)s).Invoke(lastException); - }, flushCompletion); - } else - { InternalLogger.Debug("Flush completed"); - AsyncHelpers.StartAsyncTask(s => - { - ((AsyncContinuation)s).Invoke(null); - }, flushCompletion); - } + AsyncHelpers.StartAsyncTask(s => + { + ((AsyncContinuation)s).Invoke(flushException); + }, flushCompletion); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/Config/LoggingConfiguration.cs` around lines 979 - 995, The two conditional branches that call AsyncHelpers.StartAsyncTask can be simplified: remove the duplicate branches and call AsyncHelpers.StartAsyncTask once, invoking ((AsyncContinuation)s).Invoke(lastException) so it works for both error and success (lastException may be null); keep the existing InternalLogger message (choose Warn if lastException != null else Debug) or optionally set the log before the single StartAsyncTask call, and reuse the same AsyncContinuation/flushCompletion invocation to avoid duplicated code (refer to AsyncHelpers.StartAsyncTask, AsyncContinuation, lastException, and flushCompletion in LoggingConfiguration.cs).src/NLog/Common/AsyncHelpers.cs (1)
221-237: Consider simplifying the two branches into a single async call.Both branches perform the same operation—invoking the continuation with
combinedException. SincecombinedExceptionis alreadynullin the first branch, you can unify them.♻️ Proposed simplification
var combinedException = GetCombinedException(exceptions ?? ArrayHelper.Empty<Exception>()); // The flush callback happens while holding target lock, so execute the flush completion asynchronously to avoid potential deadlocks. - if (combinedException is null) - { - AsyncHelpers.StartAsyncTask(s => - { - ((AsyncContinuation)s).Invoke(null); - }, asyncContinuation); - } - else + var flushException = combinedException; + AsyncHelpers.StartAsyncTask(s => { - AsyncHelpers.StartAsyncTask(s => - { - ((AsyncContinuation)s).Invoke(combinedException); - }, asyncContinuation); - } + ((AsyncContinuation)s).Invoke(flushException); + }, asyncContinuation);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/Common/AsyncHelpers.cs` around lines 221 - 237, The two branches calling AsyncHelpers.StartAsyncTask can be merged: remove the if/else and call AsyncHelpers.StartAsyncTask once, passing a lambda that casts s to AsyncContinuation and invokes it with combinedException (which may be null) and passing asyncContinuation as the state; update the block around combinedException, AsyncHelpers.StartAsyncTask, and AsyncContinuation to use this single unified call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/NLog/Common/AsyncHelpers.cs`:
- Around line 221-237: The two branches calling AsyncHelpers.StartAsyncTask can
be merged: remove the if/else and call AsyncHelpers.StartAsyncTask once, passing
a lambda that casts s to AsyncContinuation and invokes it with combinedException
(which may be null) and passing asyncContinuation as the state; update the block
around combinedException, AsyncHelpers.StartAsyncTask, and AsyncContinuation to
use this single unified call.
In `@src/NLog/Config/LoggingConfiguration.cs`:
- Around line 979-995: The two conditional branches that call
AsyncHelpers.StartAsyncTask can be simplified: remove the duplicate branches and
call AsyncHelpers.StartAsyncTask once, invoking
((AsyncContinuation)s).Invoke(lastException) so it works for both error and
success (lastException may be null); keep the existing InternalLogger message
(choose Warn if lastException != null else Debug) or optionally set the log
before the single StartAsyncTask call, and reuse the same
AsyncContinuation/flushCompletion invocation to avoid duplicated code (refer to
AsyncHelpers.StartAsyncTask, AsyncContinuation, lastException, and
flushCompletion in LoggingConfiguration.cs).
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/NLog/Common/AsyncHelpers.cssrc/NLog/Config/LoggingConfiguration.cssrc/NLog/Targets/Wrappers/SplitGroupTarget.cs
There was a problem hiding this comment.
Pull request overview
This PR adjusts NLog flush completion behavior to avoid invoking completion callbacks while holding target locks, reducing the risk of deadlocks during Flush / FlushAsync orchestration.
Changes:
- Dispatches
FlushAllTargetscompletion viaThreadPoolinstead of invoking inline under a target lock. - Updates
AsyncHelpers.ForEachItemInParallelto invoke the final continuation asynchronously (and lazily allocate the exception list). - Lazily allocates exception aggregation lists for grouped continuations (e.g.,
SplitGroupTarget) to reduce allocations.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/NLog/Targets/Wrappers/SplitGroupTarget.cs |
Lazily initializes exception aggregation for counted continuations. |
src/NLog/Config/LoggingConfiguration.cs |
Queues flush completion asynchronously to avoid executing under target lock. |
src/NLog/Common/AsyncHelpers.cs |
Lazily initializes exception aggregation; queues final completion to avoid deadlocks; adds locking around combined-exception creation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
d828ea5 to
25dd413
Compare
68cfe5a to
37dddfc
Compare
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/NLog.UnitTests/LogFactoryTests.cs`:
- Around line 164-175: The test currently attaches a continuation to
logFactory.FlushAsync(...) but never asserts that the antecedent task completed
successfully; change the pattern to capture the antecedent Task (e.g., var
flushTask = logFactory.FlushAsync(CancellationToken.None)), attach the
ContinueWith to that task (preserving
TaskContinuationOptions.ExecuteSynchronously), then after waiting for
continuationCompleted assert that flushTask.IsCompletedSuccessfully (or
Assert.True(flushTask.Status == TaskStatus.RanToCompletion) / equivalent) before
asserting continuationFired and nestedFlushError to ensure the antecedent
succeeded and the regression is properly detected.
|



Trying to resolve NLog/NLog.Web#1118