lib/Classes/Public/TMBrokerSubject.ps1


class TMBrokerSubject {

    #region Non-Static Properties

    [TMBrokerSubjectAction]$Action = [TMBrokerSubjectAction]::new()
    [Int32]$Order = 0
    [TMTask]$Task = [TMTask]::new()
    [Int32]$ProjectId = $this.Task.Project.Id
    [TMBrokerSubjectAsset]$Asset = [TMBrokerSubjectAsset]::new()
    [Object]$ActionRequest

    #endregion Non-Static Properties

    #region Constructors

    TMBrokerSubject() {}

    TMBrokerSubject([TMTask]$task) {
        $this.ProjectId = $task.Project.Id
        $this.Task = $task
        $this.Asset = [TMBrokerSubjectAsset]::new($task.Asset)
        $this.Action = [TMBrokerSubjectAction]::new($task.Action)
    }

    TMBrokerSubject([TMTask]$task, [Nullable[Int32]]$order = 0) {
        $this.Order = $order
        $this.ProjectId = $task.Project.Id
        $this.Task = $task
        $this.Asset = [TMBrokerSubjectAsset]::new($task.Asset)
        $this.Action = [TMBrokerSubjectAction]::new($task.Action)
    }

    TMBrokerSubject([TMTask]$task, [PSCustomObject]$action, [Nullable[Int32]]$order = 0) {
        $this.Order = $order
        $this.ProjectId = $task.Project.Id
        $this.Task = $task
        $this.Asset = [TMBrokerSubjectAsset]::new($task.Asset)
        $this.Action = [TMBrokerSubjectAction]::new($action)
    }

    #endregion Constructors

    #region Non-Static Methods

    [void]UpdateActionSettings([Object]$MethodParams) {
        if ($MethodParams -is [String]) {
            $MethodParams = ($MethodParams | ConvertFrom-Json -Depth 5)
        }
        if (-not ($MethodParams -is [TMTaskActionMethodParam[]])) {
            $MethodParams = $MethodParams | ForEach-Object { [TMTaskActionMethodParam]::new($_) }
        }

        # See what settings were configured on the Action in TM
        $NewSettings = [TMBrokerSubjectActionSettings]::GetSettingsFromMethodParams($MethodParams)

        # Check if the retry settings need to be updated
        if ($this.Action.Settings.Retry.Count -ne $NewSettings.Retry.Count) {
            # Settings have been updated in TM. Apply them
            $this.Action.Settings.Retry = $NewSettings.Retry
        }

        # Check if the timeout settings need to be updated
        if ($this.Action.Settings.Timeout.Seconds -ne $NewSettings.Timeout.Seconds) {
            # Settings have been updated in TM. Apply them
            $this.Action.Settings.Timeout = $NewSettings.Timeout
        }

        # Update the settings' timing based on the Task's last updated timestamp
        if ($this.Task.LastUpdated) {
            switch ($this.Task.Status) {
                'Hold' { $this.Action.Settings.Retry.SetNextRetryDate($this.Task.LastUpdated.ToLocalTime()) }
                'Started' {
                    # If the LastUpdated time is newer, use it to set the timeout
                    if (
                        (-not $this.Action.InvokedAt) -or
                        (($null -ne $this.Action.InvokedAt) -and ($this.Task.LastUpdated.ToLocalTime() -gt $this.Action.InvokedAt))) {
                        $this.Action.Settings.Timeout.SetTimeoutDate($this.Task.LastUpdated.ToLocalTime())
                    }
                }
                default {}
            }
        }
    }

    [void]QueueRetry([String]$TMSession) {
        $this.Action.Settings.Retry.RemainingRetries--
        $this.Action.ExecutionStatus = 'Pending'
        Reset-TMTaskAction -TMSession $TMSession -TaskId $this.Task.Id
        $this.Task | Update-TMTask -TMSession $TMSession -Status 'Ready' -Note "Action reset. Retry $($this.Action.Settings.Retry.Count - $this.Action.Settings.Retry.RemainingRetries) of $($this.Action.Settings.Retry.Count)"
        $this.Task.Status = 'Ready'
    }

    [void]Timeout([String]$TMSession) {
        $this.Action.ExecutionStatus = 'Failed'
        $this.Task | Update-TMTask -TMSession $TMSession -Status 'Hold' -Note "Action Timed out after $($this.Action.Settings.Timeout.Minutes) minutes"
        $this.Task.Status = 'Hold'
    }

    [void]Invoke([String]$TMSession, [Object]$Cache) {
        try {
            Invoke-SubjectTaskAction -TMSession $TMSession -Subject $this -Cache $Cache
        }
        catch {
            $ErrorString = $_.Exception.Message
            if ($ErrorString -match 'The argument .*?Started.*? does not belong') {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] This Subject Task has already been started elsewhere." -ForegroundColor Magenta
                return
            }
            else {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking the Task failed: [$($ErrorString)]" -ForegroundColor Magenta
                return
            }
        }
    }

    [void]Invoke([String]$TMSession) {
        try {
            Invoke-SubjectTaskAction -TMSession $TMSession -Subject $this
        } catch {
            $ErrorString = $_.Exception.Message
            if ($ErrorString -match '"The argument "Started" does not belong') {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] This Subject Task has already been started elsewhere." -ForegroundColor Magenta
                return
            } else {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking the Task failed: [$($ErrorString)]" -ForegroundColor Magenta
                return
            }
        }
    }

    [void]InvokeParallel([String]$TMSession, [Object]$Cache) {
        ## Invoke Parallel execution tasks. This sends the ActionRequest to SessionManager for execution
        try {
            Invoke-SubjectTaskActionParallel -TMSession $TMSession -Subject $this -Cache $Cache
        }
        catch {
            $ErrorString = $_.Exception.Message
            if ($ErrorString -match '"The argument "Started" does not belong') {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] This Subject Task has already been started elsewhere." -ForegroundColor Magenta
                return
            }
            else {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking the Task failed: [$($ErrorString)]" -ForegroundColor Magenta
                return
            }
        }
    }

    [void]InvokeParallel([String]$TMSession) {
        try {
            Invoke-SubjectTaskActionParallel -TMSession $TMSession -Subject $this
        } catch {
            $ErrorString = $_.Exception.Message
            if ($ErrorString -match '"The argument "Started" does not belong') {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] This Subject Task has already been started elsewhere." -ForegroundColor Magenta
                return
            } else {
                Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking the Task failed: [$($ErrorString)]" -ForegroundColor Magenta
                return
            }
        }
    }

    #endregion Non-Static Methods
}


class TMBrokerSubjectAction {

    #region Non-Static Properties

    [Int32]$Id = 0
    [String]$Script
    [TMTaskActionMethodParam[]]$Params = @()
    [String]$Name
    [TMBrokerSubjectActionSettings]$Settings = [TMBrokerSubjectActionSettings]::new()
    [ValidateSet('Started', 'Pending', 'Successful', 'Failed')]
    [String]$ExecutionStatus = 'Pending'

    #endregion Non-Static Properties

    #region Private Fields

    hidden [Nullable[DateTime]]$_invokedAt

    #endregion Private Fields

    #region Constructors

    TMBrokerSubjectAction() {
        $this.addPublicMembers()
    }

    TMBrokerSubjectAction([Int32]$id, [String]$script, [TMTaskActionMethodParam[]]$params, [String]$name) {
        $this.Id = $id
        $this.Params = $params
        $this.Name = $name
        $this.Settings = [TMBrokerSubjectActionSettings]::GetSettingsFromMethodParams($params)
        $this.addPublicMembers()
    }

    TMBrokerSubjectAction([Object]$object) {
        $this.Id = $object.Id
        $this.Script = $object.Script
        $this.Params = $object.methodParams
        $this.Name = $object.Name
        $this.Settings = [TMBrokerSubjectActionSettings]::GetSettingsFromMethodParams($object.methodParams)
        $this.addPublicMembers()
    }

    #endregion Constructors

    #region Private Methods

    hidden [void]addPublicMembers() {
        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'ShouldTimeout',
                { # get
                    return (
                        $this.Settings.Timeout.Enabled -and
                        ((Get-Date) -ge $this.Settings.Timeout.TimeoutDate)
                    )
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'ShouldRetry',
                { # get
                    return (
                        $this.Settings.Retry.Enabled -and
                        ($this.Settings.Retry.RemainingRetries -gt 0) -and
                        ((Get-Date) -ge $this.Settings.Retry.NextRetryDate)
                    )
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'InvokedAt',
                { # get
                    return $this._invokedAt
                },
                { # set
                    param ($value)

                    $this._invokedAt = $value
                    $this.Settings.Timeout.SetTimeoutDate($value)
                }
            )
        )
    }

    #endregion Private Methods

}


class TMBrokerSubjectActionSettings {

    #region Non-Static Properties

    [TMBrokerSubjectActionRetrySetting]$Retry = [TMBrokerSubjectActionRetrySetting]::new()
    [TMBrokerSubjectActionTimeoutSetting]$Timeout = [TMBrokerSubjectActionTimeoutSetting]::new()

    #endregion Non-Static Properties

    #region Constructors

    TMBrokerSubjectActionSettings() {}

    TMBrokerSubjectActionSettings([Int32]$retryCount, [Int32]$retryWaitSeconds, [Int32]$timeoutSeconds) {
        $this.Retry = [TMBrokerSubjectActionRetrySetting]::new($retryCount, $retryWaitSeconds)
        $this.Timeout = [TMBrokerSubjectActionTimeoutSetting]::new($timeoutSeconds)
    }

    TMBrokerSubjectActionSettings([Object]$object) {
        $this.Retry = [TMBrokerSubjectActionRetrySetting]::new($object.RetryCount, $object.RetryWaitSeconds)
        $this.Timeout = [TMBrokerSubjectActionTimeoutSetting]::new($object.TimeoutSeconds)
    }

    #endregion Constructors

    #region Static Methods

    static [TMBrokerSubjectActionSettings]GetSettingsFromMethodParams([TMTaskActionMethodParam[]]$MethodParams) {
        $ActionSettings = [PSCustomObject]@{
            RetryCount       = 0
            RetryWaitSeconds = 30
            TimeoutSeconds   = 0
        }

        $SettingParams = $MethodParams | Where-Object {
            ($_.Context -eq 'USER_DEF') -and
            ($_.ParamName -match 'brokersetting_') -and
            (-not [String]::IsNullOrWhiteSpace($_.Value))
        }

        foreach ($Param in $SettingParams) {
            switch (($Param.ParamName -split '_')[1]) {
                'RetryCount' {
                    $ActionSettings.RetryCount = [Int32]$Param.Value
                }

                { $_ -in 'RetryWaitSeconds', 'WaitSeconds' } {
                    $ActionSettings.RetryWaitSeconds = [Int32]$Param.Value
                }

                { $_ -in 'RetryWaitMinutes', 'WaitMinutes' } {
                    $ActionSettings.RetryWaitSeconds = [Int32]$Param.Value * 60
                }

                {$_ -in 'Timeout', 'TimeoutMinutes'} {
                    $ActionSettings.TimeoutSeconds = [Int32]$Param.Value * 60
                }

                'TimeoutSeconds' {
                    $ActionSettings.TimeoutSeconds = [Int32]$Param.Value
                }

                default { }
            }
        }

        return [TMBrokerSubjectActionSettings]::new($ActionSettings)
    }

    #endregion Static Methods

}


class TMBrokerSubjectActionTimeoutSetting {

    #region Private Fields

    hidden [Nullable[DateTime]]$_timeoutDate
    hidden [Int32]$_seconds = 0

    #endregion Private Fields

    #region Constructors

    TMBrokerSubjectActionTimeoutSetting() {
        $this.addPublicMembers()
        $this.SetTimeoutDate((Get-Date).AddYears(10))
    }

    TMBrokerSubjectActionTimeoutSetting([Int32]$seconds) {
        $this.addPublicMembers()
        $this._seconds = $seconds
        $this.SetTimeoutDate((Get-Date))
    }

    #endregion Constructors

    #region Non-Static Methods

    [void]SetTimeoutDate([Nullable[DateTime]]$LastUpdated) {
        if ($null -ne $lastUpdated) {
            $this._timeoutDate = $LastUpdated.AddSeconds($this._seconds)
        }
    }

    [String]ToString() {
        return "$($this._timeoutDate)"
    }

    #endregion Non-Static Methods

    #region Private Methods

    hidden [void]addPublicMembers() {
        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'Seconds',
                {   # get
                    return $this._seconds
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'Minutes',
                {   # get
                    return ($this._seconds -ge 60 ? [Math]::Round(($this._seconds / 60)) : [Math]::Round(($this._seconds / 60) , 1))
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'TimeoutDate',
                {   # get
                    return $this._timeoutDate
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'Enabled',
                {   # get
                    return ($this._seconds -gt 0)
                }
            )
        )
    }

    #endregion Private Methods

}


class TMBrokerSubjectActionRetrySetting {

    #region Non-Static Properties

    [Int32]$RemainingRetries = 0

    #endregion Non-Static Properties

    #region Private Fields

    hidden [Int32]$_count = 0
    hidden [Int32]$_waitSeconds = 0
    hidden [Nullable[DateTime]]$_nextRetryDate

    #endregion Private Fields

    #region Constructors

    TMBrokerSubjectActionRetrySetting() {
        $this.addPublicMembers()
        $this.SetNextRetryDate((Get-Date).AddYears(10))
    }

    TMBrokerSubjectActionRetrySetting([Int32]$count, [Int32]$waitSeconds) {
        $this.addPublicMembers()
        $this._count = $count
        $this.RemainingRetries = $count
        $this._waitSeconds = $waitSeconds
        $this.SetNextRetryDate((Get-Date))
    }

    TMBrokerSubjectActionRetrySetting([Object]$object) {
        $this.addPublicMembers()
        $this._count = $object.Count
        $this.RemainingRetries = $object.Count
        $this._waitSeconds = $object.WaitSeconds
        $this.SetNextRetryDate((Get-Date))
    }

    #endregion Constructors

    #region Non-Static Methods

    [void]SetNextRetryDate([Nullable[DateTime]]$LastUpdated) {
        $this._nextRetryDate = ${LastUpdated}?.AddSeconds($this._waitSeconds) ?? (Get-Date).AddSeconds($this._waitSeconds)
    }

    [String]ToString() {
        return "$($this._nextRetryDate)"
    }

    #endregion Non-Static Methods

    #region Private Methods

    hidden [void]addPublicMembers() {
        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'Count',
                { # get
                    return $this._count
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'WaitSeconds',
                { # get
                    return $this._waitSeconds
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'WaitMinutes',
                { # get
                    return ($this._waitSeconds -ge 60 ? ([Math]::Round(($this._waitSeconds / 60))) : ([Math]::Round(($this._waitSeconds / 60), 1)))
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'NextRetryDate',
                { # get
                    return $this._nextRetryDate
                }
            )
        )

        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'Enabled',
                { # get
                    return ($this._count -gt 0)
                }
            )
        )
    }

    #endregion Private Methods

}


class TMBrokerSubjectAsset {

    #region Non-Static Properties

    [Int32]$Id
    [String]$Name
    [String]$Class
    [String]$Type
    [TMReference]$Bundle

    #endregion Non-Static Properties

    #region Constructors

    TMBrokerSubjectAsset() {}

    TMBrokerSubjectAsset([Int32]$id, [String]$name, [String]$class, [String]$type, [TMReference]$bundle) {
        $this.Id = $id
        $this.Name = $name
        $this.Class = $class
        $this.Type = $type
        $this.Bundle = [TMReference]::new($bundle)
    }

    TMBrokerSubjectAsset([Object]$object) {
        $this.Id = $object.Id
        $this.Name = $object.Name
        $this.Class = $object.Class
        $this.Type = $object.Type
        $this.Bundle = [TMReference]::new($object.Bundle)
    }

    #endregion Constructors

}