Conversation
AfterPipelineLogger uses StringBuilder and List<string> which are not thread-safe. Multiple modules can call LogOnPipelineEnd() concurrently, leading to potential race conditions. Added explicit lock synchronization to: - LogOnPipelineEnd(): protect _values list modifications - GetOutput(): protect both _stringBuilder and _values access - WriteLogs(): copy _values under lock before iterating 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MethodImplOptions.Synchronized is a legacy synchronization pattern that locks on 'this' which can lead to deadlocks if external code acquires the same lock. Replaced with explicit lock statement using a private lock object for better encapsulation and clarity. Also removed the now-unused System.Runtime.CompilerServices using. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryThis PR adds thread-safety to AfterPipelineLogger and modernizes ModuleLoggerProvider synchronization. Critical Issues1. Race Condition in AfterPipelineLogger.GetOutput() (Line 35-51) The cached output in _stringBuilder can become stale if LogOnPipelineEnd() is called after the first GetOutput() call. Scenario: Thread A calls GetOutput() and builds cache, Thread B calls LogOnPipelineEnd() adding new value, Thread A calls GetOutput() again and gets stale cached output missing the new message. Fix: Either clear the cache in LogOnPipelineEnd() when adding new values, or rebuild the output every time. 2. ModuleLoggerProvider.MakeLogger() Not Thread-Safe (Line 74-80) MakeLogger() modifies shared state (_moduleLogger) but is NOT protected by the lock. Can be called from both GetLogger() (locked) and GetLogger(Type type) (NOT locked). If both are called concurrently, they can both execute the assignment to _moduleLogger without synchronization. Fix: Either protect MakeLogger() with the lock, or protect GetLogger(Type type) with the lock. SuggestionsThe copy-then-iterate pattern in WriteLogs() is good - it prevents holding the lock during I/O. VerdictREQUEST CHANGES - Critical thread-safety bugs found. Both issues must be resolved before merging. |
There was a problem hiding this comment.
Pull request overview
This PR modernizes the logging infrastructure's thread-safety mechanisms by replacing legacy synchronization patterns and adding explicit locking to address concurrent access scenarios.
Key changes:
- Replaced
MethodImplOptions.Synchronizedwith explicit lock pattern inModuleLoggerProvider - Added thread-safety to
AfterPipelineLoggermethods to handle concurrent calls toLogOnPipelineEnd()
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/ModularPipelines/Logging/ModuleLoggerProvider.cs | Modernized synchronization from MethodImplOptions.Synchronized to explicit lock with dedicated lock object for better encapsulation |
| src/ModularPipelines/Logging/AfterPipelineLogger.cs | Added thread-safety using lock synchronization for concurrent access to log buffer across multiple modules |
| public void LogOnPipelineEnd(string value) | ||
| { | ||
| _values.Add(value); | ||
| lock (_lock) | ||
| { | ||
| _values.Add(value); | ||
| } | ||
| } |
There was a problem hiding this comment.
The PR description states this change adds thread-safety to handle concurrent calls to LogOnPipelineEnd, but the existing tests don't verify concurrent access behavior. Consider adding a test that calls LogOnPipelineEnd from multiple threads simultaneously to validate the thread-safety guarantees, especially given the race condition in GetOutput that needs to be fixed.
| lock (_lock) | ||
| { | ||
| return _stringBuilder.ToString(); | ||
| } | ||
| if (_stringBuilder.Length > 0) | ||
| { | ||
| return _stringBuilder.ToString(); | ||
| } | ||
|
|
||
| // Build once and cache | ||
| foreach (var value in _values) | ||
| { | ||
| _stringBuilder.AppendLine(value); | ||
| } | ||
| // Build once and cache | ||
| foreach (var value in _values) | ||
| { | ||
| _stringBuilder.AppendLine(value); | ||
| } | ||
|
|
||
| return _stringBuilder.ToString(); | ||
| return _stringBuilder.ToString(); | ||
| } |
There was a problem hiding this comment.
Race condition: If LogOnPipelineEnd is called after the StringBuilder length check (line 40) but before the foreach loop completes (lines 46-49), the newly added value won't be included in the cached StringBuilder, but it will be present in _values. This creates an inconsistency where subsequent calls to GetOutput will return stale data missing the new value.
To fix this, the caching logic should be removed entirely, or _values should be copied to a local list inside the lock and then processed. The simplest solution is to build the string from _values each time, or clear both _values and _stringBuilder together once the string is built to make the cache immutable.
Fix two critical thread-safety bugs identified in review: 1. AfterPipelineLogger.GetOutput() race condition: - Clear cached _stringBuilder when adding new values - Prevents stale cache if LogOnPipelineEnd() called after GetOutput() 2. ModuleLoggerProvider.MakeLogger() not thread-safe: - Wrap GetLogger(Type type) call to MakeLogger() with lock - Prevents race condition when both GetLogger overloads called concurrently 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes AppliedAddressed both critical thread-safety issues from the review: 1. AfterPipelineLogger.GetOutput() Race Condition
2. ModuleLoggerProvider.MakeLogger() Not Thread-Safe
Commit: 590443f |
SummaryAdds thread-safety to Critical Issues1. AfterPipelineLogger: Broken caching logic in
|
Fix issues identified in second review:
1. AfterPipelineLogger: Use boolean flag for cache validity
- Added _isCacheValid field instead of relying on StringBuilder.Length
- Prevents unnecessary rebuilds when GetOutput() called multiple times
- Clear StringBuilder on rebuild, set flag to true after build
2. ModuleLoggerProvider: Separate caching concerns
- GetLogger(Type type) no longer modifies _moduleLogger cache
- Removes lock from GetLogger(Type type) - doesn't need shared cache
- Renamed MakeLogger to CreateLogger for clarity
- Parameterless GetLogger() properly caches with lock protection
- Avoids potential deadlock by not holding lock during DI resolution
in GetLogger(Type type)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Additional Fixes Applied (Commit 949233f)Addressed the issues from the second review: 1. AfterPipelineLogger: Fixed caching logic
2. ModuleLoggerProvider: Fixed caching separation
|
SummaryThis PR adds thread-safety to Critical Issues1. Unnecessary
|
Fixes Applied (Commit bfdf153)Addressed all review comments: 1. AfterPipelineLogger: Fixed Performance Regression
2. ModuleLoggerProvider: Documented Intentional Behavior ChangeAdded comprehensive XML documentation explaining:
3. Thread-safety DocumentationAdded detailed remarks to both classes explaining:
|
Summary
This PR addresses several items from issue #1812:
LogOnPipelineEnd()concurrentlyChanges
1. Thread-Safety Gap - AfterPipelineLogger (Fixed)
Added thread-safety using explicit
locksynchronization:LogOnPipelineEnd(): Protected_values.Add()with lockGetOutput(): Protected entire method body with lockWriteLogs(): Created a copy of_valuesunder lock before iterating2. Legacy Synchronization Pattern (Fixed)
Replaced
[MethodImpl(MethodImplOptions.Synchronized)]in ModuleLoggerProvider with explicitlockstatement:System.Runtime.CompilerServicesusage_lockobject for better encapsulationthiswhich can cause deadlocks if external code acquires the same lock3. DRY Investigation (No changes needed)
Investigated duration formatting - the codebase already consistently uses
TimeSpanFormatter.ToDisplayString().Test plan
🤖 Generated with Claude Code