SQL Agent Powershell script not erroring as expected

Nathan Heaivilin 20 Reputation points
2024-08-07T15:43:48.6233333+00:00

All,

I know that this is a common questions, but the 'fixes' I'm seeing in those questions aren't resolving my issue. The code below is being written to take backups of SSAS Cubes using the SQLServer PS Module.

The basic process is that it pulls the servers, backup location, and backup retention from a JSON file, loops through each server and then takes backups, removes old backups, etc. Think a poor mans version of Ola Hallengren scripts.

My issue is that the error handling I've tried putting in doesn't work correctly. Specifically, it seems to always be returning an exit code of 0 for success, than an exit code of 1. Yet, when I run it in a PS window, I get the results I'm expecting.

Things I've tried:

  • Running as both PowerShell and CMDExec mode in the SQL Agent Job.
  • Throw vs Write-error
  • With and without specifying exit code
  • Various methods of try/catch, try/catch/finally, moving exit logic to END block, etc.

I'm at a loss as to why I'm not getting failures on the SQL Server Agent based upon the code below.

powershell.exe 'D:\dba\dba - SSAS - Take Backups\Backup_SSAS_Databases.ps1'

[CmdletBinding()]
param ()

BEGIN{
    Import-Module SQLServer

    $ErrorActionPreference = "Stop"

    [boolean]$ErrorOccurred = $false
    [boolean]$ExitWhenFinished = $true
    [string]$LogFileName = "SSAS_Backup_log_$(GET-DATE -Format 'yyyyMMdd_HHmmss').txt"
    [string]$FileNameTemplate = "<ServerName>_<DBName>_$(get-date -Format 'yyyyMMdd_HHmmss').abf"
    [string]$ConfigFile = $MyInvocation.MyCommand.Source.Replace($MyInvocation.MyCommand.Name, 'SSAS_Config.json')

    if ($LogPath)
        {[String]$LogFile = "$LogPath\$LogFileName"}
    else 
        {[String]$LogFile = $MyInvocation.MyCommand.Source.Replace($MyInvocation.MyCommand.Name, $LogFileName)  }
    function Write-Log {
        [CmdletBinding()]
        param(
            [Parameter()]
            [ValidateNotNullOrEmpty()]
            [string]$Message,
     
            [Parameter()]
            [ValidateNotNullOrEmpty()]
            [ValidateSet('INFO','WARN','EROR')]
            [string]$Severity = 'INFO',

            [Parameter()]
            [ValidateNotNullOrEmpty()]
            [string]$Directory
        )
        [pscustomobject]@{
            Time = "[$(Get-Date -f "MM/dd/yyyy HH:mm:ss")]"
            Severity = "[$Severity]"
            Message = $Message
        } | Export-Csv -Path "$Directory" -Append -NoTypeInformation -Delimiter "`t"
    }

    Write-Log -Message "Let the games begin." -Severity INFO -Directory $LogFile
}
PROCESS
{
    try {
        ## Read the config file to capture the required server information.
        $SSASConfigInfo = (Get-Content -path $ConfigFile | Out-String | ConvertFrom-Json)     
        Write-Log -Message "Config File: $ConfigFile" -Severity INFO -Directory $LogFile
        Write-Log -Message "Records Read : $($SSASConfigInfo.Count)" -Severity INFO -Directory $LogFile
        Write-Log -Message "Exit when finished : $ExitWhenFinished)" -Severity INFO -Directory $LogFile
        

        ## Loop through each server
        foreach ($ServerInfo in $SSASConfigInfo)
        {
            [string]$Server = $ServerInfo.SSAS_Server
            [string]$BackupFolder = $ServerInfo.BackupFolder
            [int]$RetentionInHours = $ServerInfo.Retention

            Write-Log -Message "Server:  $Server" -Severity INFO -Directory $LogFile
            Write-Log -Message "BackupPath: $BackupFolder" -Severity INFO -Directory $LogFile
            Write-Log -Message "Retention: $RetentionInHours hours."  -Severity INFO -Directory $LogFile
            Write-Log -Message "Error Occurred : $ErrorOccurred"   -Severity INFO -Directory $LogFile

            Write-Log "Starting backups of [$Server]."  -Severity INFO -Directory $LogFile
            ## Let's get the db/cube information from the SSAS Server
            try
            {
                [xml]$AllDBInfo = Invoke-ASCmd -Server $Server -Query  "<Discover xmlns='urn:schemas-microsoft-com:xml-analysis'><RequestType>DBSCHEMA_CATALOGS</RequestType><Restrictions /><Properties /></Discover>" -ErrorAction Stop
                $DBList = $AllDBInfo.DiscoverResponse.return.root.row.Catalog_Name
                Write-Log "DB count retrieved = $($DBList.Count)"  -Severity INFO -Directory $LogFile
            }
            catch {
                $ErrorOccurred = $true
                Write-Log "$($_.Exception.Message)" -Severity EROR -Directory $LogFile
                continue
            }
           
            foreach ($db in $DBList)
            {
                ## Let's set our vairables 
                Write-Log "`n"  -Severity INFO -Directory $LogFile
                Write-Log "Starting backup of [$db]."  -Severity INFO -Directory $LogFile
                $BackupFileName = $FileNameTemplate.Replace('<ServerName>', $Server)
                $BackupFileName = $BackupFileName.Replace('<DBName>', $DB)

                $BackupPath = Join-Path -Path $BackupFolder -ChildPath $Server
                $BackupPath = Join-Path -Path $BackupPath -ChildPath $db
                $BackupPath = Join-Path -Path $BackupPath -ChildPath 'SSASBackup'

                $FullBackupName = join-path -path $BackupPath -childpath $BackupFileName
                Write-Log "Backup Path: $BackupPath"  -Severity INFO -Directory $LogFile
                Write-Log "Backup Name: $BackupFileName"  -Severity INFO -Directory $LogFile
                Write-Log "Full Path  : $FullBackupName" -Severity INFO -Directory $LogFile
                
                if (test-path -path $BackupPath)
                    {Write-Log "BackupPath validated successfully."-Severity INFO -Directory $LogFile}
                else
                    {
                        Write-Log "Created the backup path: '$BackupPath'"-Severity INFO -Directory $LogFile
                        New-Item -Path $BackupPath -ItemType Directory | out-null
                    }
                
                <##############################################     Backup SSAS Cube/DB #########################################>
                try
                {
                    write-log "Backup Command:  Backup-ASDatabase -Server $Server -BackupFile $FullBackupName -Name $db -AllowOverwrite -ApplyCompression -Verbose" -Severity INFO -Directory $LogFile
                    Write-Log "Backup Start" -Severity INFO -Directory $LogFile
                    measure-command {Backup-ASDatabase -Server $Server -BackupFile $FullBackupName -Name $db -AllowOverwrite -ApplyCompression -ErrorAction Stop} -OutVariable duration | out-null
                    Write-Log "Backup Command Completed. "  -Severity INFO -Directory $LogFile
                    Write-Log "Backup Duration: $($duration.Hours) hour(s) $($duration.Minutes) minute(s) $($duration.Seconds) seconds" -Severity INFO -Directory $LogFile
                    write-log "Results: Success." -Severity INFO -Directory $LogFile
                    
                }
                catch
                {
                    ## write the error to the log file and then continue to the next cube.  Note this will also bypass the delete of the older backup, which is intended.
                    Write-Log -Message "$($Error.exception.message)" -Severity EROR -Directory $LogFile
                    Write-Log "Backup Command Completed"  -Severity INFO -Directory $LogFile
                    Write-Log "Backup Duration: $($duration.Hours) hour(s) $($duration.Minutes) minute(s) $($duration.Seconds) seconds" -Severity INFO -Directory $LogFile
                    Write-Log -Message "Result: Fail" -Severity INFO -Directory $LogFile
                    $ErrorOccurred = $true
                    continue
                }
        
                ## Let's remove older backups
                if ($RetentionInHours -gt 0)
                    {$RemoveFilesOlderThan = (get-date).AddHours($Retentioninhours * -1)}
                else 
                    {$RemoveFilesOlderThan = (get-date).AddHours($Retentioninhours)}
        
                ## Get file list
                $filesToRemove = get-childitem -Path $BackupPath | Where-Object {$_.extension -ieq '.abf' -and $_.LastWriteTime -le $RemoveFilesOlderThan}

                try 
                    {$filesToRemove  |  Remove-Item -ErrorAction Stop }
                catch {
                    ## Write the error but contiue. Shouldn't stop taking new backups if we can't remove the old ones.  
                    Write-Log -Message "$($Error.exception.message)" -Severity EROR -Directory $LogFile
                    $ErrorOccurred = $true
                    continue
                }     
                
                Write-Log "Server: [$server] completed.`n"  -Severity INFO -Directory $LogFile
            }
        }
    }
    catch {
        Write-Log -Message 'Primary TRY/CATCH triggered.  Error below.' -Severity EROR -Directory $LogFile
        Write-Log -Message "$($Error.exception.message)" -Severity EROR -Directory $LogFile
        Write-Log "Server [$Server] completed. `n"  -Severity INFO -Directory $LogFile
        $ErrorOccurred = $true
    }
    finally {
    
        if ($ExitWhenFinished -and $ErrorOccurred) 
            {
                Write-Error  -Message "Error has occurred.  Log at $LogFile"
                #throw "Error has occurred.  Log at $LogFile"
                [System.Environment]::Exit(1)
            }
        elseif ( $ExitWhenFinished) 
            {
                Write-Host 'Process completed Successfully.'
                [System.Environment]::Exit(0)
            }
        else {
            Write-Host 'The process completed, but unable to determine if successful or not.  This is basically a catch all statement.  Review logs and code logic to figure out how we got here.'
        }
    }
}
END{
    Write-Verbose 'Got to the END segment of the code.'     
}

SQL Server
SQL Server
A family of Microsoft relational database management and analysis systems for e-commerce, line-of-business, and data warehousing solutions.
14,491 questions
Windows Server PowerShell
Windows Server PowerShell
Windows Server: A family of Microsoft server operating systems that support enterprise-level management, data storage, applications, and communications.PowerShell: A family of Microsoft task automation and configuration management frameworks consisting of a command-line shell and associated scripting language.
5,628 questions
PowerShell
PowerShell
A family of Microsoft task automation and configuration management frameworks consisting of a command-line shell and associated scripting language.
2,864 questions
0 comments No comments
{count} votes

Accepted answer
  1. Rich Matheisen 47,686 Reputation points
    2024-08-09T02:42:14.9333333+00:00

    Whether or not it fixes the problem, try the code below. It's basically your code minus the four scriptblocks (begin, process, finally, and end). Because your script isn't accepting pipelined input there's no need for a PROCESS block, the BEGIN block code can just be part of script body (it will still execute just once before the code in what was the PROCESS block). The code in the scriptblock FINALLY will run once the main "foreach" finishes. Removing those scriptblocks makes error handling clearer; there's no longer nested try/catch blocks.

    FWIW, there's an undefined variable on line 14 ($logfile). It won't disrupt anything unless you add "Set-PSDebug -Strict" in the script (which you should do while you're developing the code). I eliminated a pipeline (or two) in your code, and I replaced the variable interpolation inside double-quoted strings with PowerShell format operators ("-f") mostly because it's cleaner and also eliminates any quoting errors. I also added some error checking in the code that processed the config file.

    NOTE: I have no way to check the execution of the code -- I have no SQL server.

    [CmdletBinding()]
    param ()
    # BEGIN
        Import-Module SQLServer     # if Import-Module fails shouldn't the script terminate????
    
        $ErrorActionPreference = "Stop"
    
        [boolean]$ErrorOccurred     = $false
        [boolean]$ExitWhenFinished  = $true
        [string]$LogFileName        = "SSAS_Backup_log_{0}.txt" -f (GET-DATE -Format 'yyyyMMdd_HHmmss')
        [string]$FileNameTemplate   = "<ServerName>_<DBName>_{0}.abf" -f (get-date -Format 'yyyyMMdd_HHmmss')
        [string]$ConfigFile         = $MyInvocation.MyCommand.Source.Replace($MyInvocation.MyCommand.Name, 'SSAS_Config.json')
    
        if ($LogPath){      # $LogPath is undefined! If "Set-PSDebug -Strict" was used the script would throw an error (undefined variable)
            [String]$LogFile = "{0}\{1}" -f $LogPath, $LogFileName
        }
        else {
            [String]$LogFile = $MyInvocation.MyCommand.Source.Replace($MyInvocation.MyCommand.Name, $LogFileName)  
        }
        function Write-Log {
            [CmdletBinding()]
            param(
                [Parameter()]
                [ValidateNotNullOrEmpty()]
                [string]$Message,
         
                [Parameter()]
                [ValidateNotNullOrEmpty()]
                [ValidateSet('INFO','WARN','EROR')]
                [string]$Severity = 'INFO',
    
                [Parameter()]
                [ValidateNotNullOrEmpty()]
                [string]$Directory
            )
            [pscustomobject]@{
                Time = "[$(Get-Date -f "MM/dd/yyyy HH:mm:ss")]"
                Severity = "[$Severity]"
                Message = $Message
            } | Export-Csv -Path "$Directory" -Append -NoTypeInformation -Delimiter "`t"
        }   # end Write-Log function
    
        Write-Log -Message "Let the games begin." -Severity INFO -Directory $LogFile
    #PROCESS
        try {
            ## Read the config file to capture the required server information.
            $SSASConfigInfo = (Get-Content -path $ConfigFile | Out-String | ConvertFrom-Json)
            if ($SSASConfigInfo.Count -lt 1){
                Throw "Empty config file"
            }
            Write-Log -Message "Config File: $ConfigFile" -Severity INFO -Directory $LogFile
            Write-Log -Message "Records Read : $($SSASConfigInfo.Count)" -Severity INFO -Directory $LogFile
            Write-Log -Message "Exit when finished : $ExitWhenFinished)" -Severity INFO -Directory $LogFile
        }
        catch{
            Write-Log -Message "Failed to process configuration file [$ConfigFile]" -Severity "EROR" -Directory $LogFile
            $ErrorOccurred = $true
            Exit 1
        }
    
        ## Loop through each server
        foreach ($ServerInfo in $SSASConfigInfo)
        {
            [string]$Server = $ServerInfo.SSAS_Server
            [string]$BackupFolder = $ServerInfo.BackupFolder
            [int]$RetentionInHours = $ServerInfo.Retention
    
            Write-Log -Message "Server:  $Server" -Severity INFO -Directory $LogFile
            Write-Log -Message "BackupPath: $BackupFolder" -Severity INFO -Directory $LogFile
            Write-Log -Message "Retention: $RetentionInHours hours."  -Severity INFO -Directory $LogFile
            Write-Log -Message "Error Occurred : $ErrorOccurred"   -Severity INFO -Directory $LogFile
    
            Write-Log "Starting backups of [$Server]."  -Severity INFO -Directory $LogFile
            ## Let's get the db/cube information from the SSAS Server
            try {
                $query = "<Discover xmlns='urn:schemas-microsoft-com:xml-analysis'><RequestType>DBSCHEMA_CATALOGS</RequestType><Restrictions /><Properties /></Discover>"
                [xml]$AllDBInfo = Invoke-ASCmd -Server $Server -Query $query -ErrorAction Stop
                $DBList = $AllDBInfo.DiscoverResponse.return.root.row.Catalog_Name
                Write-Log "DB count retrieved = $($DBList.Count)"  -Severity INFO -Directory $LogFile
            }
            catch {
                $ErrorOccurred = $true
                Write-Log "$($_.Exception.Message)" -Severity EROR -Directory $LogFile
                continue    # foreach ($ServerInfo in $SSASConfigInfo)
            }
               
            foreach ($db in $DBList){
                ## Let's set our vairables 
                Write-Log "`n"  -Severity INFO -Directory $LogFile
                Write-Log "Starting backup of [$db]."  -Severity INFO -Directory $LogFile
                $BackupFileName = $FileNameTemplate.Replace('<ServerName>', $Server)
                $BackupFileName = $BackupFileName.Replace('<DBName>', $DB)
    
                $BackupPath = Join-Path -Path $BackupFolder -ChildPath $Server
                $BackupPath = Join-Path -Path $BackupPath -ChildPath $db
                $BackupPath = Join-Path -Path $BackupPath -ChildPath 'SSASBackup'
    
                $FullBackupName = join-path -path $BackupPath -childpath $BackupFileName
                Write-Log "Backup Path: $BackupPath"  -Severity INFO -Directory $LogFile
                Write-Log "Backup Name: $BackupFileName"  -Severity INFO -Directory $LogFile
                Write-Log "Full Path  : $FullBackupName" -Severity INFO -Directory $LogFile
                    
                if (test-path -path $BackupPath){
                    Write-Log "BackupPath validated successfully."-Severity INFO -Directory $LogFile
                }
                else{
                    Write-Log "Created the backup path: '$BackupPath'"-Severity INFO -Directory $LogFile
                    New-Item -Path $BackupPath -ItemType Directory | out-null
                }
                    
                <##############################################     Backup SSAS Cube/DB #########################################>
                try {
                    write-log "Backup Command:  Backup-ASDatabase -Server $Server -BackupFile $FullBackupName -Name $db -AllowOverwrite -ApplyCompression -Verbose" -Severity INFO -Directory $LogFile
                    Write-Log "Backup Start" -Severity INFO -Directory $LogFile
                    measure-command {
                            Backup-ASDatabase -Server $Server -BackupFile $FullBackupName -Name $db -AllowOverwrite -ApplyCompression -ErrorAction Stop
                            } -OutVariable duration | out-null
                    Write-Log "Backup Command Completed. "  -Severity INFO -Directory $LogFile
                    Write-Log "Backup Duration: $($duration.Hours) hour(s) $($duration.Minutes) minute(s) $($duration.Seconds) seconds" -Severity INFO -Directory $LogFile
                    write-log "Results: Success." -Severity INFO -Directory $LogFile
                }
                catch {
                    ## write the error to the log file and then continue to the next cube.  Note this will also bypass the delete of the older backup, which is intended.
                    Write-Log -Message "$($Error.exception.message)" -Severity EROR -Directory $LogFile
                    Write-Log "Backup Command Completed"  -Severity INFO -Directory $LogFile
                    Write-Log "Backup Duration: $($duration.Hours) hour(s) $($duration.Minutes) minute(s) $($duration.Seconds) seconds" -Severity INFO -Directory $LogFile
                    Write-Log -Message "Result: Fail" -Severity INFO -Directory $LogFile
                    $ErrorOccurred = $true
                    continue    # foreach ($db in $DBList)
                }
            
                ## Let's remove older backups
                if ($RetentionInHours -gt 0){
                    $Retentioninhours *= -1
                }
                $RemoveFilesOlderThan = (get-date).AddHours($Retentioninhours)
            
                ## Get file list
                $filesToRemove = (get-childitem -Path $BackupPath | 
                                    Where-Object {$_.extension -ieq '.abf' -and $_.LastWriteTime -le $RemoveFilesOlderThan}).FullName
    
                try {
                    Remove-Item -Path $filesToRemove -ErrorAction Stop 
                }
                catch {
                    ## Write the error but contiue. Shouldn't stop taking new backups if we can't remove the old ones.  
                    Write-Log -Message "$($Error.exception.message)" -Severity EROR -Directory $LogFile
                    $ErrorOccurred = $true
                    continue    # foreach ($db in $DBList)
                }     
                Write-Log "Server: [$server] completed.`n"  -Severity INFO -Directory $LogFile
            }   # end foreach ($db in $DBList)
        }    # end foreach ($ServerInfo in $SSASConfigInfo)
    # FINALLY
        if ($ExitWhenFinished -and $ErrorOccurred){
            Write-Error  -Message "Error has occurred.  Log at $LogFile"
            #throw "Error has occurred.  Log at $LogFile"
            Exit 1
        }
        elseif ($ExitWhenFinished){
            Write-Host 'Process completed Successfully.'
            Exit 0
        }
        else {  # $ExitWhenFinished is not $true
            Write-Host 'The process completed, but unable to determine if successful or not.  This is basically a catch all statement.  Review logs and code logic to figure out how we got here.'
        }
    
    #END
        Write-Verbose 'Got to the END segment of the code.'
        # at this point the script will exit without setting exit code
        # even though $ExitWhenFinished implies the script should continue  
    
    1 person found this answer helpful.

2 additional answers

Sort by: Most helpful
  1. Erick Moreno 330 Reputation points
    2024-08-07T17:50:21.35+00:00

    Please try changing the scope for your $ErrorActionPreference variable, scope changes a bit when calling the script file. Don't forget to change it back at the end of the script.

    $global:ErrorActionPreference = 'Stop'
    

  2. Rich Matheisen 47,686 Reputation points
    2024-08-07T19:47:30.28+00:00

    What is the intention of using [System.Environment]::Exit(0) (or 1)? That kills the entire PowerShell process. Using PowerShells' "Exit 0" (or 1) just exits the script or function.

    If you add -Verbose to the parameters when starting the script, do you see the 'Got to the END segment of the code.'


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.