Complete scratch iteration 004 setup and grouped route-action fixes

This commit is contained in:
master
2026-03-12 19:28:42 +02:00
parent d8d3133060
commit 317e55e623
26 changed files with 1124 additions and 304 deletions

View File

@@ -142,6 +142,166 @@ function Get-ServiceHttpProbeUrl([string]$serviceName, [int]$containerPort, [str
return "http://${probeHost}:$($Matches.port)$path"
}
function Get-ComposeServiceRecords([string[]]$composeFiles) {
$records = @()
$seenContainers = @{}
foreach ($composeFile in $composeFiles) {
$composePath = if ([System.IO.Path]::IsPathRooted($composeFile)) {
$composeFile
} else {
Join-Path $ComposeDir $composeFile
}
$expectedServices = Get-ComposeExpectedServices $composePath
$services = Get-ComposeServices $composePath
if ($expectedServices.Count -gt 0) {
$allowed = @{}
foreach ($name in $expectedServices) {
$allowed[$name.ToLowerInvariant()] = $true
}
$services = $services | Where-Object {
$service = "$($_.Service)".ToLowerInvariant()
$service -and $allowed.ContainsKey($service)
}
}
foreach ($svc in $services) {
$name = "$($svc.Name)"
if (-not $name -or $seenContainers.ContainsKey($name)) {
continue
}
$seenContainers[$name] = $true
$records += [pscustomobject]@{
ComposeFile = $composePath
Service = "$($svc.Service)"
Name = $name
State = "$($svc.State)".ToLowerInvariant()
Health = "$($svc.Health)".ToLowerInvariant()
}
}
}
return $records
}
function Wait-ForComposeConvergence(
[string[]]$composeFiles,
[string]$successMessage,
[int]$maxWaitSeconds = 180,
[int]$restartAfterSeconds = 45,
[int]$pollSeconds = 5,
[switch]$RestartStalledServices
) {
$restartedServices = @{}
$elapsed = 0
while ($elapsed -lt $maxWaitSeconds) {
$records = Get-ComposeServiceRecords $composeFiles
if ($records.Count -gt 0) {
$pending = @()
$blocking = @()
foreach ($record in $records) {
if ($record.State -ne 'running') {
$blocking += $record
continue
}
if (-not $record.Health -or $record.Health -eq 'healthy') {
continue
}
if ($record.Health -eq 'starting') {
$pending += $record
continue
}
$blocking += $record
}
if ($blocking.Count -eq 0 -and $pending.Count -eq 0 -and $elapsed -gt $pollSeconds) {
Write-Ok $successMessage
return $true
}
if ($RestartStalledServices -and $elapsed -ge $restartAfterSeconds -and $blocking.Count -gt 0) {
$restartGroups = @{}
foreach ($record in $blocking) {
$restartKey = "$($record.ComposeFile)|$($record.Service)"
if ($restartedServices.ContainsKey($restartKey)) {
continue
}
if (-not $restartGroups.ContainsKey($record.ComposeFile)) {
$restartGroups[$record.ComposeFile] = New-Object System.Collections.Generic.List[string]
}
$restartGroups[$record.ComposeFile].Add($record.Service)
$restartedServices[$restartKey] = $true
}
foreach ($group in $restartGroups.GetEnumerator()) {
$servicesToRestart = @($group.Value | Sort-Object -Unique)
if ($servicesToRestart.Count -eq 0) {
continue
}
Write-Warn "Restarting stalled services from $($group.Key): $($servicesToRestart -join ', ')"
Push-Location $ComposeDir
try {
docker compose -f $group.Key restart @servicesToRestart | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Ok "Restarted stalled services: $($servicesToRestart -join ', ')"
} else {
Write-Warn "Failed to restart stalled services: $($servicesToRestart -join ', ')"
}
}
finally {
Pop-Location
}
}
}
}
Start-Sleep -Seconds $pollSeconds
$elapsed += $pollSeconds
}
$finalRecords = Get-ComposeServiceRecords $composeFiles
$blockingSummary = @(
$finalRecords | ForEach-Object {
if ($_.State -ne 'running') {
"$($_.Name) (state=$($_.State))"
}
elseif ($_.Health -and $_.Health -ne 'healthy' -and $_.Health -ne 'starting') {
"$($_.Name) (health=$($_.Health))"
}
}
) | Where-Object { $_ }
$pendingSummary = @(
$finalRecords | Where-Object {
$_.State -eq 'running' -and $_.Health -eq 'starting'
} | ForEach-Object {
"$($_.Name) (health=starting)"
}
)
if ($blockingSummary.Count -gt 0) {
Write-Warn "Timed out waiting for compose convergence after ${maxWaitSeconds}s. Blocking services: $($blockingSummary -join ', ')"
} elseif ($pendingSummary.Count -gt 0) {
Write-Warn "Timed out waiting for compose convergence after ${maxWaitSeconds}s. Still starting: $($pendingSummary -join ', ')"
} else {
Write-Warn "Timed out waiting for compose convergence after ${maxWaitSeconds}s."
}
return $false
}
# ─── 1. Check prerequisites ────────────────────────────────────────────────
function Test-Prerequisites {
@@ -365,44 +525,7 @@ function Start-Infrastructure {
}
Write-Host ' Waiting for containers to become healthy...' -ForegroundColor Gray
$maxWait = 120
$elapsed = 0
while ($elapsed -lt $maxWait) {
$expectedServices = Get-ComposeExpectedServices 'docker-compose.dev.yml'
$services = Get-ComposeServices 'docker-compose.dev.yml'
if ($expectedServices.Count -gt 0) {
$allowed = @{}
foreach ($name in $expectedServices) {
$allowed[$name.ToLowerInvariant()] = $true
}
$services = $services | Where-Object {
$service = "$($_.Service)".ToLowerInvariant()
$service -and $allowed.ContainsKey($service)
}
}
if ($services.Count -gt 0) {
$allHealthy = $true
foreach ($svc in $services) {
$state = "$($svc.State)".ToLowerInvariant()
$health = "$($svc.Health)".ToLowerInvariant()
if ($state -ne 'running') {
$allHealthy = $false
continue
}
if ($health -and $health -ne 'healthy') {
$allHealthy = $false
}
}
if ($allHealthy -and $elapsed -gt 5) {
Write-Ok 'All infrastructure containers healthy'
return
}
}
Start-Sleep -Seconds 5
$elapsed += 5
}
Write-Warn "Timed out waiting for healthy status after ${maxWait}s. Check with: docker compose -f docker-compose.dev.yml ps"
[void](Wait-ForComposeConvergence -composeFiles @('docker-compose.dev.yml') -successMessage 'All infrastructure containers healthy' -maxWaitSeconds 120)
}
finally {
Pop-Location
@@ -465,6 +588,13 @@ function Start-Platform {
finally {
Pop-Location
}
[void](Wait-ForComposeConvergence `
-composeFiles @('docker-compose.stella-ops.yml') `
-successMessage 'Platform services converged from zero-state startup' `
-RestartStalledServices `
-maxWaitSeconds 180 `
-restartAfterSeconds 45)
}
function Test-ExpectedHttpStatus([string]$url, [int[]]$allowedStatusCodes, [int]$timeoutSeconds = 5, [int]$attempts = 6, [int]$retryDelaySeconds = 2) {