feat: defer module output until pipeline end when live progress enabled#2227
feat: defer module output until pipeline end when live progress enabled#2227
Conversation
SummaryDefers module output until pipeline end when live progress is enabled to prevent console flickering from pause/resume cycles. Critical Issues1. Race Condition in Progress Active State (ConsoleCoordinator.cs:232-233)// Wire up the progress controller for output coordination
_outputCoordinator.SetProgressController(session);
_outputCoordinator.SetProgressActive(true); // Added in PRIssue:
Fix: Move lock (_phaseLock)
{
if (_isProgressActive) { ... }
_isProgressActive = true;
_outputCoordinator.SetProgressActive(true); // Move inside lock
}2. Missing SetProgressActive(false) in ConsoleCoordinator.cs:249The _outputCoordinator.SetProgressController(NoOpProgressController.Instance);
_outputCoordinator.SetProgressActive(false);However, this is inside the Recommendation: Also call 3. Memory Leak - Unbounded Deferred Output Growth (OutputCoordinator.cs:76-80)private readonly List<DeferredModuleOutput> _deferredOutputs = new();Issue: If a pipeline runs for a very long time with many modules and progress enabled, Severity: Medium - unlikely in most scenarios but possible in very large pipelines. Recommendation: Consider a maximum size or periodic flushing strategy for extremely long-running pipelines. 4. PipelineOutputCoordinator Missing Parameter in Constructor (PipelineOutputCoordinator.cs:30-42)The PR diff shows + private readonly IOutputCoordinator _outputCoordinator;
+ IOutputCoordinator outputCoordinator)
+ _outputCoordinator = outputCoordinator;This means the PR correctly adds the dependency. However, I notice the original file I read doesn't have this - this is expected since I read the current main branch, not the PR branch. This is correct in the PR. 5. Timeout Message Change is Misleading (ModuleLogger.cs:173-183)The diff changes the timeout warning message from: -"Module output flush timed out after 30 seconds for {ModuleType}. Some output may be lost."
+"Module output handling timed out after 30 seconds for {ModuleType}."Issue: The new message removes "Some output may be lost", which is critical information for debugging. When output is deferred and the timeout occurs, output will be lost since it won't be flushed. Recommendation: Keep the "Some output may be lost" warning or change to "Output may be deferred until pipeline end." Suggestions1. Test Coverage for Race ConditionsThe new tests (OutputCoordinatorDeferredFlushTests.cs) only test happy paths. Consider adding tests for:
2. Documentation ClarityThe comment in OutputCoordinator.cs:76 says "defer output until pipeline end", but this assumes progress is enabled. Consider clarifying: "defer output until progress ends (if progress is active)" Previous Review StatusNo previous review comments found (permission issues prevented reading comments API). Verdict |
- Fix race condition: move SetProgressActive(true) inside _phaseLock to prevent modules from completing before OutputCoordinator knows progress is active - Add explicit SetProgressActive(false) when creating NoOpProgressSession for state consistency - Restore "Some output may be lost" warning in timeout message
SummaryThis PR defers module output until the pipeline ends when live progress is enabled, preventing console flickering from repeated progress pause/resume cycles. Critical Issues1. Missing disposal registration for IAsyncDisposable OutputCoordinator The PR adds In ModularPipelines, services are managed by the Microsoft.Extensions.DependencyInjection container. While the container does dispose Impact: The safety net Recommendation: Either:
Reference: src/ModularPipelines/DependencyInjection/DependencyInjectionSetup.cs:240-241 shows singleton registration 2. Potential memory leak if pipeline is cancelled before completion In If the pipeline is cancelled/aborted before reaching Impact: In long-running applications that execute multiple pipelines (e.g., a web service), this could accumulate significant memory. Recommendation: Clear
3. Lock contention in OnModuleCompletedAsync Line 74 in lock (_queueLock)
{
_deferredOutputs.Add(new DeferredModuleOutput(...));
}The
Impact: When modules complete rapidly in parallel, this could cause lock contention between deferred storage and immediate flush operations. Recommendation: Consider using a separate lock for Suggestions1. Missing unit test coverage for error scenarios The new test file
2. Race condition comment could be clearer In // CRITICAL: Set OutputCoordinator's progress state inside the lock
// to prevent race conditions where a module completes between
// _isProgressActive = true and OutputCoordinator being notifiedThis is excellent defensive programming, but the comment could specify what would happen without the lock (modules would defer output when they shouldn't, or vice versa). 3. Consider documenting the ordering guarantee The PR description mentions "Output is grouped by module and displayed in completion order" - this is implemented via Previous Review StatusNo previous review comments found. Verdict |
- Add separate _deferredLock for deferred operations to reduce lock contention with immediate flush operations - Clear stale deferred outputs when SetProgressActive(true) is called to prevent memory leaks from previous crashed pipeline runs - Improve DisposeAsync documentation to clarify it's a safety net and the primary flush happens in PipelineOutputScope.DisposeAsync
SummaryDefers module output until pipeline end when live progress is enabled to prevent console flickering from repeated pause/resume cycles. Critical Issues1. IAsyncDisposable Won't Be Called - Safety Net is Ineffective
Impact: The safety net Recommendation: Remove the 2. Memory Leak on Pipeline CancellationWhen
Issue: If a pipeline is cancelled mid-execution (before reaching Impact: In long-running applications that execute multiple pipelines (e.g., a web service running CI/CD pipelines), this accumulates unbounded memory. Example scenario:
Recommendation: Clear public void SetProgressActive(bool isActive)
{
if (!isActive)
{
// Progress ending - clean up any remaining deferred outputs
lock (_deferredLock)
{
if (_deferredOutputs.Count > 0)
{
_logger.LogWarning(
"Progress ended with {Count} unflushed deferred outputs. " +
"This indicates FlushDeferredAsync was not called. Clearing to prevent memory leak.",
_deferredOutputs.Count);
_deferredOutputs.Clear();
}
}
}
else
{
// ... existing stale output check ...
}
_isProgressActive = isActive;
}SuggestionsTest Coverage for Error ScenariosThe new test file
Previous Review StatusFirst Review Issues:
Second Review Issues:
Verdict
|
- Remove IAsyncDisposable implementation (dead code - DI container doesn't automatically dispose async disposables for singletons) - Clear deferred outputs when SetProgressActive(false) is called to handle cancellation scenarios where FlushDeferredAsync wasn't called - This ensures memory cleanup happens whenever progress ends, regardless of whether it was cancelled or completed normally
SummaryDefers module output until pipeline completion when live progress is enabled to prevent console flickering from repeated progress pause/resume cycles. Critical IssuesNone found ✅ Suggestions1. Consider logging level adjustment for cancellation scenarios (OutputCoordinator.cs:71-77) The warning at line 72-76 treats cancellation-related unflushed outputs the same as abnormal termination. Since cancellation is a normal operational scenario, consider using 2. Test coverage could be expanded (OutputCoordinatorDeferredFlushTests.cs) The new tests only verify basic pipeline completion with progress disabled. Consider adding tests for:
These are optional improvements - the current implementation appears solid. Verdict✅ APPROVE - No critical issues The race condition fix (moving |
Summary
Changes
SetProgressActive,OnModuleCompletedAsync, andFlushDeferredAsyncmethods toIOutputCoordinatorOutputCoordinatorstores deferred buffers with timestamps and flushes them in order at pipeline endModuleLoggernow callsOnModuleCompletedAsyncwhich decides whether to defer or flush immediatelyPipelineOutputCoordinatorcallsFlushDeferredAsyncafter progress ends, before results are printedIAsyncDisposablesafety net to ensure deferred output is never lostTest Plan