The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

長時間実行アクティビティのためのエクステンション

はじめに

QuarkusのLRAエクステンションは、インタラクションが終了したときに、成功した場合と失敗した場合のいずれかで確定的に合意したいと考えているJAX-RSサービスを構築する際に便利です。成功した場合は、すべての参加者は、他のすべてのサービスも同様に行うことを認識して、クリーンアップできます。逆に、失敗した場合は、参加者はインタラクション中に行われた行為をお互いに補償することを知っています。この機能は、参加したサービスがコンセンサスを得て、アトミックな結果を得ることができることを意味します。

このサービスインタラクションをLRA(Long Running Actionの略)と呼んでいます。LRAは、信頼性の高いサービスインタラクションを構築するための特定の特性と保証を持っています。各アクションには一意の識別子(LRAコンテキスト)が割り当てられ、他のLRAと区別できるようになっています。

サービスは、JAX-RSメソッドを @LRA annotation でマークすることで、LRAを開始(または既存のものに参加)します。このようなメソッドが呼び出されると、システムはアクションを開始し、その識別子を "Long-Running-Action" というJAX-RSヘッダーとして利用可能にします。メソッドのボディが何らかのJAX-RSの呼び出しを実行すると、このヘッダーは自動的に送信リクエストに追加されます。このようにして、対象となるサービスは、( @LRA )アノテーションが付与されていれば、対話に参加することができます。

このようにアノテーションされたメソッドが行った作業は、あるサービスがLRAを「キャンセル」した場合に、そのサービスが行った作業を補償すべきであると確実に通知されるという意味で、「補償可能」でなければなりません。各サービスは、「補償する」という概念を解釈する責任があります。唯一の要件は、補償すべきだと通知されたときに 適切 に対応することです。サービスは、メソッドの1つに @Compensate アノテーションを付けることで、どのように通知されるべきかを示します。LRAの結果を制御する方法の詳細については、 @LRA アノテーションのjavadocを参照してください。

このエクステンションは、 MicroProfile LRA仕様とその関連 アノテーションの実装を提供します。

LRAコーディネーター

The narayana-lra extension requires the presence of a running coordinator in the environment. Coordinators can be obtained from https://quay.io/repository/jbosstm/lra-coordinator or you can build your own coordinator using a maven pom that includes the appropriate dependencies. For the purpose of this blog we’ll show how to create one from scratch using the quarkus-maven-plugin. There is some extra information about configuring the coordinator in one of the narayana blogs.

コーディネーターは単なるJAX-RSリソースなので、Quarkusを使って、 resteasy-jacksonrest-client のエクステンションを追加して構築することができます。

$ mvn io.quarkus:quarkus-maven-plugin:2.2.1.Final:create \
      -DprojectGroupId=org.acme \
      -DprojectArtifactId=narayana-lra-coordinator \
      -Dextensions="resteasy-jackson,rest-client"
$ cd narayana-lra-coordinator/
$ rm -rf src

生成されたsrcディレクトリを削除したのは、コーディネーターを動かすためにquarkusフレームワークが必要なだけだからです。

pom.xmlファイルを更新して、narayanaコーディネーターの実装への依存関係を追加します。

    <dependency>
      <groupId>org.jboss.narayana.rts</groupId>
      <artifactId>lra-coordinator-jar</artifactId>
      <version>5.12.0.Final</version>
    </dependency>

次にそれをビルドして、バックグラウンドで実行します。

$ ./mvnw clean package
$ java -Dquarkus.http.port=50000 -jar target/quarkus-app/quarkus-run.jar &

ここでは、 narayana-lra quarkus extensionが使用するデフォルトのポート、すなわち 50000 でコーディネーターを実行しています。コーディネーターが正常に動作していることは、現在のLRAをリストアップすることで確認できます。

$ curl http://localhost:50000/lra-coordinator
[]

このスニペットは、リクエストが空のjson配列を返すことを示しています。

ここでは、LRAを使用するサービスを開発・テストする間、コーディネーターを(デフォルトのポートで)稼働させておきます。記事の最後では、コーディネーターをサービスに組み込む方法を紹介します(注意:この方法では、コーディネーターをネイティブ・モードで実行することはできませんが、将来的にはこの要件をサポートするエクステンションが提供される予定です)。

JVMモード

LRAをサポートするJAX-RSアプリケーションをビルドするには、アプリケーションのpomに依存関係 io.quarkus:quarkus-narayana-lra を追加します。これにより、JAX-RSサポートが追加され、サービスを開発する際にLRAアノテーションを利用できるようになります。また、LRAへのサービスの参加を自動的に管理するJAX-RSフィルタも登録されます。

上述のように、 LRA仕様が要求する(最終的な一貫性の)保証は、LRAに参加するサービスを調整するJAX-RSアプリケーションの存在に依存しています。このコンポーネントは、インタラクションの開始時、インタラクションへの参加時、およびインタラクションの終了時に存在しなければなりません。コーディネーターが利用できなくなった場合は、再起動する必要があります。同様に、LRAに参加しているサービスは、終了段階でも利用可能でなければなりません。システムは、サービスがLRAを終了したことを示すまで再試行を続け、サービスが補償(または完了)に成功したことを示すと、そのサービスはもはや相互作用に参加しません(ただし、すべてのサービスの補償または完了が終了したことを示す信頼性の高い通知に登録することは可能です)。コーディネーターは多数存在しますが、本稿執筆時点では、特定のLRAを管理できるのは1人だけです。

ステップ1:アプリケーションの作成

$ mvn io.quarkus:quarkus-maven-plugin:2.2.1.Final:create \
      -DprojectGroupId=org.acme \
      -DprojectArtifactId=narayana-lra-quickstart \
      -Dextensions="narayana-lra"
$ cd lra-quickstart

なお、コーディネーターがデフォルトとは異なるポート(例: 50000 )で動作している場合は、アプリケーションの設定ファイル( src/main/resources/application.properties )を更新し、ホストとポートを指定する必要があります。

quarkus.lra.coordinator-url=http://localhost:<port>/lra-coordinator

これらの変更後に、生成されたアプリケーションが動作することを確認します。

$ ./mvnw clean package

ステップ2:LRAサポートの追加

ここで、生成されたアプリケーションを更新し、HelloメソッドがLong Running Actionのコンテキストで実行されるようにします。

ファイル src/main/java/org/acme/GreetingResource.java をエディタで開き、 hello メソッドに @LRA アノテーションを付けます(また、JAX-RS javax.ws.rs.HeaderParam アノテーションを使用して、LRA コンテキストをメソッドに注入します)。さらに、LRAが閉じられたときやキャンセルされたときに呼び出される2つのコールバックメソッドを追加します。

インポートも含めて最終的には以下のようになるはずです。

package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

// Step 2a: Add imports for reading the LRA context and for using LRA annotations
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.core.Response;
import java.net.URI;
import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
import org.eclipse.microprofile.lra.annotation.Compensate;
import org.eclipse.microprofile.lra.annotation.Complete;
import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER;

@Path("/hello")
public class GreetingResource {

    @GET
    @LRA // Step 2b: The method should run within an LRA
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId /* Step 2c the context is useful for associating compensation logic */) {
        System.out.printf("hello with context %s%n", lraId);
        return "Hello RESTEasy";
    }

    // Step 2d: There must be a method to compensate for the action if it's cancelled
    @PUT
    @Path("compensate")
    @Compensate
    public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
        System.out.printf("compensating %s%n", lraId);
        return Response.ok(lraId.toASCIIString()).build();
    }

    // Step 2e: An optional callback notifying that the LRA is closing
    @PUT
    @Path("complete")
    @Complete
    public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
        System.out.printf("completing %s%n", lraId);
        return Response.ok(lraId.toASCIIString()).build();
    }
}

これらの変更により、アプリケーションをビルドしてから hello メソッドを呼び出すと、LRAはメソッドに入る前に開始され、メソッドが終了した後に終了します。

$ ./mvnw clean package
$ java -jar target/quarkus-app/quarkus-run.jar &
[1] 2389948
$ curl http://localhost:8080/hello
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_8c1f_612a6e9b_a
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_8c1f_612a6e9b_a
Hello RESTEasy

コーディネーターが動作していることを確認してください。そうでない場合は、以下のようなエラーメッセージが表示されます。

2021-08-11 14:27:45,779 WARN  [io.nar.lra] (executor-thread-0) LRA025025: Unable to process LRA annotations: -3: StartFailed (start LRA client request timed out, try again later) ()'

@Complete のコールバックが呼び出されたことを示す System.out のメッセージに注目してください。ここで、次のステップに備えてjavaプロセスをkillします(プロセスIDがコンソールに表示され、私の例ではpidが2389948なので、 kill 2389948 と入力しました)。

ステップ3:LRAを2つのサービス方式に拡張

このステップでは LRAアノテーションのエンド要素 を使用することで、LRAを開始しますが、メソッドが終了してもLRAを終了しません。

そのために、二番目のビジネスメソッドを次のように定義します:

    @GET
    @Path("/start")
    @LRA(end = false) // Step 3a: The method should run within an LRA
    @Produces(MediaType.TEXT_PLAIN)
    public String start(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
        System.out.printf("hello with context %s%n", lraId);
        return lraId.toASCIIString();
    }

helloメソッドとの唯一の違いは、 @Path@LRA のアノテーションがあることと、LRA IDを呼び出し元に返すことです。これは、LRAを終了させるためにhelloメソッドにリクエストを送信する際に、ヘッダーを設定するために必要となります(このヘッダーは、JAX-RSのレスポンスヘッダーの1つでも利用可能であることに注意してください)。

既存のインスタンスをkillし( kill 2389948)、アプリケーションを再ビルドし( ./mvnw package -DskipTests)、バックグラウンドで実行を開始します。

$ java -jar target/quarkus-app/quarkus-run.jar &
[1] 2495275

curl を使ってLRAを開始し、今回追加した新しいメソッドにリクエストを送信します。

$ LRA_URL=$(curl http://localhost:8080/hello/start)
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a909_611a92ea_2

startメソッドはLRAのIDを返すようにコード化されており、 bash を使ってそれを LRA_URL という環境変数に保存しています。オリジナルのhelloメソッドは、 @LRA アノテーションの end 要素のデフォルト値を使用しているため、LRAのコンテキストでこのメソッドを呼び出すと、メソッドの終了時にLRAが自動的にクローズされます。

$ curl --header "Long-Running-Action: $LRA_URL" http://localhost:8080/hello
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a909_611a92ea_2
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a909_611a92ea_2
Hello RESTEasy

completeWork メソッドが呼び出されたことに注目してください。

ステップ4:あるマイクロサービスでLRAを開始し、別のマイクロサービスで終了する

このステップでは、2つの異なるマイクロサービスが結合していないにもかかわらず、それぞれのアクティビティを調整する方法を紹介します。helloアプリケーションの2つ目のインスタンスを別のポートで起動します。

$ java -Dquarkus.http.port=8081 -jar target/quarkus-app/quarkus-run.jar &
[2] 2495369

同じアプリケーションリソースファイルと外部コーディネーターを使用しているので、設定を更新する必要はありません。

再度、 curl を使用して LRA を起動し、最初のサービスの start メソッドにリクエストを送信します。

$ LRA_URL=$(curl http://localhost:8080/hello/start)
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11

これを、2つ目のサービス(ポート8081で動作しているもの)で終了させます。

$ curl --header "Long-Running-Action: $LRA_URL" http://localhost:8081/hello
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_11
Hello RESTEasy

両方のマイクロサービスが、完了コールバックを受け取ったことを示していることに注目してください。

両方のJavaプロセスを終了してください( kill 2495275 2495369)。

オプショナルステップ:MANDATORY要素の使用

LRAを閉じるために既存のメソッドを使うのではなく、コンテキストがあることを前提としたメソッドを書きたいと思うかもしれません。この場合、 LRA.Type 要素を設定することになります。

    @GET
    @Path("/end")
    @LRA(value = LRA.Type.MANDATORY) // Step 3a: The method MUST be invoked with an LRA
    @Produces(MediaType.TEXT_PLAIN)
    public String end(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) {
        return lraId.toASCIIString();
    }

end メソッドは @LRA(value = LRA.Type.MANDATORY) でアノテーションされているため,コンテクストヘッダが存在していなければならず,そうでなければメソッドはエラー応答コードを返すことになります。

$ ./mvnw clean package -DskipTests
$ java -Dquarkus.http.port=8081 -jar target/quarkus-app/quarkus-run.jar &
[1] 300189
$ LRA_URL=$(curl http://localhost:8081/hello/start)
$ curl -v http://localhost:8081/hello/end
...
HTTP/1.1 412 Precondition Failed
...

一方、LRAのコンテキストヘッダを提供するとうまくいきます。

$ curl --header "Long-Running-Action: $LRA_URL" -I http://localhost:8081/hello/end
HTTP/1.1 200 OK
Content-Type: application/octet-stream
connection: keep-alive
$ kill 300189

アプリケーションの目的に応じて、他の LRA.Type の値も有効です。JTAに慣れた読者は、LRA.Typeは Java Transactional TxType アノテーションをおおまかにモデルにして開発されたことを知っておくと良いでしょう。

ネイティブモード

ネイティブモードでは、外部のコーディネーター(アプリケーションに組み込まれていないもの)しかサポートされていません(この欠点を解決するために、後のリリースでコーディネーターのエクステンションを提供する予定です)。

まず、ネイティブ実行可能ファイルをビルドします。

$ ./mvnw package -DskipTests -Pnative

コーディネーターのセクション で起動した外部コーディネーターがポート50000で動作していることを確認し、バックグラウンドでネイティブ実行可能ファイルとしてサービスを開始します。コーディネーターがデフォルト・ポートで実行されていない場合は、実行中のコーディネーターの場所をJavaシステム・プロパティ( -Dquarkus.lra.coordinator-url=http://localhost:50000/lra-coordinator)として渡すか、アプリケーション・コンフィグを更新してネイティブ実行可能ファイルを再ビルドする必要があることに注意してください。

ネイティブサービスのインスタンスを起動します。

$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner &
[1] 2434426

後でプロセスをkillする必要があるので、プロセスID(例:2434426)をメモしておきます。

新しいLRAを開始します。

$ LRA_URL=$(curl http://localhost:8080/hello/start)

そして別の方法で終わらせます。

$ curl --header "Long-Running-Action: $LRA_URL" http://localhost:8080/hello
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_8479_612e13fa_2
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_8479_612e13fa_2
Hello RESTEasy

次のステップに備えてサービスを停止するか( kill 2434426 )、そのまま稼働させておきます。

エラーハンドリング

このステップでは、あるサービスでLRAの実行を開始し、LRAが終了する前にサービスを終了させます。次に、2つ目のサービスを使用してLRAを終了させます。2つ目のサービスは完了しますが、最初に失敗したサービスの参加者がまだ完了する必要があるため、LRAはまだ Closing の状態にあります。LRAが Closed の状態になるには、失敗したサービスを再起動して、 Complete の要求に応答できるようにする必要があります。

fistサービスをデフォルトのポート8080で再起動します(そしてプロセスIDもメモしておきます)。

$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner &
[1] 2434936

そして2つ目のサービスインスタンス(ポート8082)を起動します。

$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner -Dquarkus.http.port=8082 &
[2] 2434984

最初のサービスでLRAを開始します。

$ LRA_URL=$(curl http://localhost:8080/hello/start)
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34

最初のサービスをkill

$ kill 2434936
2021-08-11 16:02:24,542 INFO  [io.quarkus] (Shutdown thread) narayana-lra-quickstart stopped in 0.003s

ここで、2つ目のサービスだけが稼働している状態で、LRAを終了させてみます。

$ curl --header "Long-Running-Action: $LRA_URL" http://localhost:8082/hello
hello with context http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
Hello RESTEasy

コーディネーターに問い合わせれば、LRAはまだ動いていることが確認できます (curl http://localhost:50000/lra-coordinator)。

LRAを終了するために、失敗したサービス(ポート8080で待ち受けていた)を再起動します。

$ ./target/narayana-lra-quickstart-1.0.0-SNAPSHOT-runner &
[3] 2435130

リカバリー処理は定期的に行われます(リカバリーパスのデフォルトの期間は2分です)。2分も待てない場合は、コーディネーターのリカバリーエンドポイントを使って、以下のように手動でリカバリーサイクルを開始することができます。

$ curl http://localhost:50000/lra-coordinator/recovery
completing http://localhost:50000/lra-coordinator/0_ffffc0a8000e_a355_6113dede_34
[]

ここで注意すべき点は、再起動したサービスが完了通知を受け取ったことです( completing …​)。復旧サイクルの実行要求の結果は、復旧したLRAのjson配列です(この例では、最後のLRAが終了したため、リストは空のjson配列 [] )。

2つのサービスを停止してクリーンアップします( kill 2434984 2435130)。

付録1

埋め込みコーディネーター

コーディネーターは単なるJAX-RSアプリケーションなので、アプリケーションのpom.xmlファイルにLRAコーディネーターの依存関係を追加することで、JAX-RSサービスに簡単に組み込むことができます。

    <dependency>
      <groupId>org.jboss.narayana.rts</groupId>
      <artifactId>lra-coordinator-jar</artifactId>
      <version>5.12.0.Final</version>
    </dependency>

また、デフォルトでは、Quarkusは1つのデプロイメントにつき1つのアプリケーションしか許可しないため、アプリケーション設定ファイル( src/main/resources/application.properties)に以下のプロパティを追加する必要があります。

quarkus.resteasy.ignore-application-classes=true

コーディネーターのセクションで説明した注意点と同じです。

  • ネイティブ実行可能ファイルには対応していません。

  • 各インスタンスには、コーディネーターのトランザクションログ用の専用ストレージが必要です(現在、トランザクションストアの共有はサポートされていないため)。

埋め込まれたコーディネーターは、アプリケーションと同じポート(パス lra-coordinator )で利用できますが、デフォルトのコーディネーターのポートは 50000 なので、アプリケーションのコンフィグでその場所を設定して、アプリケーションに使用させる必要があることに注意してください。

quarkus.http.port=8080
quarkus.lra.coordinator-url=http://localhost:8080/lra-coordinator

トランザクションログの場所はこの方法では設定できないため、システムプロパティ( ObjectStoreEnvironmentBean.objectStoreDir)で設定する必要があります。

$ java -DObjectStoreEnvironmentBean.objectStoreDir=target/lra-logs -jar target/quarkus-app/quarkus-run.jar &
[1] 2443349
$ LRA_URL=$(curl http://localhost:8080/hello/start)
02021-08-11 17:42:30,464 INFO  [com.arj.ats.arjuna] (executor-thread-1) ARJUNA012170: TransactionStatusManager started on port 35827 and host 127.0.0.1 with service com.arjuna.ats.arjuna.recovery.ActionStatusService
hello with context http://localhost:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2
$ curl http://localhost:8080/lra-coordinator
[{"lraId":"http://localhost:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2","clientId":"org.acme.GreetingResource#start","status":"Active","startTime":1628700150466,"finishTime":0,"httpStatus":204,"topLevel":true,"recovering":false}]

今度は別の方法でLRAを終了します。

$ curl --header "Long-Running-Action: $LRA_URL" http://localhost:8080/hello
hello with context http://localhost:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2
completing http://localhost:8080/lra-coordinator/0_ffffc0a8000e_a985_6113fdf6_2
Hello RESTEasy

今後、エクステンションでは、埋め込み式のコーディネーターのサポートが強化される予定です(標準的なQuarkusのメカニズムを使った設定を含む)。