Narayana LRA participant support

はじめに

LRA(Long Running Actionの略)参加者エクステンションは、マイクロサービスベースの設計で、異なるサービスが分散一貫性の緩和された概念を利用できる場合に便利です。

このアイデアは、複数のサービスが協調して異なる計算/アクションを実行する一方で、計算中に実行されたアクションを補正するオプションを保持するというものです。このようなサービスの緩やかな結合は、JTA/XAのような強力な一貫性モデルと、「自家製の」アドホックな一貫性ソリューションとの間のギャップを埋めるものです。

このモデルは、 Eclipse MicroProfile LRA仕様に基づいています。 このアプローチは、開発者がビジネスメソッドにJavaアノテーション(https://download.eclipse.org/microprofile/microprofile-lra-1.0/apidocs/[@LRA])を付けるアプローチです。 このようなメソッドが呼び出されると、(まだ存在しない場合)LRA コンテキストが作成され、LRA を閉じるかキャンセルすることを示す属性を持つ @LRA アノテーションを含むメソッドに到達するまで、その後の Jakarta REST 呼び出しと一緒に渡されます。デフォルトは、LRA を開始したのと同じメソッド(メソッド実行中にコンテキストを伝播した可能性がある)で LRA がクローズされることです。 Jakarta RESTリソースは、最低限、 @Compensateアノテーションでメソッドの1つをマークすることで、対話に参加したいことを示します。コンテキストが後でキャンセルされた場合、この @Compensate アクションは、障害があっても呼び出されることが保証され、リソースが LRA のコンテキストで実行した活動を補償するためのトリガーとなります。この保証により、(すべての補償活動が完了したときに)最終的な整合性が保証され、サービスが確実に動作することができます。参加者は、 @Completeアノテーションでメソッドの1つをマークすることによって、参加しているLRAが終了したときに確実に通知されるように求めることができます。このように、LRAをキャンセルすると、Compensateコールバックを通じてすべての参加者に通知され、LRAを閉じると、Completeコールバックを通じてすべての参加者に通知されます(ある場合)。 参加者をコントロールするためのその他のアノテーションについては、 MicroProfile LRA API v1.0 javadocに記載されています。

設定

Quarkus Mavenプロジェクトの設定が完了したら、プロジェクトのベースディレクトリで以下のコマンドを実行して、 narayana-lra エクステンションを追加できます。

CLI
quarkus extension add narayana-lra,resteasy-jackson,resteasy-client-jackson
Maven
./mvnw quarkus:add-extension -Dextensions='narayana-lra,resteasy-jackson,resteasy-client-jackson'
Gradle
./gradlew addExtension --extensions='narayana-lra,resteasy-jackson,resteasy-client-jackson'

これにより、ビルドファイルに以下が追加されます:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-narayana-lra</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-client-jackson</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-narayana-lra")
implementation("io.quarkus:quarkus-resteasy-jackson")
implementation("io.quarkus:quarkus-resteasy-client-jackson")

quarkus-narayana-lra を動作させるには、サーバーの Jakarta REST 実装と REST Client 実装と共に使用する必要があります。 つまり、 quarkus-resteasy-jackson と quarkus-resteasy-client-jackson 、または quarkus-rest-jackson と quarkus-rest-client-jackson の依存関係も必要です。

コーディネーターがいる場合は、これだけで新しいLRAを作り、参加者を募ることができます。

LRAエクステンションは、 src/main/resources ディレクトリ内の application.properties ファイルを更新することで設定できます。LRA固有のプロパティは、 quarkus.lra.coordinator-url=<url> 、外部コーディネーターのHTTPエンドポイントを指定するだけです。

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

ナラヤナのコーディネーターの場合、URLのパス部分は通常 lra-coordinator です。コーディネーターは https://hub.docker.com/r/jbosstm/lra-coordinator から入手できます。また、適切な依存関係を含むmaven pomを使用して独自のコーディネーターをビルドすることもできます。この方法については、Quarkusのクイックスタートが提供されます。また、 Narayanaのクイックスタートの1つを参照することもできます。もう一つの方法は、WildFlyアプリケーションサーバ内で管理された状態で実行することです。

Securing LRA with JWT

Narayana LRA can forward a JWT bearer token on the Jakarta REST invocations that flow between participants and the coordinator. This is useful when the services taking part in an LRA are protected and require an Authorization: Bearer header.

The feature is opt-in. To propagate the bearer token that arrived on an incoming request, annotate the @LRA method (or its class) with io.narayana.lra.PropagateToken:

@LRA(LRA.Type.REQUIRES_NEW) // a new LRA is created on method entry
@PropagateToken // forward the inbound bearer token on outgoing LRA calls
@Path("/work")
@PUT
public void doInNewLongRunningAction(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId)
{
    ...
}

For the token to be available, your service must authenticate the incoming request with a JWT security extension such as quarkus-smallrye-jwt. Once authenticated, the inbound bearer token is propagated automatically on the outgoing LRA calls; no further configuration is required.

Issuing a service token to authenticate the services themselves is a coordinator-side concern and is not handled by the participant extension.

失敗の処理

LRAが終了するように指示された場合、つまり、 @LRA(end = true, …​) でアノテーションされたメソッドが呼び出された場合、コーディネーターは対話に関わるすべてのサービスに終了するように指示します。サービスが利用できない場合(または終了中の場合)、コーディネーターは定期的に再試行します。失敗したサービスを、LRAに最初に参加したときに使用したのと同じエンドポイントで再起動するか、新しいエンドポイントでの通知を希望することをコーディネーターに伝えるのは、ユーザーの責任です。LRAは、 すべての 参加者が終了したことを確認するまで、終了したとはみなされません。

コーディネーターは、LRAを確実に作成・終了し、参加者の参加を管理する役割を担っているため、利用可能でなければなりません(例えば、コーディネーターやネットワークに障害が発生した場合、環境の何かがコーディネーターの再起動やネットワークの修復を行う責任を持たなければなりません)。このタスクを遂行するために、コーディネーターはログを保存するための耐久性のあるストレージにアクセスできなければなりません(ファイルシステムまたはデータベースを介して)。本稿執筆時点では、コーディネーターの管理はユーザーの責任です。「すぐに使える」ソリューションが今後登場する予定です。

例

以下は、LRAを開始する方法と、後にLRAがキャンセルされたり( @Compensate のアノテーション付きメソッドが呼び出される)、クローズされたり( @Complete が呼び出される)したときに通知を受け取る方法の簡単な例です。

@Path("/")
@ApplicationScoped
public class SimpleLRAParticipant
{
    @LRA(LRA.Type.REQUIRES_NEW) // a new LRA is created on method entry
    @Path("/work")
    @PUT
    public void doInNewLongRunningAction(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId)
    {
        /*
         * Perform business actions in the context of the LRA identified by the
         * value in the injected Jakarta REST header. This LRA was started just before
         * the method was entered (REQUIRES_NEW) and will be closed when the
         * method finishes at which point the completeWork method below will be
         * invoked.
         */
    }

    @org.eclipse.microprofile.lra.annotation.Complete
    @Path("/complete")
    @PUT
    public Response completeWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId,
                                 String userData)
    {
        /*
         * Free up resources allocated in the context of the LRA identified by the
         * value in the injected Jakarta REST header.
         *
         * Since there is no @Status method in this class, completeWork MUST be
         * idempotent and MUST return the status.
         */
         return Response.ok(ParticipantStatus.Completed.name()).build();
    }

    @org.eclipse.microprofile.lra.annotation.Compensate
    @Path("/compensate")
    @PUT
    public Response compensateWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId,
                                   String userData)
    {
        /*
         * The LRA identified by the value in the injected Jakarta REST header was
         * cancelled so the business logic should compensate for any actions
         * that have been performed while running in its context.
         *
         * Since there is no @Status method in this class, compensateWork MUST be
         * idempotent and MUST return the status
         */
         return Response.ok(ParticipantStatus.Compensated.name()).build();
    }
}

また、この例では、LRAが存在する場合、 @HeaderParam Jakarta RESTアノテーションタイプを使用してリクエストヘッダを読み取ることで、その識別子を取得することができることを示しています。

ここでは、 LRA アノテーションの end 要素を使用して、あるリソース・メソッドで LRA を開始し、別のリソース・メソッドで閉じる方法の例を示しています。また、ビジネスメソッドが cancelOn および cancelOnFamily 要素で特定される特定の HTTP ステータスコードを返した場合に、LRA が自動的にキャンセルされるように設定する方法も示しています。

  @LRA(value = LRA.Type.REQUIRED, // if there is no incoming context a new one is created
       cancelOn = {
           Response.Status.INTERNAL_SERVER_ERROR // cancel on a 500 code
       },
       cancelOnFamily = {
           Response.Status.Family.CLIENT_ERROR // cancel on any 4xx code
       },
       end = false) // the LRA will continue to run when the method finishes
  @Path("/book")
  @POST
  public Response bookTrip(...) { ... }

  @LRA(value = LRA.Type.MANDATORY, // requires an active context before method can be executed
       end = true) // end the LRA started by the bookTrip method
  @Path("/confirm")
  @PUT
  public Booking confirmTrip(Booking booking) throws BookingException { ... }

bookTripメソッドの end = false 要素は、メソッドの終了時にLRAの実行を継続させ、confirmTripメソッドの end = true 要素は、メソッドの終了時にLRA(bookTripメソッドで開始)を終了させます。この終了要素は、Jakarta RESTリソースに置くことができることに注意してください(つまり、あるサービスがLRAを開始し、別のサービスがそれを終了することもできます)。 Microprofile LRA仕様 とMicroprofile LRA TCK には、この他にも多くの例が掲載されています。