【UiPath】APIで長時間実行ジョブを検出する

この投稿は「UiPath Advent Calendar 2024」の7日目のエントリです。

https://qiita.com/advent-calendar/2024/uipath

# 長時間実行中のジョブ
OCのジョブ管理で「ジョブが実行中のままで、手動で停止する」という時があります。
「**気付くまでに時間がかかると、後続ジョブのリカバリ対応に追われてしまうため、なるべく早く気づきたい！**」という悩みを「APIを使って定期監視することで解決しよう」という話です。

# どういう時に、長時間実行になるのか？
そもそも「**アクティビティに適切なタイムアウトが設定されていれば、タイムアウトエラーで終了する**」はずですが、エラーにならずに実行中のままになることがあります。例えば、
```
 - ミリ秒のはずが秒で指定していた（指定ミス。待機時間が1000倍に）
 - アクティビティによってはレアなタイミングで、タイムアウト設定が無視されてずっと待機状態になる
```
いずれも、意図しないケースです。

プロセス実行には「ジョブ開始、トリガー開始、Appsからの実行」などがありますが、トリガーには 「ジョブの実行を終了するスケジュールを設定」 という「**開始から一定時間経過で、自動停止させる機能**」があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154139/fee309a4-b75f-4f9b-af49-e4c318336a62.png)
<br>
逆に言うと、トリガー以外では、この「長時間動いている場合は自動停止させる」という機能がありません。例えば、OCから手動で再実行した場合や、UiPathAppsの「プロセスを開始」で実行したジョブは、この時限制約が無く、無限に動き続けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154139/7574a274-d265-4e3a-9b45-6affeb030aee.png)
<br>

ちなみに「Apps経由実行」はその性質上「業務ユーザーに開放している」ことが多く、管理者の監視下にないため、気付くのが遅れてしまいがち。
なので「Apps経由実行」を実現すると、この「長時間実行を検知したい」という課題にぶつかります。


# APIで定期的に監視する
長時間実行を監視する方法として、例えば、30分おきにAPIで実行中のジョブを監視して、「2時間以上 実行中 のジョブがないか」をチェックする方法があります。

今回は、どこかのマシンからPowerShellでAPI呼び出しをする想定で実装します。タスクスケジューラーで一定時間おきに実行するように設定すれば、定期監視の出来上がりです。

どこかで勝手に実行して欲しい！という場合は、クラウド契約してサーバーレスで実行かと思いますが、個人的には、PowerAutomateの「スケジュール済みクラウド フロー」で実行するのがオススメです。（安い＆作りやすい＆他にも色々使い道がある）

# コードサンプル

以下は「開始から120分以上経過したジョブがあれば、Slackに通知する」PowerShellのコードです。

``` bat
# テナントURL
$baseUrl = 'https://cloud.uipath.com/{個別のID}/{テナント名}'

# プライベートアクセストークン
$token = 'rt_・・・・・・（AutomationCloudで取得できます）'

# フォルダー
$ary_folder = @()
$ary_folder += New-Object PSObject -Property @{Id = "6127328"; Name = "管理部"}
$ary_folder += New-Object PSObject -Property @{Id = "6127329"; Name = "マーケ部"}

# しきい値（開始からN分以上経過したジョブがあれば通知）
$th_Minutes = 120

# Slack投稿
$slack_channel = "#uipath"
$slack_token = "xoxb-・・・・・・"

# 実行中ジョブの取得
$ary_job_running = @()
$url = $baseUrl + '/orchestrator_/odata/Jobs?$filter=State%20eq%20''Running''' # 実行中のみ
foreach($folder in $ary_folder){
    $headers = @{"Authorization" = "Bearer " + $token; "X-UIPATH-OrganizationUnitId" = $folder.id}
    $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -ContentType "application/json"
    if ($response.StatusCode -lt 300){
        foreach($dat in $response.value){
            $item = New-Object PSObject -Property @{
                StartTime = (Get-Date -Date $dat.StartTime).AddHours(0).ToString("yyyy/MM/dd HH:mm:ss")
                LapseTime = (Get-Date -Date "2000/01/01 00:00:00").AddSeconds((New-TimeSpan -Start $dat.StartTime -End $now).TotalSeconds).ToString("HH:mm:ss")
                LapseMinutes = [Math]::Floor((New-TimeSpan -Start $dat.StartTime -End (Get-Date)).TotalMinutes)
                State = $dat.State
                MachineName = $dat.HostMachineName
                UnitName = $folder.Name
                Name = if($dat.SourceType -eq "Schedule"){ $dat.Source }else{ $dat.ReleaseName } # 実行元が「Schedule(タイムトリガー)」なら実行名
                SourceType = $dat.SourceType
            }
            $ary_job_running += $item
        }
    }else{
         Write-Host $response.StatusCode
         Write-Host $response.StatusDescription
         Throw 'OC-API 呼び出しエラー'
    }
    Write-Host "[GetJob]Sleep - 5 Seconds...(Optional delay between requests)"
    Sleep 5
}

Write-Host ("[Getjob]Result - data count: " + $ary_job_running.count)
$ary_job_running = $ary_job_running | Select-Object "StartTime","LapseTime","LapseMinutes","State","MachineName","UnitName","SourceType","Name" 
$ary_job_running | Format-Table


# N分以上経過に絞り込み
$ary_alert = ""
foreach($job in $ary_job_running){
    if($job.LapseMinutes -ge $th_Minute){
        $ary_alert += $job.StartTime + " 開始 (" + $job.LapseMinutes + "分経過) " + $job.UnitName + "「" + $job.Name + "」@" + $job.MachineName
    }
}
Write-Host ("[Getjob]Filter - data count: " + $ary_alert.count)

# Slack投稿
if($ary_alert.count -gt 0){
    $message = @{channel = $slack_channel; text = ":alarm_clock: 長時間実行中ジョブ検出 ※ 閾値:" + $th_Minute + "分\n" + $ary_alert -join '\n'}
    $response = Invoke-WebRequest -Uri "https://slack.com/api/chat.postMessage" `
                      -Method Post `
                      -ContentType "application/json" `
                      -Headers @{Authorization = "Bearer $slack_token"} `
                      -Body ($message | ConvertTo-Json)
    $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -ContentType "application/json"
    Write-Host ("[SlackPost]Response - StatusCode: " + $response.StatusCode)
    if ($response.StatusCode -lt 300){
        Write-Host ("[SlackPost]Response - Content: " + $response.Content)
    }else{
         Write-Host $response.StatusCode
         Write-Host $response.StatusDescription
         Throw 'SlackAPI 呼び出しエラー'
    }
}
```

OCのAPI「odata/Jobs」から実行中のジョブを取得し、実行開始から一定期間経過したジョブがあれば、Slackに通知しています。
（私はもう少し手を加えて、プロセス名で保守担当を割り出して、メンションをつけて通知する形で運用しています。通知が飛んでくれば 早く気付けるので）

# 実行結果・出力内容
上記コードを実行すると、長時間実行中のジョブがあれば、指定のSlackチャンネルに通知が投稿されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154139/332cb24a-80cf-e163-71d3-d3ee9ff72a53.png)

# 終わりに
以上、APIで長時間実行ジョブを検出する方法の紹介でした。

OCありの現場で「RPAの本数を増やしたい・規模を拡大したい」場合、無いと困る機能なので、なにかの参考になれば幸いです。（UiPathの標準機能であれば良いんですけどね。）