OutSystemsの無限ループ問題

OutSystemsでは無限ループを発生させやすく、一度発生すると解消が難しいため、注意が必要。

# 修正履歴
(2024/08/20追記)ODCの場合の懸念について確認したので追記した

# 問題の概要
サーバサイドで無限ループを作ってしまうと、止めるのが難しい。そこで、無限ループが発生する条件と、各種の対策をまとめておく。

なお、問題がクライアント側（Client Action）で発生したのであれば、タブを閉じてしまえば良いのでさほど問題ではない。

# 発生を防ぐ対策
## なるべくIfでなくFor Eachを使う
Ifループは、カウンタを進める部分や継続条件の判定でミスをすると無限ループになってしまう可能性が、For Eachより高くなる。
よって可能な場合は、For Eachを選ぶ。

## For EachのMaximum Iterationsを設定しておく
For Eachを使った場合でも、Flowの繋ぎ方を間違えると、無限ループになる。
また、開発中に何らかのミスで不要な繰り返しをしてしまうこともある。
そこで、可能な限り、For EachのMaximum Iterationsを設定しておきたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/9390fb7b-db57-4373-d2ac-84cd7f760133.png)

## Text/String_Split Actionを使ってループ対象のListを用意する
繰り返し回数の上限を、事前に明確にできるのなら、
- Local Variableで「1,2,...,N」というText型変数を用意しておく
- Text/String_Split Actionを使って「,」で分割したリストを得る
- リストを対象にFor Eachを適用

という方法で、指定回数のループを行える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/cc5cec9d-af44-78a5-547a-47c383f742d7.png)

## Rangeを自作する
Developer Day TokyoのExternal Logicのセッションで話した方法。
以下のような形式のExternal Logicを用意し、そのOutput ParameterをFor Eachでループする。

- Name: Range
- Input Parameter: (Integer型）生成するListの要素数
- Output Parameter: 指定する要素数のInteger List（含まれる値は1から開始し、1ずつカウントアップ）
- 例：Input Parameter=10のとき、1,2,3,4,5,6,7,8,9,10の計10要素を持つInteger Listが変える

要素を連番にしておけば、今処理しているのが何番目かもListの.Currentから明らかになる。

### コード例

Interface例
``` cs:IDeveloperDayTokyoExample1.cs
using OutSystems.ExternalLibraries.SDK;

namespace DeveloperDayTokyoExample1;

[OSInterface(Description = "Developer Day Tokyoサンプル")]
public interface IDeveloperDayTokyoExample1
{
    [OSAction(Description = "指定要素数のListを返す",
              ReturnName = "Result")]
    public List<int> Range(int Count);
}
```

Class例
``` cs:DeveloperDayTokyoExample1.cs
namespace DeveloperDayTokyoExample1;
public class DeveloperDayTokyoExample1 : IDeveloperDayTokyoExample1
{
    public List<int> Range(int Count)
    {
        return Enumerable.Range(1, Count).ToList();
    }
}
```

# 発生した時の対策
## 運用者が処理を止めるスイッチを組み込んでおく
Timerに実装することがあるパターン。
1. Site Property (OutSystems 11) 、Setting (ODC) にBoolean型のスイッチを作成しておく
1. ループの先頭でスイッチをチェック（If）し、Trueであれば処理の分岐をEndへ

すべてのループに組み込むのは、実装が無駄に複雑になってしまい、現実的ではない。

## 非同期の要素はTimeoutを待つ方法もある
OutSystemsが用意している非同期の仕組みには、それぞれTimeoutがある。そのため、そのTimeoutを待つことで無限ループは強制的に停止される。
- Timer：デフォルトで20分。開発時、運用時共に設定可能
- BPTのAutomatic Activity：5分
- Light BPT：3分

なお、Client Actionから呼び出したサーバー側処理にはタイムアウトがなさそうなので注意（つまり無限ループにすると無限に動作する可能性がある）。サーバー側処理を呼び出した通信自体にはタイムアウトが（Server Request Timeoutとして）あるが、これは通信のみが停止し、背後の処理はそのまま動作し続ける。

### リトライに注意
非同期処理がエラー終了すると、自動でリトライが行われる点には注意。

Timerなら上限回数はConfiguration Toolで設定可能。
Light BPTの場合は、リトライの度に、間隔を大幅に広げていくらしい。
調査した人がいた。
[OutSystems BPT — Timeouts and exceptions in ‘Light Processes’ — Part](https://joaoheleno.medium.com/outsystems-bpt-timeouts-and-exceptions-in-light-processes-part-i-bb483ae28d22)

ロジックのミス無限ループになった場合、リトライするともう一度無限ループに至ってしまう。Timerについては、この問題に行き当たったことがある。

その際には、タイムアウトを待っている間に、Timerを開始直後に正常終了するように変更してPublishしておくことで停止できた。

## 強制的にエラーを発生させる
ループ中の何らかのロジックが例外になるような変更を行う。
これは迂闊に試せないので、アイデアだけになるが、更新先のEntityの定義かデータをエラーになるように修正しておくといった案がある。

## サービス再起動（オンプレミスの場合）
上記の方法がうまくいかない場合は、やむを得ず、IISの再起動等で強制停止するしかないかもしれない。
IISの何を再起動するかというと、たぶんApplication Pool？　テストできる環境がないので動作確認はできないが……。

## サポートに依頼（クラウドの場合）
見出しの通り。

# ODCの場合の懸念
ODCではPublish/リリースしたとき、新しいコンテナが配置される。

ドキュメントでは記述が見つからないのだが、Publish/リリース時に動作中のロジックがあれば、強制停止されず、そのまま動作し続けるのでは？　

 ~~もしそうなら、無限ループしていると、そのロジックは永遠に残ってしまうかもしれない。これはドキュメントかサポートで実際どうかを確認したいところ。確認できる機会があれば、ここに追記する。~~

:::note info
(2024/08/20追記)
[[ODC]Server Request Timeoutにかかったとき、サーバー側の処理も停止することを確認する #outsystems - Qiita](https://qiita.com/jyunji_watanabe/items/4427a2404967f1b1c67c#%E5%85%88%E3%81%AB%E7%B5%90%E8%AB%96)
で確認した。「観察からは、Client Actionからサーバー側の処理（Server/Service Action、ODC上のREST API）を呼び、Server Request Timeoutにかかった場合、サーバー側の処理も停止されていた。」
:::