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 }
        }
    }
}