長時間実行アクティビティのためのエクステンション
はじめに
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-jackson
と rest-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のメカニズムを使った設定を含む)。