Main/RetryPolicies/New-LinearRetryPolicy.ps1

<#
.SYNOPSIS
    Creates a Linear Retry Policy
 
.DESCRIPTION
    Creates a new object of type LinearRetryPolicy to be used with Invoke-ScriptBlockWithRetry. The algorithm takes a
    base delay with an optional delay increase, processing this policy will take
    baseDelay + (delayIncrease x retryCount) milliseconds to complete.
    Constructing a linear retry policy requires also a maximum number of retries mandatory. Once processing the maximum
    number of retries is reached succeeding retries will throw an error of type MaxRetryLimitReached.
    Delay Increase time is optional and if not provided the total delay time is calculated as follows:
    baseDelay + (baseDelay x retryCount)
    Error references can be passed in order to provide filtering on the types of errors to retry or not as well as a
    comparison type operator which will determine the comparison type to perform. Filtering is done through a method
    named [bool] shouldProcess() which takes a sample error and compares it against the reference. This method is used
    to determine whether to process the policy or not. shouldProcess method also considers the number of retries already
    performed and compare it agains the retry limit.
    The type of comparison determine the aspects of the sample error to compare agains the references provided. This
    comparison is performed in the base object and the possible behaviors are:
    - NoComparison: Performs no comparison and always returns true
    - TypeCompare: Compares the type of errors to ensure they're the same
    - ActivityCompare: Compares the CategoryInfo.Activity property to ensure are the same
    - CategoryCompare: Compares the CategoryInfo.Category property to ensure are the same
    - IdCompare: Compares the FullyQualifiedErrorId property to ensure sample error contains thew reference's id
    - AnyCompare: Dismiss the comparison and returns true
 
.EXAMPLE
PS> $retry = New-LinearRetryPolicy -DelayBase 1000 -NumberOfRetries 3
Creates a new retry policy object that starts with a delay of 1 second and will attempt 3 times before throwing an error
with an increase of 1 second between retries (1s, 2s, 3s)
 
.EXAMPLE
PS> $retry.getPolicyName()
Returns the name of the policy instance
 
.EXAMPLE
PS> $retry.getPolicyVersion()
Returns the version of the policy instance
 
.EXAMPLE
PS> $retry.clone()
Returns a new instance of the policy instance using the data of the current object
 
.EXAMPLE
PS> $retry.shouldProcess($null)
Determines whether the policy should be processed or not based on the sample error provided in comparison to the error
reference objects passed when the object was built
 
.EXAMPLE
PS> $ex = New-Object ArgumentNullException -ArgumentList "Invalid Argument"
PS> $er = New-Object System.Management.Automation.ErrorRecord -ArgumentList $ex, 'ErrID', 'InvalidArgument', $null
PS> $retry.shouldProcess($er)
Creates a sample Exception and a sample Error Record based of that, then the sample error record is used against the
retry policy to validate whether to process the policy or not
 
.EXAMPLE
PS> $retry = New-LinearRetryPolicy -DelayBase 1000 -NumberOfRetries 3
PS> Measure-Command { $retry.processPolicy() }
 
Days : 0
Hours : 0
Minutes : 0
Seconds : 1
Milliseconds : 15
Ticks : 10158458
TotalDays : 1.1757474537037E-05
TotalHours : 0.000282179388888889
TotalMinutes : 0.0169307633333333
TotalSeconds : 1.0158458
TotalMilliseconds : 1015.8458
 
Creates a sample linear retry policy with initial delay of a second, increments of a second under any error type, and
with 3 attempts
 
.EXAMPLE
PS> Measure-Command { $retry.processPolicy() }
 
Days : 0
Hours : 0
Minutes : 0
Seconds : 2
Milliseconds : 0
Ticks : 20007518
TotalDays : 2.3156849537037E-05
TotalHours : 0.000555764388888889
TotalMinutes : 0.0333458633333333
TotalSeconds : 2.0007518
TotalMilliseconds : 2000.7518
 
PS> Measure-Command { $retry.processPolicy() }
 
Days : 0
Hours : 0
Minutes : 0
Seconds : 3
Milliseconds : 0
Ticks : 30007282
TotalDays : 3.4730650462963E-05
TotalHours : 0.000833535611111111
TotalMinutes : 0.0500121366666667
TotalSeconds : 3.0007282
TotalMilliseconds : 3000.7282
 
Based on the previous example each processing of the policy increases the delay period of a second
 
.EXAMPLE
PS> $retry.shouldProcess($null)
False
PS> $retry.processPolicy()
[LinearRetryPolicy.invokePolicy:MaxRetryLimitReached] Max number of retries reached: 3/3
At D:\repos\GitHub\PsxUtility\Main\RetryPolicies\New-LinearRetryPolicy.ps1:192 char:13
+ throw [xUtilityException]::New(
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (:) [], xUtilityException
    + FullyQualifiedErrorId : [LinearRetryPolicy.invokePolicy:MaxRetryLimitReached] Max number of retries reached: 3/3
 
Based on previous two examples the existing policy object has already reatched the maximum of attempts, hence any
additional retry should fail
 
.EXAMPLE
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3
 
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 100
Ticks : 1007339
TotalDays : 1.16590162037037E-06
TotalHours : 2.79816388888889E-05
TotalMinutes : 0.00167889833333333
TotalSeconds : 0.1007339
TotalMilliseconds : 100.7339
 
PS> Measure-Command { $retry.processPolicy() }
 
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 250
Ticks : 2502949
TotalDays : 2.89693171296296E-06
TotalHours : 6.95263611111111E-05
TotalMinutes : 0.00417158166666667
TotalSeconds : 0.2502949
TotalMilliseconds : 250.2949
 
PS> Measure-Command { $retry.processPolicy() }
 
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 400
Ticks : 4002831
TotalDays : 4.63290625E-06
TotalHours : 0.00011118975
TotalMinutes : 0.006671385
TotalSeconds : 0.4002831
TotalMilliseconds : 400.2831
 
Creates a retry policy with initial delay of 100 milliseconds with increases of 150 milliseconds and 3 retries
 
.EXAMPLE
PS> $el = $null
PS> try{ 1 / 0 } catch { $el = $_ }
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3 -ErrorReferences @($el)
    -ErrorRecordComparisonType ActivityCompare
PS> $retry.shouldProcess($null)
False
PS> $es = $null
PS> try{ 5/0 }catch{$es = $_}
PS> $retry.shouldProcess($es)
True
 
Creates a linear retry policy that is triggered by the comparison on the Activity of a reference error. A null error
provided did not return a positive match. However a similar error, with the same activity does trigger the match.
 
.EXAMPLE
PS> $el = $null
PS> try{ 1 / 0 } catch { $el = $_ }
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3 -ErrorReferences @($el)
    -ErrorRecordComparisonType AnyCompare
PS> $retry.shouldProcess($null)
True
PS> $es = $null
PS> try{ 5/0 }catch{$es = $_}
PS> $retry.shouldProcess($es)
True
 
Creates a linear retry policy that is triggered with any sample error given the AnyCompare enum value. Both null and a
sample error trigger the match
 
.EXAMPLE
PS> $el = $null
PS> try{ 1 / 0 } catch { $el = $_ }
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3 -ErrorReferences @($el)
    -ErrorRecordComparisonType CategoryCompare
PS> $retry.shouldProcess($null)
False
PS> $es = $null
PS> try{ 5/0 }catch{$es = $_}
PS> $retry.shouldProcess($es)
True
 
Creates a linear retry policy that is triggered with error category match. Null values do not trigger the error while
similar errors do match
 
.EXAMPLE
PS> $el = $null
PS> try{ 1 / 0 } catch { $el = $_ }
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3 -ErrorReferences @($el)
    -ErrorRecordComparisonType IdCompare
PS> $retry.shouldProcess($null)
False
PS> Write-Error -Message 'Some Error' -ErrorId 'RuntimeException'
PS> $es = $Error[0]
PS> $retry.shouldProcess($es)
True
PS> Write-Error -Message 'Some Error' -ErrorId 'SomeId'
PS> $es = $Error[0]
PS> $retry.shouldProcess($es)
False
 
Creates a linear retry policy that is triggered based on the Fully Qualified Id property. Null value does not trigger
the comparison match, while an error with the same ErrorId does trigger the match
 
.EXAMPLE
PS> $el = $null
PS> try{ 1 / 0 } catch { $el = $_ }
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3 -ErrorReferences @($el)
    -ErrorRecordComparisonType NoComparison
PS> $retry.shouldProcess($null)
False
PS> Write-Error -Message 'Some Error' -ErrorId 'RuntimeException'
PS> $es = $Error[0]
PS> $retry.shouldProcess($es)
True
PS> Write-Error -Message 'Some Error' -ErrorId 'SomeId'
PS> $es = $Error[0]
PS> $retry.shouldProcess($es)
True
 
Creates a linear retry policy that is triggered regardles of the object passed since no comparison is made.
 
.EXAMPLE
PS> $el = $null
PS> try{ 1 / 0 } catch { $el = $_ }
PS> $retry = New-LinearRetryPolicy -DelayBase 100 -DelayIncrease 150 -NumberOfRetries 3 -ErrorReferences @($el)
    -ErrorRecordComparisonType NoComparison
PS> $retry.shouldProcess($null)
False
PS> $es = $null
PS> try{ 5/0 }catch{$es = $_}
 
Creates a linear retry policy that is triggered when the type of the inner exception matches. It returns false otherwise
 
#>


function New-LinearRetryPolicy {
  [CmdletBinding()]
  param(
      # Initial delay in milliseconds to start with
      [Parameter(Mandatory)]
      [ValidateScript({$_ -ge 0})]
      [int] $DelayBase = 0,

      # Delay increase in milliseconds
      [Parameter()]
      [ValidateScript({$_ -ge 0})]
      [int] $DelayIncrease = 0,

      # Number of times to retry
      [Parameter(Mandatory)]
      [ValidateScript({$_ -gt 0})]
      [int] $NumberOfRetries = 0,

      # Errors to compare against
      [Parameter()]
      [ValidateNotNullOrEmpty()]
      [System.Management.Automation.ErrorRecord[]] $ErrorReferences = @(),

      # Specify the type of comparison to perform
      [Parameter()]
      [ErrorRecordComparisonType] $ErrorRecordComparisonType = [ErrorRecordComparisonType]::AnyCompare
  )

  $ErrorActionPreference = 'Stop'
  if ($DelayIncrease -eq 0) {
      $DelayIncrease = $DelayBase
  }
  
  return [LinearRetryPolicy]::New(
      $DelayBase, 
      $DelayIncrease, 
      $NumberOfRetries, 
      $ErrorReferences, 
      $ErrorRecordComparisonType)
}

# Implements a linear retry policy
class LinearRetryPolicy : BaseRetryPolicy {
  hidden [int] $RetryCount
  hidden [int] $MaxRetries
  hidden [int] $DelayBaseMs
  hidden [int] $DelayDeltaMs
  hidden [System.Management.Automation.ErrorRecord[]] $ErrorMatches
  hidden [ErrorRecordComparisonType] $ErrorComparisonType

  LinearRetryPolicy(
      # Delay base in milliseconds
      [int] $delayBase, 

      # Increase in delay in milliseconds
      [int] $delayDelta,
      
      # Number of retries to attempt
      [int] $numberOfRetries,
      
      # Errors that will trigger a retry
      [System.Management.Automation.ErrorRecord[]] $errorDetection,
      
      # Types to compare from the errors found
      [ErrorRecordComparisonType] $comparisonType
  ) {
      if ($delayBase -lt 0) {
          throw [xUtilityException]::New(
              "LinearRetryPolicy:BaseRetryPolicy.Constructor",
              [xUtilityErrorCategory]::InvalidParameter,
              "Delay Base (milliseconds) has to be greater or equal to zero"
          )
      }

      if ($delayDelta -lt 0) {
          throw [xUtilityException]::New(
              "LinearRetryPolicy:BaseRetryPolicy.Constructor",
              [xUtilityErrorCategory]::InvalidParameter,
              "Delay Delta (milliseconds) has to be greater or equal to zero"
          )
      }

      if ($numberOfRetries -lt 0) {
          throw [xUtilityException]::New(
              "LinearRetryPolicy:BaseRetryPolicy.Constructor",
              [xUtilityErrorCategory]::InvalidParameter,
              "Number of Retries has to be greater or equal to zero"
          )
      }


      $this.RetryCount = 0
      $this.MaxRetries = $numberOfRetries
      $this.DelayBaseMs  = $delayBase
      $this.DelayDeltaMs = $delayDelta
      $this.ErrorMatches = $errorDetection
      if ($comparisonType -ne $null) {
          $this.ErrorComparisonType = $comparisonType
      }
      else {
          $this.ErrorComparisonType = [ErrorRecordComparisonType]::AnyCompare
      }
  }

  # Gets the policy name
  [string] getPolicyName() {
      return 'LinearRetryPolicy'
  }

  # Get version of the current policy
  [System.Version] getPolicyVersion() {
      return [System.Version] '0.1.0.0'
  }

  # Determines whether to keep processing the policy or exit
  [bool] shouldProcess([System.Management.Automation.ErrorRecord] $operationError) {
      [int] $currentRetryCount = $this.RetryCount + 1
      if ($currentRetryCount -ge $this.MaxRetries) {
          return $false
      }

      return [BaseRetryPolicy]::errorMatches($operationError, $this.ErrorMatches, $this.ErrorComparisonType)
  }

  # Creates a new instance of the implemented policy
  [BaseRetryPolicy] clone() {
      return [LinearRetryPolicy]::New(
          $this.DelayBaseMs,
          $this.DelayDeltaMs,
          $this.MaxRetries,
          $this.ErrorMatches,
          $this.ErrorComparisonType)
  }

  # Process the policy
  [void] processPolicy() {
      if ($this.RetryCount -ge $this.MaxRetries) {
          throw [xUtilityException]::New(
              ("{0}.invokePolicy" -f $this.getPolicyName()),
              [xUtilityErrorCategory]::MaxRetryLimitReached,
              ("Max number of retries reached: {0}/{1}" -f $this.RetryCount, $this.MaxRetries)
          )
      }

      Write-Verbose ("[{0}] Retry {1}/{2} about to sleep {3} milliseconds" -f 
          $this.getPolicyName(),
          $this.RetryCount,
          $this.MaxRetries,
          $this.DelayBaseMs)
      
      Start-Sleep -Milliseconds $this.DelayBaseMs
      $this.DelayBaseMs += $this.DelayDeltaMs
      $this.RetryCount++
  }
}