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'
    hidden [Nullable[DateTime]]$_invokedAt

    #endregion Non-Static Properties

    #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 Non-Static Methods

    hidden addPublicMembers() {
        $MemberSplat = @{
            Name        = 'ShouldTimeout'
            MemberType  = 'ScriptProperty'
            Value       = {
                return (
                    $this.Settings.Timeout.Enabled -and
                    ((Get-Date) -ge $this.Settings.Timeout.TimeoutDate)
                )
            }
            SecondValue = {
                Write-Warning "This is a readonly property"
            }
        }
        $this | Add-Member @MemberSplat

        $MemberSplat = @{
            Name        = 'ShouldRetry'
            MemberType  = 'ScriptProperty'
            Value       = {
                return (
                    $this.Settings.Retry.Enabled -and
                    ($this.Settings.Retry.RemainingRetries -gt 0) -and
                    ((Get-Date) -ge $this.Settings.Retry.NextRetryDate)
                )
            }
            SecondValue = {
                Write-Warning "This is a readonly property"
            }
        }
        $this | Add-Member @MemberSplat

        $MemberSplat = @{
            Name        = 'InvokedAt'
            MemberType  = 'ScriptProperty'
            Value       = {
                return $this._invokedAt
            }
            SecondValue = {
                param($value)
                $this._invokedAt = $value
                $this.Settings.Timeout.SetTimeoutDate($value)
            }
        }
        $this | Add-Member @MemberSplat
    }

    #endregion Non-Static 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 Non-Static Properties
    #endregion Non-Static Properties

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

}