Modules/businessdev.ALbuild.Core/Private/Invoke-ALbuildLicenseRequest.ps1
|
function Invoke-ALbuildLicenseRequest { <# .SYNOPSIS Performs one licensing-service request, retrying the failures that retrying can fix. .DESCRIPTION Verifying a license is not a read: the service answers by moving the license's validity forward, so two builds verifying the same tenant in the same instant are two writes to the same record and the service rejects one of them with HTTP 400. Measured against the live service: two simultaneous verifications both succeed, four produce two rejections. With four parallel build jobs on one organization that is an ordinary Tuesday, and without a retry it fails a licensed feature outright - which is how the runtime factory lost a whole slice. Retried: no response at all (timeout, dropped connection, DNS/TLS) and the status codes that describe a momentary condition - 400 (the collision above), 408, 425, 429 and every 5xx. Not retried: an answer about the license itself (401, 403, 404 and the rest), where every further attempt gets the same answer and only delays the message. Delays grow exponentially and carry jitter of up to one full base delay. The jitter is the point, not decoration: colliding builds that back off by the same amount collide again on the retry, which is the failure this function exists to prevent. .PARAMETER Uri Absolute request URI. .PARAMETER Method HTTP method (Get or Post). .PARAMETER Body JSON body for Post. .PARAMETER RetryCount Additional attempts after the first. 0 disables retrying. .PARAMETER RetryDelayMilliseconds Base delay; attempt n waits RetryDelayMilliseconds * 2^n plus jitter. .PARAMETER TimeoutSec Per-attempt timeout. .OUTPUTS The deserialized response. Throws the last error when every attempt failed. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Uri, [ValidateSet('Get', 'Post')] [string] $Method = 'Get', [string] $Body, [ValidateRange(0, 10)] [int] $RetryCount = 4, [ValidateRange(0, 60000)] [int] $RetryDelayMilliseconds = 750, [ValidateRange(1, 600)] [int] $TimeoutSec = 30 ) $attempt = 0 while ($true) { try { $arguments = @{ Uri = $Uri Method = $Method TimeoutSec = $TimeoutSec ErrorAction = 'Stop' } if ($Method -eq 'Post') { $arguments['ContentType'] = 'application/json' $arguments['Body'] = $Body } return Invoke-RestMethod @arguments } catch { # Hold the record: $_ is the enclosing block's, and it is not worth betting a # diagnostic message on what a nested call leaves in it. $err = $_ $status = Get-ALbuildHttpStatusCode -ErrorRecord $err $isTransient = Test-ALbuildTransientHttpFailure -StatusCode $status if (-not $isTransient -or $attempt -ge $RetryCount) { throw } $attempt++ $delay = $RetryDelayMilliseconds * [Math]::Pow(2, $attempt - 1) if ($RetryDelayMilliseconds -gt 0) { $delay += Get-Random -Minimum 0 -Maximum $RetryDelayMilliseconds } $delay = [int] $delay $what = if ($status -gt 0) { "HTTP $status" } else { $err.Exception.Message } Write-ALbuildLog -Level Warning ("Licensing request to '$Uri' failed ($what); retrying in $delay ms " + "(attempt $($attempt + 1) of $($RetryCount + 1)).") if ($delay -gt 0) { Start-Sleep -Milliseconds $delay } } } } |