fix: Improve timeout handling and fix hanging tests#1451
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>
SummaryAdds timeout handling infrastructure and fixes hanging tests by introducing a TimeoutHelper, refactoring cancellation propagation, and replacing problematic AppData folder tests. Critical Issues1. Potential resource leak in ModuleRunner.cs In ModuleRunner.ExecuteModuleWithPipeline (src/ModularPipelines/Engine/Execution/ModuleRunner.cs:150-161), the AsyncLocal cleanup is added but the actual module execution was moved to ExecuteModuleLifecycle without the corresponding finally block being present in the diff. The try-finally pattern for AsyncLocal cleanup should wrap the entire module execution. Can you verify the full implementation ensures ModuleLogger.Values.Value is always cleared? 2. ConcurrentBag enumeration allocation The change from List to ConcurrentBag in ModuleLoggerContainer is good for thread safety, but calling .ToArray() on every GetLogger call (line 41) creates allocations on what might be a hot path. Consider if GetLogger is called frequently during module execution - if so, this could be a performance regression compared to the previous lock-based approach. Suggestions1. TimeoutHelper edge case: very short timeouts In TimeoutHelper.cs line 91, you schedule the timeout AFTER setting up registration. With extremely short timeouts (microseconds) there could be a theoretical race. This is likely fine for practical timeouts (milliseconds+), but worth noting for robustness. 2. Default test timeout might be aggressive 30-second default timeout (TestHostSettings.DefaultTestTimeout) might be short for complex pipelines or slow CI environments. Consider documenting this clearly so users know to override for complex tests, or increasing to 60 seconds. 3. FolderTests changes look good The replacement of problematic AppData folder searches with controlled temporary folder tests is exactly the right fix. Verdict |
- Add TUnit-inspired TimeoutHelper with clean timeout pattern - Fast path for no timeout (avoids allocations) - Linked token sources for combining cancellation - 1-second grace period for cleanup after timeout - Distinguishes external cancellation from timeout - Refactor ModuleExecutionPipeline to use TimeoutHelper - Replaces redundant CancelAfter + Task.Delay pattern - Cleaner single-point timeout handling - Fix cancellation propagation in ModuleExecutionContext - Make ModuleCancellationTokenSource settable - Use CreateLinkedTokenSource for proper token linking - Fix hanging FolderTests that searched entire AppData folder - Replace with controlled temporary folder structure tests - Tests now complete in ~1.6s vs hanging indefinitely - Add test timeout infrastructure - Default 60s timeout in TestHostSettings - Timeout parameter in ExecutePipelineAsync overloads 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
a1f7b35 to
d63b236
Compare
SummaryAdds robust timeout handling infrastructure and fixes hanging tests by introducing a TimeoutHelper pattern, refactoring cancellation propagation with linked token sources, and replacing problematic folder search tests. Critical IssuesNone found ✅ Suggestions1. ModuleLoggerContainer performance consideration In ModuleLoggerContainer.cs:40, the switch from List with locks to ConcurrentBag is good for thread safety. However, calling .ToArray() on every GetLogger call creates an allocation. Since GetLogger is only called once per module execution (not in hot loops - see ModuleRunner.cs:275 and DependencyWaiter.cs:68), this is acceptable. Just noting for awareness. 2. Consider documenting grace period rationale The 1-second grace period in TimeoutHelper.cs:15 is a good pattern borrowed from TUnit. Consider adding a brief comment explaining why 1 second was chosen (balance between cleanup time and user experience when timeout occurs). 3. Test timeout documentation The 30-second default timeout in TestHostSettings.cs is reasonable for unit tests. The XML doc comment correctly mentions users can override it, which is good for complex integration tests. Previous Review StatusThe github-actions bot raised two concerns that I've verified are non-issues:
Verdict✅ APPROVE - No critical issues. The timeout infrastructure is well-designed, cancellation token linking is correct, and the test fixes properly address the hanging issues. Clean implementation following TUnit patterns. |
Summary
Add TUnit-inspired TimeoutHelper with clean timeout pattern
Refactor ModuleExecutionPipeline to use TimeoutHelper
CancelAfter+Task.DelaypatternFix cancellation propagation in ModuleExecutionContext
ModuleCancellationTokenSourcesettableCreateLinkedTokenSourcefor proper token linkingFix hanging FolderTests that searched entire AppData folder
Add test timeout infrastructure
Test plan
🤖 Generated with Claude Code