Private/Invoke-RateLimitedUserRetry.ps1

function Invoke-RateLimitedUserRetry {
    <#
    .SYNOPSIS
        Processes rate-limited users with exponential backoff retry logic.

    .DESCRIPTION
        Retries sign-in log retrieval for users who encountered rate limiting,
        using exponential backoff and progressively smaller batch sizes.

    .PARAMETER RateLimitedUsers
        Array of user objects that encountered rate limiting.

    .PARAMETER Date
        Start date for filtering sign-in logs.

    .PARAMETER RetryAttempt
        Current retry attempt number (0-based).

    .PARAMETER InitialDelay
        Initial delay in milliseconds before first retry.

    .PARAMETER CurrentBatchSize
        Original batch size to calculate reduced retry batch size.

    .EXAMPLE
        $result = Invoke-RateLimitedUserRetry -RateLimitedUsers $users -Date $date -RetryAttempt 0 -InitialDelay 100 -CurrentBatchSize 20

    .NOTES
        This is a private helper function for use within the PS.OC.M365 module.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [array]$RateLimitedUsers,

        [Parameter(Mandatory)]
        [DateTime]$Date,

        [Parameter(Mandatory)]
        [int]$RetryAttempt,

        [Parameter(Mandatory)]
        [int]$InitialDelay,

        [Parameter(Mandatory)]
        [int]$CurrentBatchSize
    )

    if ($RateLimitedUsers.Count -eq 0) {
        return @{
            Results = @()
            NewRateLimitedUsers = @()
        }
    }

    # Calculate exponential backoff delay
    $delayMs = $InitialDelay * [Math]::Pow(2, $RetryAttempt)
    Write-Verbose "Retrying $($RateLimitedUsers.Count) rate-limited users (Attempt $($RetryAttempt + 1)) with delay of $($delayMs)ms"

    Start-Sleep -Milliseconds $delayMs

    $retryResults = @()
    $newRateLimitedUsers = @()
    $retryRef = [ref]$newRateLimitedUsers

    # Process in smaller batches to reduce rate limiting
    $retryBatchSize = [Math]::Max(1, [Math]::Floor($CurrentBatchSize / [Math]::Pow(2, $RetryAttempt)))

    for ($i = 0; $i -lt $RateLimitedUsers.Count; $i += $retryBatchSize) {
        $batchEnd = [Math]::Min($i + $retryBatchSize, $RateLimitedUsers.Count)
        $retryBatch = $RateLimitedUsers[$i..($batchEnd - 1)]

        Write-Verbose "Processing retry batch $([Math]::Floor($i / $retryBatchSize) + 1) of $([Math]::Ceiling($RateLimitedUsers.Count / $retryBatchSize))"

        $batchResults = Invoke-SignInBatch -Users $retryBatch -Date $Date -RateLimitedUsers $retryRef

        if ($batchResults) {
            $retryResults += $batchResults
        }

        # Add delay between retry batches
        if ($i + $retryBatchSize -lt $RateLimitedUsers.Count) {
            Start-Sleep -Milliseconds ($delayMs / 2)
        }
    }

    return @{
        Results = $retryResults
        NewRateLimitedUsers = $newRateLimitedUsers
    }
}