Skip to content
ConsoleMonster

Parallel patch wave

Run forty hosts eight at a time, bound each task and retain restart failures and timeouts.

Demos Dashboards Showcase

Use it when a batch operation needs bounded concurrency and failures retained for retry.

  • Live
  • 120×48
  • PowerShell 7.4+
  • .NET 8.0+
Parallel patch wave 120×4824-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; both languages draw the same cells.

PowerShellparallel-patch.ps1View on GitHub
# Patch a forty-host wave with bounded concurrency, timeouts and retained failures.
[CmdletBinding()]
param(
    [switch] $StaticSnapshot,
    [ValidateRange(0, 3)] [int] $Step = 0
)
Import-Module ConsoleMonster

# Data: all hosts, outcomes and delays are synthetic. No remote command runs.
$servers = @(1..40 | ForEach-Object { 'web{0:00}' -f $_ })
$operations = @('Stage update', 'Apply update', 'Restart', 'Verify health')
$restartFailures = @(13, 26, 39)
$timeoutHost = 22
$throttle = 8
$timeoutSeconds = 4

# Layout: a wave summary, every host, eight lane details and retained failures.
function New-PatchFrame([int] $Phase, $Actual) {
    $done = @(0, 8, 24, 40)[$Phase]
    $active = if ($Phase -in 0, 3) { 0 } else { 8 }
    $states = @(foreach ($n in 1..40) {
        if ($Actual) { $Actual[$n - 1].Status }
        elseif ($n -le $done) { if ($n -in $restartFailures -or $n -eq $timeoutHost) { 'Fail' } else { 'Pass' } }
        elseif ($n -le ($done + $active)) { 'Running' }
        else { 'Pending' }
    })
    $failed = @($states | Where-Object { $_ -eq 'Fail' }).Count
    New-ConsoleRows -Gap 1 {
        New-ConsoleRule '[bold]Patching the web farm[/]'
        '40 hosts · throttle 8 · per-host timeout 4s'
        New-ConsoleSummaryBar -Done $done -Total 40 -Counter ([ordered] @{
            passed = $done - $failed; failed = $failed; running = $active; pending = 40 - $done - $active
        })
        New-ConsolePanel -Title 'Host outcomes' {
            New-ConsoleCellMap -Status $states -Columns 10 -LegendLines 2 -RowLabels 'web01–10', 'web11–20', 'web21–30', 'web31–40'
        }
        New-ConsolePanel -Title $(if ($Phase -eq 3) { 'Last eight hosts' } else { 'Worker lanes' }) {
            New-ConsoleLanes {
                foreach ($lane in 0..7) {
                    $n = if ($Phase -eq 3) { 33 + $lane } else { $done + $lane + 1 }
                    $status = $states[$n - 1]
                    $finished = $Phase -eq 3
                    $progress = if ($finished) { 4 } elseif ($Phase -eq 0) { 0 } else { 1 + $lane % 3 }
                    $detail = if ($finished) {
                        if ($status -eq 'Fail') { 'Restart verification failed' } else { 'Health verified' }
                    } elseif ($Phase -eq 0) { 'Waiting for a worker' } else { $operations[$progress - 1] }
                    $passed = if ($finished) { if ($status -eq 'Pass') { 4 } else { 3 } } else { $progress }
                    $laneFailed = if ($finished -and $status -eq 'Fail') { 1 } else { 0 }
                    New-ConsoleLane $servers[$n - 1] -Total 4 -Done $progress -Status $status -Passed $passed -Failed $laneFailed -Detail $detail
                }
            }
        }
        New-ConsolePanel -Title 'Retained failures' {
            if (-not $failed) { '[muted]No failures recorded[/]' }
            else {
                New-ConsoleStatusList {
                    foreach ($n in 1..40) {
                        if ($states[$n - 1] -eq 'Fail') {
                            $detail = if ($n -eq $timeoutHost) { '4s timeout · retry separately' } else { 'Restart verification failed' }
                            New-ConsoleStatusItem $servers[$n - 1] -Status Fail -Detail $detail
                        }
                    }
                }
            }
        }
        if ($Phase -eq 3) { '[bold]Wave finished[/] · failed hosts remain available for a separate retry' }
        else { '[muted]Ctrl+C cancels active work and skips queued hosts[/]' }
    }
}

# Behaviour: the native run monitor owns scheduling, cancellation and the live UI.
if ($StaticSnapshot -or [Console]::IsInputRedirected) {
    New-PatchFrame $Step | Write-ConsoleWidget
    return
}
$successes = @($servers | ForEach-ConsoleItem -Parallel -ThrottleLimit $throttle -TimeoutSeconds $timeoutSeconds -Title 'Patching the web farm' -ErrorAction SilentlyContinue -ErrorVariable retainedErrors {
    $n = [int] $_.Substring(3)
    Start-Sleep -Milliseconds (500 + $n * 137 % 900)
    if ($n -in $using:restartFailures) { throw "$_ restart verification failed" }
    if ($n -eq $using:timeoutHost) { Start-Sleep -Seconds 10 }
    $_
})
# Keep both success objects and ErrorRecords; never retry automatically.
$outcomes = foreach ($server in $servers) {
    [pscustomobject] @{ Host = $server; Status = if ($server -in $successes) { 'Pass' } else { 'Fail' } }
}
New-PatchFrame 3 $outcomes | Write-ConsoleWidget
C#Program.csView on GitHub
// Patch a forty-host wave with bounded concurrency, timeouts and retained failures.
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Live;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: all hosts, outcomes and delays are synthetic. No remote command runs.
string[] servers = Enumerable.Range(1, 40).Select(n => $"web{n:00}").ToArray();
string[] operations = { "Stage update", "Apply update", "Restart", "Verify health" };
int[] restartFailures = { 13, 26, 39 };
int timeoutHost = 22;
const int throttle = 8;
const int timeoutSeconds = 4;
bool snapshot = args.Contains("--snapshot") || (!args.Contains("--interactive") && Console.IsInputRedirected);
int stepAt = Array.IndexOf(args, "--step");
int step = stepAt < 0 ? 0 : int.Parse(args[stepAt + 1], System.Globalization.CultureInfo.InvariantCulture);
if (step < 0 || step > 3) throw new ArgumentOutOfRangeException(nameof(step));

// Layout: a wave summary, every host, eight lane details and retained failures.
IWidget Frame(int phase, IReadOnlyList<RunResult<string, string>>? actual = null) {
    int done = new[] { 0, 8, 24, 40 }[phase];
    int active = phase is 0 or 3 ? 0 : 8;
    StatusKind State(int n) => n <= done
        ? restartFailures.Contains(n) || n == timeoutHost ? StatusKind.Fail : StatusKind.Pass
        : n <= done + active ? StatusKind.Running : StatusKind.Pending;
    if (actual != null) done = actual.Count;
    CellMap map = new CellMap { Columns = 10, LegendLines = 2 };
    for (int i = 0; i < servers.Length; i++) {
        map.Add(actual == null ? State(i + 1) : actual[i].Succeeded ? StatusKind.Pass : StatusKind.Fail);
    }
    foreach (string label in new[] { "web01–10", "web11–20", "web21–30", "web31–40" }) map.RowLabels.Add(label);
    int failed = actual?.Count(r => !r.Succeeded) ?? Enumerable.Range(1, done).Count(n => State(n) == StatusKind.Fail);
    SummaryBar summary = new SummaryBar();
    summary.SetProgress(done, 40);
    summary.SetCounter("passed", done - failed, StatusKind.Pass);
    summary.SetCounter("failed", failed, StatusKind.Fail);
    summary.SetCounter("running", active, StatusKind.Running);
    summary.SetCounter("pending", 40 - done - active, StatusKind.Pending);
    Lanes lanes = new Lanes();
    for (int lane = 0; lane < throttle; lane++) {
        int n = phase == 3 ? 33 + lane : done + lane + 1;
        StatusKind status = actual == null ? State(n) : actual[n - 1].Succeeded ? StatusKind.Pass : StatusKind.Fail;
        bool finished = phase == 3;
        int progress = finished ? 4 : phase == 0 ? 0 : 1 + lane % 3;
        string detail = finished ? (status == StatusKind.Fail ? "Restart verification failed" : "Health verified")
            : phase == 0 ? "Waiting for a worker" : operations[progress - 1];
        Lane item = lanes.Add(servers[n - 1], servers[n - 1], 4);
        item.Done = progress;
        item.Status = status;
        item.Passed = finished ? status == StatusKind.Pass ? 4 : 3 : progress;
        item.Failed = finished && status == StatusKind.Fail ? 1 : 0;
        item.Detail = detail;
    }
    StatusList failures = new StatusList();
    if (actual != null) {
        foreach (var result in actual.Where(r => !r.Succeeded)) {
            failures.Add(result.Item, StatusKind.Fail, result.TimedOut ? "4s timeout · retry separately" : "Restart verification failed");
        }
    } else {
        foreach (int n in Enumerable.Range(1, done).Where(n => State(n) == StatusKind.Fail)) {
            failures.Add(servers[n - 1], StatusKind.Fail, n == timeoutHost ? "4s timeout · retry separately" : "Restart verification failed");
        }
    }
    IWidget failureContent = failures.Items.Count == 0 ? new TextBlock("[muted]No failures recorded[/]") : failures;
    return new Rows(new Rule("[bold]Patching the web farm[/]"),
        new TextBlock("40 hosts · throttle 8 · per-host timeout 4s"), summary,
        new Panel(map, "Host outcomes"),
        new Panel(lanes, phase == 3 ? "Last eight hosts" : "Worker lanes"),
        new Panel(failureContent, "Retained failures"),
        new TextBlock(phase == 3 ? "[bold]Wave finished[/] · failed hosts remain available for a separate retry"
            : "[muted]Ctrl+C cancels active work and skips queued hosts[/]")) { Gap = 1 };
}

// Behaviour: the native run monitor owns scheduling, cancellation and the live UI.
if (snapshot) {
    Terminal.Write(Frame(step));
    return;
}
IReadOnlyList<RunResult<string, string>> results = await ConsoleRun.ForEachAsync(servers, async (server, cancel) => {
    int n = int.Parse(server.AsSpan(3), System.Globalization.CultureInfo.InvariantCulture);
    await Task.Delay(500 + n * 137 % 900, cancel);
    if (restartFailures.Contains(n)) throw new InvalidOperationException(server + " restart verification failed");
    if (n == timeoutHost) await Task.Delay(TimeSpan.FromSeconds(10), cancel);
    return server + " patched";
}, new RunOptions {
    ThrottleLimit = throttle, Timeout = TimeSpan.FromSeconds(timeoutSeconds),
    Title = "Patching the web farm", Interactive = true
});
// Keep the structured outcomes; never retry automatically after a failure.
Terminal.Write(Frame(3, results));

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into parallel-patch.ps1.

  3. Run it

    From the folder you saved it in.

    ./parallel-patch.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n Parallelpatch; cd Parallelpatch
  2. Add ConsoleMonster

    The only package the program needs.

    dotnet add package ConsoleMonster
  3. Replace Program.cs

    Download it, or copy the program from above.

  4. Run it

    dotnet run

Or skip the copying. Every demo ships with the module and the project template.

Show-ConsoleDemo parallel-patch
Get-ConsoleDemo parallel-patch -Path ./parallel-patch.ps1
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo parallel-patch -n Parallelpatch

Make it yours

Start with these, then read the rest of the file.

  1. Replace the host list with your approved targets.

  2. Set a concurrency limit and timeout for your workload.

  3. Inspect retained outcomes before choosing a separate retry.

More dashboards

All demos