Transaction Finalizersについてまとめてみた

##概要

Summer'21リリースにてTransaction Finalizers機能がGAになるようです。

・[Attach Actions to Asynchronous Apex Jobs Using Transaction Finalizers (Generally Available)](https://help.salesforce.com/articleView?id=release-notes.rn_apex_Transaction_Finalizers_Interface_GA.htm&type=5&release=232)

これはキュー可能Apexの処理が失敗したときにリトライ処理を実施させるなど、キュー可能Apexの処理後に何らかのアクションを実施させることができる機能になります。
これまでもベータ版が出ていたのですが今回GAになるとのことで勉強してみましたので以下にまとめます。

Apex開発者ガイドの内容を参考にしています。
[Transaction Finalizers (ベータ)](https://developer.salesforce.com/docs/atlas.ja-jp.230.0.apexcode.meta/apexcode/apex_transaction_finalizers.htm)

##Transaction Finalizersとは
キュー可能Apexに紐づけることで、キュー可能apexのジョブ実行終了後に何らかの処理を実行させることができる機能です。
キュー可能apexが成功したか・未対応の例外やガバナ制限によって失敗かの判定ができるので、キュー可能Apexの処理状況に応じてリトライをさせたりログを出力したりといった後処理を実行させることができます。

リトライさせるためにキューに再度ジョブを追加することは5回まで可能です。

##実装方法
まず初めに、キュー可能Apexに`Finalizer`を実装する必要があります。

```Java
public without sharing class SampleTransactionFinalizers implements Queueable, Finalizer {}
```

次に、キュー可能Apexのexecuteメソッド内で、`System.attach(finalizer)`でキュー可能ApexにFinalizerを紐づけます。
これをしないとFinalizerは起動しません。

```Java
//Finalizerをセットする
Finalizer finalizer = new SampleTransactionFinalizers();
System.attachFinalizer(finalizer);
```

最後に`execute(FinalizerContext ctx)`メソッドをクラス内で実装します。
これでキュー可能Apex実行後にexecuteメソッドが実行されることとなります。

```Java
public void execute(FinalizerContext context) {}
```

##FinalizerContextのメソッド
FinalizerContextには4つのメソッドがあり、それらをexecute内で使用することが可能です。

・`global Id getAsyncApexJobId {}`
紐づけられたキュー可能ジョブのIDを返します。

・`global String getRequestId {}`
実行した処理のリクエストIDを返します。

・`global System.ParentJobResult getResult {}`
実行した処理が成功したかどうかを示す列挙型の値を返します。
`System.ParentJobResult.SUCCESS`または`System.ParentJobResult.NHANDLED_EXCEPTION`のどちらかになります。

・`global System.Exception getException {}`
発生したExceptionまたはnullを返します。


##検証
取引先を作成するキュー可能Apexをサンプルで作成してみました。
実行するときは引数に取引先の名前を文字列で指定しますが、その際に名前に応じて例外を発生させるようにしてFinalizerの挙動を確認しました。
結果としては以下の通りでした。

例外をキャッチできた場合(MathException)：Finalizerはキュー可能Apexが正常に終了したとみなす。
例外をキャッチできなかった場合(NullPointerException)：Finalizerはキュー可能Apexが失敗したとみなす。
ガバナ制限に抵触した場合(LimitException)：Finalizerはキュー可能Apexが失敗したとみなす。

ガバナ制限もちゃんと処理してくれるので便利そうですね。

```Java
public without sharing class SampleTransactionFinalizers implements Queueable, Finalizer {
    //Finalizerをimplementsする必要がある

    //作成する取引先の名前
    private String name;

    public SampleTransactionFinalizers() {}

    public SampleTransactionFinalizers(String name) {
        this.name = name;
    }

    public void execute(QueueableContext context) {
        try {
            String jobId = context.getJobId();
            System.debug('jobId is ' + jobId);

            //Finalizerをセットする
            Finalizer finalizer = new SampleTransactionFinalizers();
            System.attachFinalizer(finalizer);

            // 設定された名前に応じてExceptionを起こすか決定
            if (this.name.contains('MathException')) {  //Exceptionを起こす
                System.debug('Occuring MathException.');
                Integer i = 1 /0;
            }
            else if (this.name.contains('NullPointerException')) { //NullPointerExceptionを起こす
                System.debug('Occuring NullPointerException.');
                List<String> str;
                str[0] = 'a';
            }
            else if (this.name.contains('LimitException')) { //LimitExceptionを起こす
                System.debug('Occuring LimitException.');
                List<Account> acc;
                for (Integer i = 0; i <= 200; i++) {
                    acc = [SELECT ID FROM ACCOUNT LIMIT 1];
                }
            }
            else { //正常時の処理
                System.debug('Successfully record created.');
                Account acc = new Account(name=this.name);
                insert acc;
            }
        }
        catch (MathException e) {
            //Exceptionをキャッチできたのでその場でリトライするパターン
            //Finalizerは正常終了時の処理となる
            System.debug('MathException Occured.');
            String newJobId = System.enqueueJob(new SampleTransactionFinalizers('Mathretry'));
        }
        catch (NullPointerException e) {
            //ExceptionをキャッチできたがFinalizerでリトライさせるパターン
            //Finalizerはエラー時の処理となる
            System.debug('NullPointerException Occured.');
            throw e;
        }
    }

    public void execute(FinalizerContext context) {
        //紐づけられたキュー可能ApexのjobIdの取得
        String parentJobId = context.getAsyncApexJobId();

        //イベントログに使えるrequestidの取得
        String requestId = context.getRequestId();

        //ジョブの結果を取得(SUCCESS または UNHANDLED_EXCEPTION)
        System.ParentJobResult result = context.getResult();

        //発生した例外の取得
        System.Exception e = context.getException();

        System.debug('Attached jobId : ' + parentJobId);
        System.debug('RequestId: ' + requestId);

        if (result == System.ParentJobResult.SUCCESS) { //成功時の処理
            System.debug('Queueable apex completed successfully.');
        }
        else {  //失敗時の処理
            System.debug('Queueable apex failed.');
            System.debug('Error message: ' + e.getMessage());
            //リトライ処理
            String newJobId = System.enqueueJob(new SampleTransactionFinalizers('retry'));
        }
    }
}
```

##まとめ
ガバナ制限や考慮漏れの例外発生時などに取り急ぎのエラー処理・ロギング処理ができるのはいいですね。