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

Dev Services のより優れた作成方法

Quarkus 3.25 では、 Dev Services を作成するための新しい API が導入されました。この新しいモデルは、すべてのテストのためのすべての Dev Services が JUnit のディスカバリーフェーズで開始され、ポートの競合、設定の混信、および過剰なリソース消費を引き起こす可能性があるという問題を修正します。この問題は、Quarkus 3.22 における テストクラスローディングの書き換え の副作用でした。リソース消費の削減に加え、この API がエクステンション開発者にとって Dev Services の作成をよりシンプルにし、ディスカバリーの管理やコンテナーの再利用といった重い処理の一部を Quarkus コアに移行させることも期待しています。

ユーザーにとっての変更点

ユーザー側で必要なアクションはありません。

複数のプロファイルやテストリソースを使用するテストスイートがある場合、重複するコンテナーが同時にアクティブになることはなくなります。コンテナーは順番に起動するようになります。

ただし、これは各エクステンションを新しいモデルに変換する必要があるため、エクステンションに依存します。これまでに Redis、Lambda、Narayana、Kafka のエクステンションが変換されました。変換の進捗状況は、 #45785 のサブイシューで確認できます。回避策として、依存しているエクステンションがまだ変換されていない場合は、競合するテストを別々のプロジェクトに分割することで症状が解消されるはずです。

いつものように、問題や奇妙な点に気づいた場合は、 zulip で知らせるか、 イシューを起票 してください。

背景

旧 API を使用するすべての Dev Services は、(Quarkus 3.22 以降) JUnit のディスカバリーフェーズで開始されます。これは、それらがバイトコード操作や他のアプリケーション初期化ステップとともに、 オーグメンテーションフェーズ 中に開始されるためです。テストの設計が変更された際、すべてのオーグメンテーションはテスト実行の開始時、つまり JUnit のディスカバリーフェーズ中に行われるようになりました。これは、すべての Dev Services もテスト実行の開始時に開始されることを意味します。異なる Dev Services 設定を持つ複数のテストクラスがテスト実行前にオーグメンテーションされると、設定の異なる複数の Dev Services が同時に実行される可能性があります。

新しいモデルでは、Dev Services はオーグメンテーションの後、アプリケーションの実際の起動の前に開始されます。

エクステンション所有者にとっての変更点

新しい Dev Services モデルは古いモデルとの後方互換性を維持しているため、エクステンション所有者が何かを行う 必要 はありません。実際、新しいモデルの最初の数リリースでは、API が安定するまでエクステンション所有者は何もしないことを推奨していました。

現在は、より簡潔なプログラミングモデルとリソース使用量の削減という利点を享受するために、エクステンションの移行を開始するのに良い時期です。これにより、古いモデルによって引き起こされるいくつかの非推奨警告も解決されます。新しい API に移行したエクステンションは、古いバージョンの Quarkus では動作しなくなることに注意してください。3.25 が可能な最小バージョンとなりますが、最小バージョンとして 3.27 または 3.28 を設定することをお勧めします (詳細は後述)。

新設計の原則

  • Dev Services はビルド時に準備されますが、実際の start() 呼び出しはビルド後、ランタイム前に行われます。

  • エクステンションプロセッサーで静的変数を使用しないこと。

  • Dev Service の作成はビルダーによって処理されます (外部管理インスタンスへの接続と新しいサービスの作成には、それぞれ異なるビルダーがあります)。

  • サービスの開始後にのみ判明する設定は、 configProvider() を使用して渡すことができます。

移行チェックリスト

  • エクステンションのビルドファイルを更新し、 quarkus-devservices ランタイムモジュールと quarkus-devservices-deployment モジュールの両方に依存するようにします (ただし、これによる影響については Quarkus バージョンの選択に関する議論 を参照してください)。

  • 新しいサービスの開始と停止のメソッドを持つ io.quarkus.deployment.builditem.Startable実装 を提供します。コンテナーベースのサービスの場合、 GenericContainer を継承して Startable を実装するのが良いパターンです。

  • DevServicesResultBuildItem を直接構築する代わりに、 DevServicesResultBuildItem 上の discovered() および owned() ビルダー を使用するように切り替えます。

    • ビルダーのすべてのメソッドを呼び出す必要はありませんが、所有(owned)サービスの場合は startable()serviceName()configProvider() 、および serviceConfig() がほぼ常に必要になります。

  • アンチパターンのコードチェック。

    • エクステンションのコードで Dev Service を停止または開始してはいけません。

    • シャットダウンリスナーを削除します。クリーンアップはサービスの Startablestop() メソッドで処理する必要があります。

    • 新しいモデルでは RunningDevService 型を使用してはいけません。

    • エクステンションプロセッサー内の 静的変数 (サービスインスタンスへのポインターなど) を削除します。

    • システムプロパティーやその他のオーバーライドを使用して、新しいサービスにアクセスするための設定を直接設定しようとしないでください。代わりに configProvider() を使用してください。

移行の詳細

エクステンションプロセッサーの静的フィールドの排除

エクステンション開発者は、インスタンス間の通信を静的変数に依存すべきではありません。プロセッサーの呼び出し順序がアプリケーションの実行順序と同じになると想定してはいけません。

エクステンション作成ガイド には、「ビルドステップ間の状態の受け渡しは、たとえステップが同じクラス内にあっても、ビルドアイテムを介してのみ行うべきである」と記載されています。しかし、ほぼすべての Dev Service 実装がこのルールを破り、以前に作成されたサービスを追跡するために静的フィールドを使用していました。

新しいモデルに移行する際の優れたヒューリスティックは、すべての静的フィールドをなくすことです。たとえば、次のようなフィールドをすべて削除します。

private static volatile RunningDevService devService;
private static volatile MyDevServicesConfig capturedDevServicesConfiguration;
private static volatile boolean first = true;

サービスの再利用か交換かの判断は、設定の差分に基づいて 一元的に処理 されるようになりました。

シャットダウンロジックの排除

サービスのライフサイクルが一元的に管理されるため、シャットダウンリスナーやサービスを停止するためのその他のロジックも削除する必要があります。

RunningDevService への参照の削除

プロセッサーはサービスの開始を処理しないため、 RunningDevService を返してはいけません。

ビルダーの使用

直接構築する代わりに、新しいビルダー API を使用してください。作成されるサービスには owned() を、検出された外部管理サービスを登録するには discovered() を選択します。

例:

    DevServicesResultBuildItem = DevServicesResultBuildItem.owned()
                    .feature(MY_FEATURE_NAME)
                    .serviceName(name)
                    .serviceConfig(myConfig)
                    .startable(() -> new MyContainer(
                            myImageName,
                            myConfig.port(),
                            useSharedNetwork)
                            .withEnv(myConfig.containerEnv())
                     .configProvider(
                            Map.of(someProp, s -> s.getConnectionInfo()))
                    .build());

再利用の適格性

一元化されたライフサイクル管理は、サービスが再利用可能かどうかをどのように判断するのでしょうか。これは、比較の基準として使用するためにビルダーに渡される「同一性キー」(設定オブジェクト) に基づいています。

主要なメソッドは .serviceConfig(myConfig) です。現在の設定は、再起動ごとに実行中のサービスの設定とリフレクションによって比較されます。

Startable の役割

遅延開始をサポートするために、 Startable の実装をビルダーに渡します。 (言うまでもないことですが、 Startablestart()呼び出さないで ください。 Quarkus インフラストラクチャーが適切なタイミングでサービスを開始します。) (言うまでもないことですが、 Startablestart()呼び出さないで ください。 Quarkus インフラストラクチャーが適切なタイミングでサービスを開始します。)

コンテナーベースのサービスの場合、通常は GenericContainer を継承するのが便利です。その場合、 start() を実装する必要さえありません。ほとんどの Dev Services 実装はすでに GenericContainer のサブクラスを提供しているため、差分は implements Startable を追加し、 close() メソッドを追加するだけです。 close メソッドはスーパークラスに委譲できます。

例:

private static class MyContainer extends GenericContainer<MyContainer> implements Startable {

        private final OptionalInt fixedExposedPort;

        private final String hostName;

        public MyContainer(String imageName, OptionalInt fixedExposedPort) {
            super(imageName);
            this.fixedExposedPort = fixedExposedPort;

            this.hostName =  ...

        }

        @Override
        protected void configure() {
            super.configure();

            if (fixedExposedPort.isPresent()) {
                addFixedExposedPort(fixedExposedPort.getAsInt(), DEFAULT_PORT);
            } else {
                addExposedPort(DEFAULT_PORT);
            }
        }

        public int getPort() {
            if (fixedExposedPort.isPresent()) {
                return fixedExposedPort.getAsInt();
            }
            return super.getFirstMappedPort();
        }

        // This looks strange, but is needed to satisfy the interface
        @Override
        public void close() {
            super.close();
        }

        @Override
        public String getConnectionInfo() {
            return getHost() + ":" + getPort();
        }

依存関係の変更と最小 Quarkus バージョンの設定

この部分は残念ながら少し厄介です。 Quarkus 3.28 では、新しい devservices ランタイムモジュールが導入されました。ほとんどのエクステンションにはデプロイメントモジュールとランタイムモジュールの両方がありますが、歴史的に Dev Services にはデプロイメントモジュールしかありませんでした。関連するランタイムクラスは他のモジュールに存在していました。ランタイムモジュールは 3.28 で追加されました。破壊的な変更になる可能性があるため、LTS 後に行われました。それは良いアイデアのように思われましたが、予期せぬ結果をもたらしました。新しいモジュールの導入は後方互換性を維持する方法で行われましたが、前方互換性は維持されませんでした。つまり、3.27 に対してビルドされたエクステンションは 3.27 LTS で動作しますが、3.28 以降のバージョンでは動作しません。3.28 でビルドされたエクステンションは 3.27、および 3.28 以降のバージョンで動作します。

このため、エクステンションの 3.27 用と 3.28+ 用のブランチを作成するか、単に 3.28 に対してビルドする必要があります。 3.28 に対してビルドする場合は、Quarkus ツールがそのエクステンションを 3.27 と互換性があると認識できるように、エクステンションのメタデータで最小 Quarkus バージョンを手動で設定する必要があります。

  requires-quarkus-core: "[3.27,)"

3.28 に対してビルドすることを選択した場合は、ランタイムモジュールの pom.xml に以下を追加してください。

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-devservices</artifactId>
</dependency>

FAQ

Dev Service の開始後にビルドステップで何かを実行するにはどうすればよいですか。

Dev Service がビルドフェーズで開始されることはないため、これは不可能です。ただし、ビルダーの postStartHook を使用すると、Dev Service が開始された後にアクションを実行できます。アプリケーションに設定を渡すには、 レコーダーを使用 できます。

一般的に、Quarkus の哲学は作業をアプリケーションの開始時からビルド時に移行することであり、その逆ではありません。作業を postStartHook やレコーダーに移動する必要がある場合は、パフォーマンスへの悪影響を避けるために、移動する量を最小限に抑えるようにしてください。多くの場合、コードは Dev Service が作成される こと を知る必要がありますが、実際のアドレスを知る必要はありません。これは、マーカービルドアイテムを使用した通常のビルドフローの一部として処理し、フックの一部として実行するものを最小限に抑えることができます。

サービスに管理コンソールがある場合、Dev UI でそのリンクを公開するのは良いパターンです。旧 Dev Services モデルでは、次のようなコードでエクステンションのカードをカスタマイズしていたかもしれません。

   @BuildStep(onlyIf = IsDevelopment.class)
   public CardPageBuildItem pages(List<SomeRelevantBuildItem> containers) {
      CardPageBuildItem cardPageBuildItem = new CardPageBuildItem();

      for (SomeRelevantBuildItem container : containers) {
         cardPageBuildItem.addPage(Page.externalPageBuilder("My Extension Name")
               .url(container.getTheUrl())
               .staticLabel(container.label());
      }

      return cardPageBuildItem;
   }

URL はビルド時には不明であるため、これは機能しなくなります。代わりに、 url メソッドを dynamicUrlJsonRPCMethodName に置き換え、 RPC メソッド名を渡します

     .dynamicUrlJsonRPCMethodName("getMyUrl")

3.32 からは、メソッド呼び出しでパラメーターを渡すこともできます。たとえば、次のようになります。

     .dynamicUrlJsonRPCMethodName("getMyUrl", Map.of("name", "service-name", "configKey", "some-key");

提供クラスを登録するためのビルドステップを追加します。

    @BuildStep(onlyIf = IsLocalDevelopment.class)
    public JsonRPCProvidersBuildItem createJsonRPCService() {
     return new JsonRPCProvidersBuildItem(MyJsonRPCService.class, BuiltinScope.SINGLETON.getName());
    }

エクステンションのランタイムモジュールで、 getMyUrl メソッドを持つ MyJsonRPCService クラスを作成します。

差分を見ると、何をすべきかがわかりやすい場合があります。

コンテナーベースではない Dev Service を使用した例については、 Lambda の変換 を参照してください。 compose を使用し、既存の外部コンテナーを再利用し、開始後の設定を行う、より複雑な変換については、 Kafka の変換KafkaDevServicesProcessor 部分のみを参照してください。

Dev Services ライフサイクルのワーキンググループ は現在も進行中であり、貢献を歓迎しています。