Modules/businessdev.ALbuild.Containers/Public/Optimize-BcImageCache.ps1
|
function Optimize-BcImageCache { <# .SYNOPSIS Trims the cached Business Central images to a size budget, least-recently-used first. .DESCRIPTION The version-specific image cache is the only thing that removes the per-container install cost, and it is also the largest thing on the agent's disk. It therefore needs a retention policy of its own rather than being swept up by the generic cache prune - Clear-ALbuildCache deliberately excludes 'albuild.image' images, because it runs under disk pressure and would delete the cache at exactly the moment it is most valuable. Eviction is by least-recent USE, not by age. An image is built once and reused for months, so evicting the oldest would discard the platform versions being asked for most. Two things are never evicted: * an image a container still references - removing it would break a running build; * an image in -Pin, which the caller is about to use. A run that pins the versions in its plan cannot have the cache pulled out from under it by its own trimming step. Note the budget is measured against each image's total reported size, which includes the layers shared with the generic base. That over-counts: the base is stored once for all of them. Being conservative here is intentional - overshooting the disk is far more expensive than keeping one image fewer. .PARAMETER BudgetGb Total size to keep. 0 disables the size check (then only -MinFreeDiskGb applies). .PARAMETER MinFreeDiskGb Keep evicting while free disk is below this. 0 disables the free-space check. .PARAMETER Pin Image names that must never be evicted. .PARAMETER DockerExecutable Docker executable. .OUTPUTS PSCustomObject: Removed[], KeptCount, FreedBytes. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param( [ValidateRange(0, [int]::MaxValue)] [int] $BudgetGb = 400, [ValidateRange(0, [int]::MaxValue)] [int] $MinFreeDiskGb = 80, [string[]] $Pin = @(), [string] $DockerExecutable = 'docker' ) $images = @(Get-BcImage -DockerExecutable $DockerExecutable) if ($images.Count -eq 0) { Write-ALbuildLog 'No cached Business Central images; nothing to trim.' return [PSCustomObject]@{ Removed = @(); KeptCount = 0; FreedBytes = [long]0 } } $pinned = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($p in $Pin) { if (-not [string]::IsNullOrWhiteSpace($p)) { [void]$pinned.Add($p.Trim()) } } # Least recently used first - that is the eviction order. $candidates = @($images | Where-Object { -not $_.InUse -and -not $pinned.Contains($_.ImageName) } | Sort-Object LastUsedUtc) $totalBytes = [long](@($images | Measure-Object -Property SizeBytes -Sum).Sum) $budgetBytes = [long]$BudgetGb * 1GB $removed = [System.Collections.Generic.List[object]]::new() $freed = [long]0 $needsTrim = { if ($BudgetGb -gt 0 -and ($totalBytes - $freed) -gt $budgetBytes) { return $true } if ($MinFreeDiskGb -gt 0) { $disk = Get-BcHostFreeDiskGb -DockerExecutable $DockerExecutable if ($disk -and $disk.FreeGb -lt $MinFreeDiskGb) { return $true } } return $false } foreach ($image in $candidates) { if (-not (& $needsTrim)) { break } if ($PSCmdlet.ShouldProcess($image.ImageName, "Remove cached BC image ($([Math]::Round($image.SizeBytes / 1GB, 2)) GB, last used $($image.LastUsedUtc))")) { try { Remove-BcImage -ImageName $image.ImageName -DockerExecutable $DockerExecutable -Confirm:$false | Out-Null $freed += $image.SizeBytes $removed.Add($image.ImageName) Write-ALbuildLog " Evicted '$($image.ImageName)' (last used $($image.LastUsedUtc), $([Math]::Round($image.SizeBytes / 1GB, 2)) GB)." } catch { Write-ALbuildLog -Level Warning " Could not remove '$($image.ImageName)': $($_.Exception.Message)" } } else { $freed += $image.SizeBytes } # -WhatIf: report what would be freed } if ($removed.Count -eq 0) { Write-ALbuildLog "Image cache within budget ($([Math]::Round($totalBytes / 1GB, 2)) GB across $($images.Count) image(s)); nothing evicted." } else { Write-ALbuildLog -Level Success "Evicted $($removed.Count) image(s), reclaiming $([Math]::Round($freed / 1GB, 2)) GB." } return [PSCustomObject]@{ Removed = $removed.ToArray() KeptCount = $images.Count - $removed.Count FreedBytes = $freed } } |