Public/Account/Get-RMMThrottleStatus.ps1

<#
    Copyright (c) 2025-2026 Robert Faddes
    SPDX-License-Identifier: MPL-2.0
#>

function Get-RMMThrottleStatus {
    <#
    .SYNOPSIS
        Retrieves a detailed snapshot of the current API rate limits, counts, and local throttle state.
 
    .DESCRIPTION
        The Get-RMMThrottleStatus function provides a combined view of the Datto RMM API rate-status
        endpoint data and the local sliding-window throttle model. It calls the rate-status API to
        fetch fresh read and write counts, limits, and per-operation bucket status, then merges
        this with the local throttle tracking state (timestamps, utilisation, flags, thresholds).
 
        The Datto RMM API tracks reads and writes as independent quotas:
        - accountCount / accountRateLimit → read (GET) operations only
        - accountWriteCount / accountWriteRateLimit → write (PUT/POST/DELETE) operations only
 
        The returned DRMMThrottleStatus object includes:
        - Active throttle profile and configured thresholds
        - Independent read and write utilisation (both API-reported and locally tracked)
        - Throttle and pause flags reflecting the current state before drift adjustment
        - Independent read and write delay values in milliseconds
        - Calibration metadata for both read and write tracks
        - A Buckets collection of DRMMThrottleBucket objects covering the read bucket,
          write bucket, and all per-operation write buckets (both mapped and unidentified)
 
        Each bucket reports its Type (Read, Write, or Operation), Name, Limit, ApiCount,
        LocalCount, and computed Utilisation ratio.
 
        This function is designed for monitoring, diagnostics, and sample capture during long-running
        load tests. It does not modify any throttle state. For the raw API response without local
        enrichment, use Get-RMMRequestRate instead.
 
    .EXAMPLE
        Get-RMMThrottleStatus
 
        Retrieves the current combined throttle status snapshot.
 
    .EXAMPLE
        $Status = Get-RMMThrottleStatus
        PS > $Status.GetSummary()
 
        Retrieves throttle status and displays a summary of utilisation, flags, and delay.
 
    .EXAMPLE
        Get-RMMThrottleStatus | Select-Object -ExpandProperty Buckets | Format-Table
 
        Retrieves throttle status and displays all rate-limit buckets in a table.
 
    .EXAMPLE
        (Get-RMMThrottleStatus).Buckets | Where-Object Type -eq 'Operation' | Sort-Object Utilisation -Descending
 
        Retrieves and filters only per-operation buckets, sorted by utilisation.
 
    .EXAMPLE
        $Status = Get-RMMThrottleStatus
        PS > $Status.Buckets | Where-Object {$_.Utilisation -gt 0}
 
        Shows only buckets with active utilisation for quick load test monitoring.
 
    .INPUTS
        None. You cannot pipe objects to Get-RMMThrottleStatus.
 
    .OUTPUTS
        DRMMThrottleStatus. Returns a throttle status object with the following properties:
        - Profile (string): Active throttle profile name
        - AccountUid (string): Account unique identifier from the API
        - WindowSizeSeconds (int): Rolling window size in seconds
        - ReadUtilisation (double): Read utilisation ratio (higher of API or local)
        - WriteUtilisation (double): Write utilisation ratio (higher of API or local)
        - AccountCutOffRatio (double): API-reported account cut-off ratio
        - ThrottleUtilisationThreshold (double): Configured threshold at which throttling activates
        - PauseThreshold (double): Computed threshold at which hard pause activates
        - Throttle (bool): Whether soft throttling is currently active
        - Pause (bool): Whether hard pause is currently active
        - ReadDelayMs (double): Current computed read delay in milliseconds
        - WriteDelayMs (double): Current computed write delay in milliseconds
        - DelayMultiplier (double): Configured delay multiplier (used for both read and write)
        - ReadLastCalibrationUtc (datetime): UTC time of the last read calibration
        - WriteLastCalibrationUtc (datetime): UTC time of the last write calibration
        - ReadSamplesAtLastCalibration (int): Local read samples at last calibration
        - WriteSamplesAtLastCalibration (int): Local write samples at last calibration
        - Buckets (DRMMThrottleBucket[]): Collection of all tracked rate-limit buckets
 
    .NOTES
        This function requires an active connection to the Datto RMM API.
        Use Connect-DattoRMM to authenticate before calling this function.
 
        This function calls the rate-status API endpoint to fetch fresh data, which itself
        counts as a read request against the account rate limit.
 
        The throttle state reported is a pre-drift-adjustment snapshot. The actual throttle
        engine may adjust utilisation values during calibration cycles.
 
    .LINK
        https://github.com/TheShadowTek/DattoRMM.Core/blob/main/docs/commands/Account/Get-RMMThrottleStatus.md
 
    .LINK
        Get-RMMRequestRate
 
    .LINK
        Connect-DattoRMM
 
    .LINK
        about_DattoRMM.CoreThrottling
 
    .LINK
        about_DRMMThrottleStatus
    #>


    [CmdletBinding()]
    param ()

    process {

        # Fetch fresh rate-status from the API
        $RateInfo = Invoke-ApiMethod -Path 'system/request_rate' -Method GET

        Write-Debug "ThrottleStatus: API returned Read=$($RateInfo.accountCount)/$($RateInfo.accountRateLimit), Write=$($RateInfo.accountWriteCount)/$($RateInfo.accountWriteRateLimit)"

        # --- Build the result object ---
        $Result = [DRMMThrottleStatus]::new()
        $Result.Profile = $Script:RMMThrottle.Profile
        $Result.AccountUid = $RateInfo.accountUid
        $Result.WindowSizeSeconds = $RateInfo.slidingTimeWindowSizeSeconds
        $Result.AccountCutOffRatio = $RateInfo.accountCutOffRatio
        $Result.ThrottleUtilisationThreshold = $Script:RMMThrottle.ThrottleUtilisationThreshold
        $Result.PauseThreshold = $RateInfo.accountCutOffRatio - $Script:RMMThrottle.ThrottleCutOffOverhead
        $Result.DelayMultiplier = $Script:RMMThrottle.DelayMultiplier
        $Result.ReadLastCalibrationUtc = $Script:RMMThrottle.ReadLastCalibrationUtc
        $Result.WriteLastCalibrationUtc = $Script:RMMThrottle.WriteLastCalibrationUtc
        $Result.ReadSamplesAtLastCalibration = $Script:RMMThrottle.ReadSamplesAtLastCalibration
        $Result.WriteSamplesAtLastCalibration = $Script:RMMThrottle.WriteSamplesAtLastCalibration

        # --- Compute utilisation from both sources (same logic as Update-Throttle, pre-drift) ---
        $ApiReadUtil = $RateInfo.accountCount / [math]::Max($RateInfo.accountRateLimit, 1)
        $LocalReadUtil = $Script:RMMThrottle.ReadLocalTimestamps.Count / [math]::Max($Script:RMMThrottle.ReadLimit, 1)
        $Result.ReadUtilisation = [math]::Max($ApiReadUtil, $LocalReadUtil)

        $ApiWriteUtil = 0.0
        $LocalWriteUtil = 0.0

        if ($RateInfo.accountWriteRateLimit -gt 0) {

            $ApiWriteUtil = $RateInfo.accountWriteCount / [math]::Max($RateInfo.accountWriteRateLimit, 1)
            $LocalWriteUtil = $Script:RMMThrottle.WriteLocalTimestamps.Count / [math]::Max($Script:RMMThrottle.WriteLimit, 1)

        }

        $Result.WriteUtilisation = [math]::Max($ApiWriteUtil, $LocalWriteUtil)

        # --- Compute flags and delays (mirrors Update-Throttle logic, pre-adjustment) ---
        $ReadThrottle = ($Result.ReadUtilisation -ge $Script:RMMThrottle.ThrottleUtilisationThreshold)
        $WriteThrottle = ($Result.WriteUtilisation -ge $Script:RMMThrottle.ThrottleUtilisationThreshold)
        $Result.Throttle = ($ReadThrottle -or $WriteThrottle)
        $Result.Pause = ($Result.ReadUtilisation -ge $Result.PauseThreshold) -or ($Result.WriteUtilisation -ge $Result.PauseThreshold)

        if ($ReadThrottle) {

            $Result.ReadDelayMs = $Result.ReadUtilisation * $Script:RMMThrottle.DelayMultiplier

        } else {

            $Result.ReadDelayMs = 0

        }

        if ($WriteThrottle) {

            $Result.WriteDelayMs = $Result.WriteUtilisation * $Script:RMMThrottle.DelayMultiplier

        } else {

            $Result.WriteDelayMs = 0

        }

        # --- Build bucket collection ---
        $BucketList = [System.Collections.Generic.List[DRMMThrottleBucket]]::new()

        # Read bucket
        $ReadBucket = [DRMMThrottleBucket]::new()
        $ReadBucket.Type = 'Read'
        $ReadBucket.Name = 'Read'
        $ReadBucket.Limit = $RateInfo.accountRateLimit
        $ReadBucket.ApiCount = $RateInfo.accountCount
        $ReadBucket.LocalCount = $Script:RMMThrottle.ReadLocalTimestamps.Count
        $ReadBucket.Utilisation = $Result.ReadUtilisation
        $BucketList.Add($ReadBucket)

        # Write bucket
        $WriteBucket = [DRMMThrottleBucket]::new()
        $WriteBucket.Type = 'Write'
        $WriteBucket.Name = 'Write'
        $WriteBucket.Limit = $RateInfo.accountWriteRateLimit
        $WriteBucket.ApiCount = $RateInfo.accountWriteCount
        $WriteBucket.LocalCount = $Script:RMMThrottle.WriteLocalTimestamps.Count
        $WriteBucket.Utilisation = $Result.WriteUtilisation
        $BucketList.Add($WriteBucket)

        # Per-operation write buckets from API response
        if ($null -ne $RateInfo.operationWriteStatus) {

            $RateInfo.operationWriteStatus.PSObject.Properties | ForEach-Object {

                $OpName = $_.Name
                $OpBucket = [DRMMThrottleBucket]::new()
                $OpBucket.Type = 'Operation'
                $OpBucket.Name = $OpName
                $OpBucket.Limit = $_.Value.limit
                $OpBucket.ApiCount = $_.Value.count

                # Merge local tracking if bucket exists locally
                if ($Script:RMMThrottle.OperationBuckets.ContainsKey($OpName)) {

                    $OpBucket.LocalCount = $Script:RMMThrottle.OperationBuckets[$OpName].LocalTimestamps.Count

                } else {

                    $OpBucket.LocalCount = 0

                }

                # Utilisation is higher of API or local
                $ApiOpUtil = $OpBucket.ApiCount / [math]::Max($OpBucket.Limit, 1)
                $LocalOpUtil = $OpBucket.LocalCount / [math]::Max($OpBucket.Limit, 1)
                $OpBucket.Utilisation = [math]::Max($ApiOpUtil, $LocalOpUtil)

                $BucketList.Add($OpBucket)

            }
        }

        # Include any locally tracked operation buckets not present in the API response
        if ($Script:RMMThrottle.OperationBuckets -and $Script:RMMThrottle.OperationBuckets.Count -gt 0) {

            $ApiOpNames = @()

            if ($null -ne $RateInfo.operationWriteStatus) {

                $ApiOpNames = @($RateInfo.operationWriteStatus.PSObject.Properties.Name)

            }

            $Script:RMMThrottle.OperationBuckets.GetEnumerator() | ForEach-Object {

                if ($_.Key -notin $ApiOpNames) {

                    $UnmappedBucket = [DRMMThrottleBucket]::new()
                    $UnmappedBucket.Type = 'Operation'
                    $UnmappedBucket.Name = $_.Key
                    $UnmappedBucket.Limit = $_.Value.Limit
                    $UnmappedBucket.ApiCount = 0
                    $UnmappedBucket.LocalCount = $_.Value.LocalTimestamps.Count
                    $UnmappedBucket.Utilisation = $UnmappedBucket.LocalCount / [math]::Max($UnmappedBucket.Limit, 1)

                    $BucketList.Add($UnmappedBucket)

                }
            }
        }

        $Result.Buckets = $BucketList.ToArray()

        Write-Debug "ThrottleStatus: Built $($Result.Buckets.Count) buckets — Read=$([math]::Round($Result.ReadUtilisation * 100, 2))%, Write=$([math]::Round($Result.WriteUtilisation * 100, 2))%, Throttle=$($Result.Throttle), Pause=$($Result.Pause)"

        $Result

    }
}

# SIG # Begin signature block
# MIIF+wYJKoZIhvcNAQcCoIIF7DCCBegCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC2n7EGOBB5KFRK
# +u3OwunpS24kWxIsiE2EYuodpw5bEqCCA04wggNKMIICMqADAgECAhB464iXHfI6
# gksEkDDTyrNsMA0GCSqGSIb3DQEBCwUAMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRk
# ZXMxIzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nMB4XDTI2MDMz
# MTAwMTMzMFoXDTI4MDMzMTAwMjMzMFowPTEWMBQGA1UECgwNUm9iZXJ0IEZhZGRl
# czEjMCEGA1UEAwwaRGF0dG9STU0uQ29yZSBDb2RlIFNpZ25pbmcwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn1EpMYQgl1RgWzQj2+wp2mvdfb3UsaBS
# nxEVGoQ0gj96tJ2MHAF7zsITdUjwaflKS1vE6wAlOg5EI1V79tJCMxzM0bFpOdR1
# L5F2HE/ovIAKNkHxFUF5qWU8vVeAsOViFQ4yhHpzLen0WLF6vhmc9eH23dLQy5fy
# tELZQEc2WbQFa4HMAitP/P9kHAu6CUx5s4woLIOyyR06jkr3l9vk0sxcbCxx7+dF
# RrsSLyPYPH+bUAB8+a0hs+6qCeteBuUfLvGzpMhpzKAsY82WZ3Rd9X38i32dYj+y
# dYx+nx+UEMDLjDJrZgnVa8as4RojqVLcEns5yb/XTjLxDc58VatdAgMBAAGjRjBE
# MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU
# H+B0vf97dYXqdUX1YMcWhFsY6fcwDQYJKoZIhvcNAQELBQADggEBAJmD4EEGNmcD
# 1JtFoRGxuLJaTHxDwBsjqcRQRE1VPZNGaiwIm8oSQdHVjQg0oIyK7SEb02cs6n6Y
# NZbwf7B7WZJ4aKYbcoLug1k1x9SoqwBmfElECeJTKXf6dkRRNmrAodpGCixR4wMH
# KXqwqP5F+5j7bdnQPiIVXuMesxc4tktz362ysph1bqKjDQSCBpwi0glEIH7bv5Ms
# Ey9Gl3fe+vYC5W06d2LYVebEfm9+7766hsOgpdDVgdtnN+e6uwIJjG/6PTG6TMDP
# y+pr5K6LyUVYJYcWWUTZRBqqwBHiLGekPbxrjEVfxUY32Pq4QfLzUH5hhUCAk4HN
# XpF9pOzFLMUxggIDMIIB/wIBATBRMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRkZXMx
# IzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nAhB464iXHfI6gksE
# kDDTyrNsMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC
# gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG
# CisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIOIHzGjppJJb5MxwOyL2ZS6CWgIV
# EpceZonXP6VyPFYhMA0GCSqGSIb3DQEBAQUABIIBACi2pRv2+fD7E+SVV8FzTixG
# itoY0353jNuyG8lb4GpEeKziUq0rP4sLhdtzrqre2EksdHQcR3HDTGLqwuceoUkF
# eEou6OxpoSuxMl8H8U335V8EPZHL/rjxK5rEDqLAGKUelC/KZ8+SARW7GHY4Iivj
# 9l4Z/X0wcIxNCw7KA0hIlCUYTdSinGtluE4WvEvikB21kENNsEYIEtXJRxp4r8VI
# P2g3lY4zfZWJTBVObvMKZWL8Y8+ciaXC9yqDa35Sqy7lnWoYe7BM79VzEGQWld8z
# RT1uUZw5Qg9vIkcpZ/Db8CCqwsm+0ISsB8z5K4Pl8ut1J8mr3ZtomUE0QRyZPgY=
# SIG # End signature block