Skip to content

Commit 0a2d8d0

Browse files
authored
+semver:major - Merge pull request #2087 from thomhurst/feature/immediate-module-output
feat: immediate module output with progress coordination
2 parents afae3ed + 530c139 commit 0a2d8d0

12 files changed

Lines changed: 402 additions & 63 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,4 +402,4 @@ requirements
402402

403403
# Git worktrees directory
404404
.worktrees/
405-
docs/plans/
405+
docs/plans/

src/ModularPipelines/Console/ConsoleCoordinator.cs

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ internal class ConsoleCoordinator : IConsoleCoordinator, IProgressDisplay
6464

6565
// Logger for output
6666
private ILogger? _outputLogger;
67+
private readonly IOutputCoordinator _outputCoordinator;
6768

6869
public ConsoleCoordinator(
6970
IBuildSystemFormatterProvider formatterProvider,
@@ -72,7 +73,8 @@ public ConsoleCoordinator(
7273
IOptions<PipelineOptions> options,
7374
ILoggerFactory loggerFactory,
7475
IBuildSystemDetector buildSystemDetector,
75-
IServiceProvider serviceProvider)
76+
IServiceProvider serviceProvider,
77+
IOutputCoordinator outputCoordinator)
7678
{
7779
_formatterProvider = formatterProvider;
7880
_resultsPrinter = resultsPrinter;
@@ -81,6 +83,7 @@ public ConsoleCoordinator(
8183
_loggerFactory = loggerFactory;
8284
_buildSystemDetector = buildSystemDetector;
8385
_serviceProvider = serviceProvider;
86+
_outputCoordinator = outputCoordinator;
8487
_unattributedBuffer = new ModuleOutputBuffer("Pipeline", typeof(void));
8588
}
8689

@@ -221,8 +224,12 @@ public async Task<IProgressSession> BeginProgressAsync(
221224
this,
222225
modules,
223226
_options,
227+
_loggerFactory,
224228
cancellationToken);
225229

230+
// Wire up the progress controller for output coordination
231+
_outputCoordinator.SetProgressController(session);
232+
226233
_activeSession = session;
227234

228235
// Start the progress display
@@ -238,6 +245,7 @@ internal void EndProgressPhase()
238245
{
239246
lock (_phaseLock)
240247
{
248+
_outputCoordinator.SetProgressController(NoOpProgressController.Instance);
241249
_isProgressActive = false;
242250
_activeSession = null;
243251
}
@@ -255,38 +263,22 @@ public IModuleOutputBuffer GetModuleBuffer(Type moduleType)
255263
/// <inheritdoc />
256264
public void FlushModuleOutput()
257265
{
266+
// Output is now flushed immediately when modules complete.
267+
// This method remains for API compatibility but only flushes
268+
// unattributed output (pipeline-level logs).
258269
if (_originalConsoleOut == null)
259270
{
260-
throw new InvalidOperationException("ConsoleCoordinator is not installed.");
271+
return; // Not installed, nothing to flush
261272
}
262273

263274
var formatter = _formatterProvider.GetFormatter();
264275

265-
// Flush unattributed output first (if any)
276+
// Flush unattributed output (if any)
266277
if (_unattributedBuffer.HasOutput)
267278
{
268279
var unattributedLogger = _outputLogger ?? _loggerFactory.CreateLogger("ModularPipelines.Output");
269280
_unattributedBuffer.FlushTo(_originalConsoleOut, formatter, unattributedLogger);
270281
}
271-
272-
// Flush module buffers in completion order
273-
var orderedBuffers = _moduleBuffers.Values
274-
.Where(b => b.HasOutput)
275-
.OrderBy(b => b.CompletedAtUtc ?? DateTime.MaxValue)
276-
.ToList();
277-
278-
foreach (var buffer in orderedBuffers)
279-
{
280-
// Resolve the registered ILogger<T> from DI to use any custom loggers injected by tests
281-
var loggerType = typeof(ILogger<>).MakeGenericType(buffer.ModuleType);
282-
var moduleLogger = (ILogger)_serviceProvider.GetService(loggerType)
283-
?? _loggerFactory.CreateLogger(buffer.ModuleType);
284-
buffer.FlushTo(_originalConsoleOut, formatter, moduleLogger);
285-
}
286-
287-
// Clear buffers after flush to release memory
288-
// This prevents accumulation in long-running pipelines
289-
_moduleBuffers.Clear();
290282
}
291283

292284
/// <inheritdoc />

src/ModularPipelines/Console/IConsoleCoordinator.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,9 @@ internal interface IConsoleCoordinator : IAsyncDisposable
7272
IModuleOutputBuffer GetUnattributedBuffer();
7373

7474
/// <summary>
75-
/// Flushes all module output in completion order.
76-
/// Should be called after progress phase ends.
75+
/// Flushes any remaining unattributed output.
76+
/// Module output is flushed immediately when modules complete.
7777
/// </summary>
78-
/// <exception cref="InvalidOperationException">Thrown if not installed.</exception>
7978
void FlushModuleOutput();
8079

8180
/// <summary>

src/ModularPipelines/Console/IModuleOutputBuffer.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ namespace ModularPipelines.Console;
1515
/// <b>Thread Safety:</b> All methods are thread-safe and can be called concurrently.
1616
/// </para>
1717
/// <para>
18-
/// <b>Flush Ordering:</b> Buffers are flushed in completion order (by CompletedAtUtc)
19-
/// to maintain logical output sequence.
18+
/// <b>Flush Behavior:</b> Buffers are flushed immediately when modules complete,
19+
/// via the OutputCoordinator which ensures ordered output.
2020
/// </para>
2121
/// </remarks>
2222
internal interface IModuleOutputBuffer
@@ -26,18 +26,6 @@ internal interface IModuleOutputBuffer
2626
/// </summary>
2727
Type ModuleType { get; }
2828

29-
/// <summary>
30-
/// Gets when the module completed (for ordering during flush).
31-
/// Null if not yet completed.
32-
/// </summary>
33-
DateTime? CompletedAtUtc { get; }
34-
35-
/// <summary>
36-
/// Records completion time for flush ordering.
37-
/// Called when the module finishes execution.
38-
/// </summary>
39-
void MarkCompleted();
40-
4129
/// <summary>
4230
/// Adds a plain string line to the buffer.
4331
/// Used for Console.WriteLine interceptions.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace ModularPipelines.Console;
2+
3+
/// <summary>
4+
/// Coordinates immediate flushing of module output with synchronization.
5+
/// Ensures modules write output in FIFO completion order without interleaving.
6+
/// </summary>
7+
internal interface IOutputCoordinator
8+
{
9+
/// <summary>
10+
/// Sets the progress controller for pause/resume coordination.
11+
/// Must be called before any modules complete.
12+
/// </summary>
13+
/// <param name="controller">The progress controller.</param>
14+
void SetProgressController(IProgressController controller);
15+
16+
/// <summary>
17+
/// Enqueues a module's buffer for immediate flushing and waits until flushed.
18+
/// Called when a module's logger is disposed.
19+
/// </summary>
20+
/// <param name="buffer">The buffer to flush.</param>
21+
/// <param name="cancellationToken">Cancellation token.</param>
22+
/// <returns>A task that completes when the buffer has been flushed.</returns>
23+
Task EnqueueAndFlushAsync(IModuleOutputBuffer buffer, CancellationToken cancellationToken = default);
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace ModularPipelines.Console;
2+
3+
/// <summary>
4+
/// Controls pausing and resuming the progress display for output flushing.
5+
/// </summary>
6+
internal interface IProgressController
7+
{
8+
/// <summary>
9+
/// Gets whether this controller supports dynamic display (interactive terminal).
10+
/// </summary>
11+
bool IsInteractive { get; }
12+
13+
/// <summary>
14+
/// Pauses the progress display to allow console output.
15+
/// On interactive terminals, this clears the progress display.
16+
/// On CI, this is a no-op.
17+
/// </summary>
18+
Task PauseAsync();
19+
20+
/// <summary>
21+
/// Resumes the progress display after output.
22+
/// On interactive terminals, this redraws the progress state.
23+
/// On CI, this is a no-op.
24+
/// </summary>
25+
Task ResumeAsync();
26+
}

src/ModularPipelines/Console/ModuleOutputBuffer.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ internal class ModuleOutputBuffer : IModuleOutputBuffer
3131
/// <inheritdoc />
3232
public Type ModuleType { get; }
3333

34-
/// <inheritdoc />
35-
public DateTime? CompletedAtUtc { get; private set; }
36-
3734
/// <summary>
3835
/// Initializes a new buffer for the specified module type.
3936
/// </summary>
@@ -80,15 +77,6 @@ public void AddLogEvent(
8077
}
8178
}
8279

83-
/// <inheritdoc />
84-
public void MarkCompleted()
85-
{
86-
lock (_lock)
87-
{
88-
CompletedAtUtc ??= DateTime.UtcNow;
89-
}
90-
}
91-
9280
/// <inheritdoc />
9381
public void SetException(Exception exception)
9482
{
@@ -165,7 +153,7 @@ public void FlushTo(TextWriter console, IBuildSystemFormatter formatter, ILogger
165153

166154
private string FormatHeader(Exception? exception)
167155
{
168-
var duration = (CompletedAtUtc ?? DateTime.UtcNow) - _startTimeUtc;
156+
var duration = DateTime.UtcNow - _startTimeUtc;
169157
var durationStr = duration.TotalSeconds >= 60
170158
? $"{duration.TotalMinutes:F1}m"
171159
: $"{duration.TotalSeconds:F1}s";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace ModularPipelines.Console;
4+
5+
/// <summary>
6+
/// Progress controller for CI environments where no dynamic display exists.
7+
/// All operations are no-ops.
8+
/// </summary>
9+
[ExcludeFromCodeCoverage]
10+
internal sealed class NoOpProgressController : IProgressController
11+
{
12+
public static NoOpProgressController Instance { get; } = new();
13+
14+
public bool IsInteractive => false;
15+
16+
public Task PauseAsync() => Task.CompletedTask;
17+
18+
public Task ResumeAsync() => Task.CompletedTask;
19+
}

0 commit comments

Comments
 (0)