fix: Improve pipeline context isolation and thread safety#1449
Conversation
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This PR addresses issue #1417 by implementing proper module-scoped isolation: **Breaking Changes:** - `IEnvironmentContext.ContentDirectory` and `WorkingDirectory` are now readonly (changed from `{ get; set; }` to `{ get; }`) **Improvements:** - Add AsyncLocal cleanup in ModuleRunner to prevent potential context leaks - Replace `List<T>` with `ConcurrentBag<T>` in ModuleLoggerContainer for lock-free thread-safe operations during high-concurrency module execution - Add documentation explaining PipelineContext scoped lifetime and logger caching safety **Files Changed:** - ModuleRunner.cs: Added try/finally to clear AsyncLocal logger context - IEnvironmentContext.cs: Made ContentDirectory/WorkingDirectory readonly with docs - EnvironmentContext.cs: Implementation matches interface changes - PipelineContext.cs: Added XML documentation for scoped DI behavior - ModuleLoggerContainer.cs: Replaced List+locks with ConcurrentBag+Interlocked Closes #1417 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
SummaryImproves pipeline context isolation and thread safety by adding AsyncLocal cleanup, making environment properties immutable, and using thread-safe collections for module logger container. Critical Issues1. AsyncLocal Set Outside Try Block (src/ModularPipelines/Engine/Execution/ModuleRunner.cs:151)Problem: The Fix: Move the AsyncLocal assignment INSIDE the try block: try
{
// Set up logging - MOVED INSIDE try
ModuleLogger.Values.Value = logger;
await ExecuteModuleLifecycle(moduleState, scopedServiceProvider, pipelineContext, executionContext, moduleContext, cancellationToken).ConfigureAwait(false);
}
finally
{
// Clear AsyncLocal to prevent potential leaks
ModuleLogger.Values.Value = null;
}This ensures the cleanup always matches the setup. Suggestions1. Optimize ConcurrentBag Enumeration (src/ModularPipelines/Logging/ModuleLoggerContainer.cs)The public IModuleLogger? GetLogger(Type type)
{
// ToArray() creates a new array on every call
return _loggers.ToArray().FirstOrDefault(logger => logger.GetType() == type);
}Suggestion: Since loggers are rarely removed and lookups happen frequently, consider caching or using a 2. Memory Barrier for Interlocked Flag (src/ModularPipelines/Logging/ModuleLoggerContainer.cs:13)The private volatile int _isDisposed;Or document that Interlocked provides sufficient guarantees (which it does). This is a minor clarity improvement. Previous Review StatusNo previous comments found. Verdict |
- Move AsyncLocal assignment inside try block to guarantee cleanup even if an exception occurs immediately after assignment (critical fix) - Optimize ModuleLoggerContainer with ConcurrentDictionary for O(1) lookups instead of O(n) ConcurrentBag enumeration - Document why volatile is unnecessary when using Interlocked.Exchange (provides full memory barrier) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
SummaryImproves pipeline context isolation and thread safety by fixing AsyncLocal cleanup scope and optimizing logger container lookups. Critical IssuesNone found ✅ SuggestionsNone - the code changes properly address all issues identified in the previous review. Previous Review StatusAll critical issues from the bot's previous review have been addressed:
Verdict✅ APPROVE - No critical issues, all previous feedback properly addressed |
Summary
This PR addresses issue #1417 by implementing proper module-scoped isolation for pipeline context objects.
Changes
AsyncLocal cleanup: Added try/finally block in
ModuleRunner.csto explicitly clearModuleLogger.Values.Valueafter module execution, preventing potential context leaks in edge cases (thread pool reuse, long-running async contexts)Immutable EnvironmentContext properties: Changed
ContentDirectoryandWorkingDirectoryfrom{ get; set; }to{ get; }to prevent modules from accidentally affecting each other's working directory stateThread-safe ModuleLoggerContainer: Replaced
List<ModuleLogger>withConcurrentBag<ModuleLogger>and lock-based synchronization withInterlockedfor lock-free thread-safe operations during high-concurrency module executionDocumentation: Added XML documentation to
PipelineContextexplaining the scoped DI lifetime and why logger caching is safeBreaking Changes
IEnvironmentContext.ContentDirectoryandIEnvironmentContext.WorkingDirectoryare now readonly. Code that was setting these properties will need to be updated to use command options orSystem.Environment.CurrentDirectorydirectly.Test plan
Closes #1417
🤖 Generated with Claude Code