月別アーカイブ: 2019年6月

Azure Service Bus の Queue/Topic にデータを出し入れする

このエントリは2019/06/28現在の情報に基づきます。今後の機能追加や廃止に伴い、記載内容との齟齬が発生する可能性があります。

Azure Service Busとは

Microsoft Azure Service Bus は、フル マネージド エンタープライズ統合メッセージ ブローカーです。 Service Bus の最も一般的な用途は、アプリとサービスを相互に分離する場合です。Service Bus は非同期データと状態転送に適した信頼性の高い安全なプラットフォームです。 データは、メッセージを使用してさまざまなアプリとサービス間で転送されます。 メッセージはバイナリ形式であり、JSON、XML、または単なるテキストを含むことができます。

Azure Service Bus とは
https://docs.microsoft.com/ja-jp/azure/service-bus-messaging/service-bus-messaging-overview

上記の通り、メッセージブローカーで、オンプレミスでいうところのWebSphere MQやRabbit MQ、JMS実装などに類するもの。名前だけだと、SOAのService Busと勘違いするが、そこは違うので要注意。

他サービスが提供するQueue/Topicとの違い

詳細は以下のドキュメントを参照。

Storage キューと Service Bus キューの比較
https://docs.microsoft.com/ja-jp/azure/service-bus-messaging/service-bus-azure-and-service-bus-queues-compared-contrasted
Storage queues and Service Bus queues – compared and contrasted
https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-azure-and-service-bus-queues-compared-contrasted

Azure メッセージング サービスの中から選択する – Azure Event Grid、Event Hubs、および Service Bus
https://docs.microsoft.com/ja-jp/azure/event-grid/compare-messaging-services
Choose between Azure messaging services – Event Grid, Event Hubs, and Service Bus
https://docs.microsoft.com/ja-jp/azure/event-grid/compare-messaging-services

もしくは、以下のエントリを参照。

Queue/Topicが使えるサービス
https://logico-jp.io/2019/06/11/how-to-choose-queue-or-topic-services/

データの出し入れ

データを入れる場合

サポートされている Service Bus API クライアントを使用した Service Bus への送信操作では、通常、明示的に解決されます。API 操作は、Service Bus からの受信結果を受け取るまで待機してから送信操作を完了します。
Service Bus によってメッセージが拒否された場合、拒否には、”追跡 ID” が含まれたテキストとエラーのインジケーターが含まれます。 拒否には、操作が成功する可能性があり、操作を再試行するかどうかに関する情報も含まれています。 この情報はクライアント内で例外に変換され、送信操作の呼び出し元に報告されます。 メッセージが受け取られた場合、操作は自動的に完了します。

メッセージの転送、ロック、および解決 – 送信操作の解決
https://docs.microsoft.com/ja-jp/azure/service-bus-messaging/message-transfers-locks-settlement#settling-send-operations

ドキュメント上、送信して結果を受け取るところまで待機する方式で実装することが推奨されている。Fire-forget形式を選択することも可能ではあるが、データのロストが発生する可能性があることは認識しておく必要がある。

データを出す場合

PeekLock

受信クライアントが、受信したメッセージの明示的な解決を求めることを、ブローカーに指示します。 受信クライアントが処理できるよう、メッセージに排他的なロックがかけられ、他の競合受信クライアントからは認識できなくなります。 ロックの有効期間は、当初キューまたはサブスクリプション レベルで定義され、ロックを所有しているクライアントによる RenewLock 操作によって延長できます。

メッセージの転送、ロック、および解決 – 受信操作の解決
https://docs.microsoft.com/ja-jp/azure/service-bus-messaging/message-transfers-locks-settlement#settling-receive-operations

MQやJMSを知っている人になじみが深い挙動で、メッセージの取り扱いはクライアントに委ねられているため、明示的に完了を発行しなければメッセージはブローカーに滞留する(最大試行回数を超えると、Dead Letter Queueに入る)。

ReceiveAndDelete(受信して削除)

ブローカーは、ブローカーが受信クライアントに送信するすべてのメッセージを、送信時点で解決済みとみなします。 つまり、ブローカーが送信するとただちにメッセージは消費されたとみなされます。 メッセージの転送が失敗した場合、メッセージは失われます。
このモードのメリットは、受信側メッセージでそれ以上のアクションを実行する必要はなく、解決の結果を待機する遅延も発生しないことです。 各メッセージに含まれるデータの価値が低い、またはデータが意味を持つ時間が非常に短時間の場合は、このモードは妥当な選択です。

メッセージの転送、ロック、および解決 – 受信操作の解決
https://docs.microsoft.com/ja-jp/azure/service-bus-messaging/message-transfers-locks-settlement#settling-receive-operations

「受け取ったらブローカー側には責任がない」ので、クライアント側で障害があった場合や受け取りに失敗した場合にメッセージをロストする可能性がある。データを入れる際のFire-forget形式に近い。データロストを避けたいのであれば、この方式は使うべきではない。

動作を確認する

今回はLogic Appsを使って確認した。なお、Topicでも同様の挙動を示すため、Queueについてのみ紹介する。

PeekLock

Queueに入ったメッセージをPeekLockモードで受信し、Queue内のメッセージを完了せずに強制的にインスタンスを終了させるようなロジックアプリを作成した。Queueにメッセージを入れてこのロジックアプリが動作すると、Queueからはメッセージは削除されず、Retryが続いて最終的にDead Letter Queueにメッセージが入る。

続いて、Queue内のメッセージを完了させるよう「キュー内のメッセージを完了する」アクションを通すと、メッセージはQueueから消去される。

QueueのメッセージをDead Letter Queueに入れるように「キューのメッセージを配信不能にする」アクションを使うと、このタイミングでメッセージはDead Letter Queueに入り、ロックは解除される。そのため、このアクションの後に 「キュー内のメッセージを完了する」アクションを配置すると、すでにロックは存在しないので例外が発生する。

ReceiveAndDelete

Logic AppsではこのモードはAuto Completeと呼ばれる。

PeekLockモードと同様に、Queueに入ったメッセージをReceiveAndDeleteモードで受信し、強制的にインスタンスを終了させるようなロジックアプリを作成した。Queueにメッセージを入れてこのロジックアプリが動作すると、Queueからメッセージは削除される。これはメッセージがQueueから取り出された時点でブローカーはメッセージを削除しているためである。

続いて、「キュー内のメッセージを完了する」アクションを通すようにすると、元々ロックされていないメッセージに対してロックの解放を指示しているので、例外が発生する。

Dead Letter Queueに入れるよう「配信不能にする」アクションを指定すると、これも「キュー内のメッセージを完了する」場合と同様、ロック対象ではないものに対してロックリリースをしようとして失敗する。

まとめ

Service BusのQueue/Topicについて受信時の挙動を確認した。今回はロジックアプリで確認したが、Javaでクライアントを記述する場合、PeekLockモードで受信する場合にはQueueClient#completeをお忘れなく。

WSL2 と Windows Terminal

このエントリは2019/06/26現在の情報を基にしています。今回取り上げている両者はともにまだGA版ではないため、将来の機能追加により、記載内容との齟齬が発生する可能性が大いにあります。

Windows TerminalとWSL2は何れもPreviewではあるが利用できる。以下の内容はWindows Terminal 0.2.1715.0をベースにしている。

Windows TerminalのSettingsをクリックすると、2019/06/26現在、profiles.jsonファイルがエディターで開く。このprofiles.jsonは以下の場所に存在する。

%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\RoamingState\profiles.json

Windows Terminalのメニューの並びを変更する

環境によって異なるが、自環境ではWindows Terminalをインストールした直後はPowerShell Core、Windows PowerShell、cmd、ubuntuの順で並んでいた。この並びは、profiles.json内のprofiles要素内の順番に一致するため、ubuntuを先頭に配置したい場合には、ubuntuの部分を配列の先頭に移動する。

デフォルトのTerminalを変更する

自環境の場合、デフォルトでPowerShell Coreが起動するようになっていたが、これをubuntuにする場合、globals/defaultProfileにubuntuのUUIDを設定する。以下はその設定例。この例では、ubuntuのGUIDが22222222-3838-5115-0940-1234567890abである場合、defaultProfileに22222222-3838-5115-0940-1234567890abを指定している。

{
    "globals" : 
    {
        "alwaysShowTabs" : true,
        "defaultProfile" : "{22222222-3838-5115-0940-1234567890ab}",
        ...
    },
    "profiles" : 
    [
        {....},
        {
            ...,
            "name" : "Ubuntu",
            "guid" : "{22222222-3838-5115-0940-1234567890ab}",
            ...
        },
        { ... }
    ],
    "schemes" : 
    [...]
}

Windows Terminalを開いたときにWSL2 ubuntuのホームディレクトリを開くようにする

このためには、ubuntuのprofileに startingDirectory を追加し、”//wsl$/Ubuntu/{ホームディレクトリのFull Path}” と設定する。この startingDirectory はPowerShell、cmdにはデフォルトで存在する要素(デフォルト値はいずれも %USERPROFILE%)だが、WSL2 環境のprofileには設定されていない。以下はその設定例。

{
    "globals" : {...},
    "profiles" : 
    [
        {
            ...,
            "name" : "Ubuntu",
            "startingDirectory":"//wsl$/Ubuntu/home/logico",
            ...
        },
        { ... }
    ],
    "schemes" : 
    [ ... ]
}

背景を透過的にする

各profileのuseAcrylicをtrueにした上で、不透明度を表すacrylicOpacityを0から1までの間で設定する。自環境の場合、デフォルトではcmd以外はuseAcrylicがfalseで、acrylicOpacityはcmdは0.75、それ以外は0.5が設定されていた。

WindowsからのWSL2環境のファイルにアクセスする

WSLのubuntu 18.04では以下のパスでrootにアクセスできた。

%LOCALAPPDATA%\Packages\CanonicalGroupLimited.Ubuntu18.04onWindows_79rhkp1fndgsc\LocalState\rootfs

しかし、WSL2のubuntu 18.04はVHDファイルでホストされているため、rootfsというディレクトリが存在しない。

%LOCALAPPDATA%\Packages\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\LocalState\ext4.vhdx

そのため、以下のようにUNCを使ってアクセスするしかない。

¥¥wsl$¥Ubuntu¥{Ubuntuでのパス}

Logic Apps と Cosmos DB

このエントリは2019/06/21現在の情報に基づきます。そのため、将来の機能追加・廃止に伴い、記載内容との乖離が発生する可能性があります。

関連エントリ

Serverlessというコンセプトに則ったサービスであるLogic Appsは、複雑なロジックは向いていないながらも、シンプルなロジックであれば、コードをできるだけ書かずにFunctionsのような動作をさせることができる。今回はCosmos DBに対する操作を確認した。なお、データはAPI Managementのエントリで使っている3レターコードを格納したコレクションを使っている。

コネクタ

Azure Cosmos DBのコネクタが存在するが、アクションとしてのみ利用可能で、トリガーとして利用することはできない。変更フィードをLogic Appで取り扱うためには、現時点では以下のように、Cosmos DBとLogic Appの間mediationするものを挟む必要がある。

  • Functionsで受け取ってmediationするものを作成、もしくは連携する(Event GridやEvent Hub、Service Busなど)
  • 変更フィードプロセッサライブラリを使ってmediationするものを作成、もしくは連携する(ただし.NETのみ)
  • Azure Cosmos DB SQL API SDK を使用してmediationするものを作成、もしくは連携する

クエリ

[1つのドキュメントを取得する]アクションを使う場合、[ドキュメントID]はダブルクォーテーションで囲まない点に注意が必要。対して、[パーティション キーの値]は、必ずダブルクォーテーションで囲む必要がある。

SFOを指定した場合、以下のような結果が返る(一部マスク済み)。当然ながら、この内容はCosmos DBのデータエクスプローラーで確認できる値と同じ(ただし_tsを除く)。

{
    "code": "SFO",
    "id": "SFO",
    "en": "San Francisco, CA",
    "ja": "サンフランシスコ",
    "_rid": "...",
    "_self": "dbs/.../",
    "_etag": "...",
    "_attachments": "attachments/",
    "_ts": 1561101814
}

もし複数件のレコードを取得する可能性がある場合、[複数のドキュメントにクエリを実行する]を使うとクエリを記載できる。

この場合、codeとしてTを指定すると、以下のように複数のレコードが返る。

[
    {
        "code": "CTS",
        "en": "Sapporo (Chitose)",
        "ja": "札幌(千歳)"
    },
    {
        "code": "TTJ",
        "en": "Tottori",
        "ja": "鳥取"
    },
    {
        "code": "AXT",
        "en": "Akita",
        "ja": "秋田"
    },
    {
        "code": "NTQ",
        "en": "Noto",
        "ja": "能登"
    },
    {
        "code": "TOY",
        "en": "Toyama",
        "ja": "富山"
    },
    {
        "code": "TAK",
        "en": "Takamatsu",
        "ja": "高松"
    },
    {
        "code": "OIT",
        "en": "Oita",
        "ja": "大分"
    },
    {
        "code": "TYO",
        "en": "Tokyo (All)",
        "ja": "東京(すべて)"
    },
    {
        "code": "ITM",
        "en": "Osaka (Itami)",
        "ja": "大阪(伊丹)"
    },
    {
        "code": "NRT",
        "en": "Tokyo (Narita)",
        "ja": "東京(成田)"
    },
    {
        "code": "TKS",
        "en": "Tokushima",
        "ja": "徳島"
    },
    {
        "code": "TSJ",
        "en": "Tsushima",
        "ja": "対馬"
    }
]

挿入・更新

挿入・更新時には、Upsertするか否かを指定できる(デフォルトでは出てこないので、[新しいパラメーターの追加]から選択する必要がある)。また、[パーティション キーの値]は、必ずダブルクォーテーションで囲むこと。

削除

これも[1つのドキュメントを取得する]アクションと同様、[ドキュメントID]はダブルクォーテーションで囲まない点に注意が必要。対して、[パーティション キーの値]は、必ずダブルクォーテーションで囲む必要がある。

まとめ

設定フィールドごとにダブルクォーテーションで囲む・囲まないの設定が必要なので、注意が必要である。

Microprofile CustomConfigSource with Database

このエントリは以下のエントリをベースにしています。
This entry is based on the following one written by Ralph Soika (Project lead of Imixs-Workflow).
https://microprofile.io/2019/06/18/microprofile-customconfigsource-with-ejbs/
https://ralph.blog.imixs.com/2019/06/11/microprofile-customconfigsource-with-database/

MicroProfile Config APIでは、アプリケーションの構成プロパティを簡単かつ新しい方法で扱うことができます。

Configuration for MicroProfile
https://microprofile.io/project/eclipse/microprofile-config

このAPIを使うと、以下のような異なるソースの構成やプロパティの値にアクセスできます。

  • System.getProperties() (ordinal=400)
  • System.getenv() (ordinal=300)
  • すべての META-INF/microprofile-config.properties ファイル

MicroProfile Config APIの手始めには以下をどうぞ。

Eclipse MicroProfile Config – what is it?
https://www.eclipse.org/community/eclipse_newsletter/2017/september/article3.php

もちろん、ご自身のConfigSourceも実装できますが、大多数は以下の例のように既存ファイルからのカスタム構成値の読み取りをベースにしています。

Microprofile Config: Creating a Custom ConfigSource
https://rhuanrocha.net/2018/12/21/microprofile-config-creating-a-custom-configsource/

このエントリではデータベースから読み取った値に基づいたMicroProfile ConfigSourceの実装方法をご紹介します。

How To Access a Database?

以下の例は、JPAデータソースもしくはEJBサービスから値を読み取るカスタムのConfigSourceを実装方法を示したものです。一見すると、アプリケーションが提供するカスタム構成ファイルへアクセスするための外部ソースやサービスを注入するのは簡単そうに見えます。

public class MyConfigSource implements ConfigSource {

    @PersistenceContext(unitName = ".....")
    private EntityManager manager;

    @Override
    public String getValue(String key) {
       .....
    }
    @Override
    public Map<String, String&gt; getProperties() {
       // read data form JPA Entity manager
       ....
    }
}

しかしながら、この直接的なやり方には問題があります。JPA Entity Managerや単なる別のEJBをCustomConfigSourceに注入しようとすると、このEntity ManagerがNullのため、値が想定通りに利用できないことに気づくでしょう。

この理由は、MicroProfile ConfigではすべてのConfigSourcesをPOJOとして取り扱うためです。そのため、注入された値は利用できません。例えば別のCDI beanが起動時に構成値が注入されることを想定していると考えてください。カスタムのConfigSource自体がCDIに依存していると、起動ループの問題が発生する可能性があります。その場合、どうすればよいでしょうか。

解決策は、Java Enterpriseではよくあることですが、非常にシンプルです。EntityManagerが注入済みであることを確実にするため、カスタムConfigSourceに@Startupを付け、@PostConstructをつけたメソッドを実装すればよいのです。

@Startup
@Singleton
public class MyConfigSource implements ConfigSource {
    @PersistenceContext(unitName = ".....")
    private EntityManager manager;

    @PostConstruct
    void init() {
        // load your data from teh JPA source or EJB
        ....
    }
   ...
}

こうすれば、init()メソッドで、自身で用意したEntity Manager(もしくは注入されたものすべて)にアクセスできます。MicroProfile Config APIは構成ソースをPOJOと引き続き見なしているため、作成したクラスを2度生成します(1回目はConfig APIから、2回目は@PostConstructのCDI実装からそれぞれコンストラクタが呼び出される)。では、両インスタンスに値を設定するにはどうすればよいのでしょうか。

こちらも解決策は極めてシンプルです。ConfigSourceはPOJOなので、静的メンバー値を使ってあたいを格納できます。この方法で、カスタムConfigSourceの各インスタンスで同一の値を確認できるのです。@PostConstructをつけると、ある種の遅延ロードで構成の値を設定します。以下の例をご覧ください。

@Startup
@Singleton
public class MyConfigSource implements ConfigSource {

    public static final String NAME = "MyConfigSource";
    public static Map<String, String&gt; properties = null; // note to use static here!

    @PersistenceContext(unitName = ".....")
    private EntityManager manager;

    @PostConstruct
    void init() {
        // load your data from teh JPA source or EJB
        ....
        // override the static property map..
        properties.put(....)
    }

    @Override
    public int getOrdinal() {
        return 890;
    }

    @Override
    public String getValue(String key) {
        if (properties != null) {
            return properties.get(key);
        } else {
            return null;
        }
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public Map<String, String&gt; getProperties() {
        return properties;
    }
}

静的メンバー値であるpropertiesを使い、すでに生成済みのConfigSourceからの値をオーバーロードします。これにより、ConfigSourceの任意のインスタンスは同じ値を共有します。値は後でロードされるため、ConfigSourceで起動時に値は設定されません。これはつまり別のCDI beanがあれば、@PostConstructの愛大これらの値にアクセスできない、ということです。しかしながら、値は実行時には利用できるようになっています。

遅延ロードメカニズムの不利な点があるため、この解決策は実装が極めてシンプルかつ簡単です。もちろんJNDI Lookupを使って、遅延ロードのトリックを使わずにデータソースからデータを取得することもできます。ここで紹介した解決策を使うと、データソースだけでなく、任意のCDIにアクセスできます。オープンソースプロジェクトlmixs-Workflowの最新版はMicroProfile 2.2ベースですが、この中でこの解決策を使っています。

Imixs Workflow – Open Source Workflow Engine for Human-Centric BPM
https://www.imixs.org/

Project Helidon and OpenAPI

このエントリは以下のエントリをベースにしています。
This entry is based on the following written by Tim Quinn (Oracle).
https://medium.com/oracledevs/project-helidon-and-openapi-54a1fadc75b1

Helidon 1.1.1以後、SE/MPともOpenAPIをサポートしており、MPではMicroProfile OpenAPI仕様をサポートしています。

Helidon
https://helidon.io/
OpenAPI Specification
https://github.com/OAI/OpenAPI-Specification
MicroProfile
https://microprofile.io/
MicroProfile OpenAPI Specification
https://github.com/eclipse/microprofile-open-api

SE、MPの両方に対し、OpenAPI機能の利用方法を説明したドキュメントとサンプルコードを用意しています。

OpenAPI support in Helidon SE
https://helidon.io/docs/latest/index.html#/openapi/01_openapi
Helidon SE OpenAPI Example
https://github.com/oracle/helidon/tree/master/examples/openapi
OpenAPI support in Helidon MP
https://helidon.io/docs/latest/index.html#/microprofile/08_openapi
Helidon MP Basic OpenAPI Example
https://github.com/oracle/helidon/tree/master/examples/microprofile/openapi-basic

このエントリではOpenAPIのサポートをHelidonで作成したアプリケーションに追加する最もシンプルな方法をご紹介します。

変更点はMP、SEいずれを使っているかで変わりますので、それぞれ分けて説明します。アプリケーションのOpenAPIドキュメントにアクセスする方法は同じなので、それはひとまとめにして最後に記載します。

Helidon MPの場合

Update your pom.xml

OpenAPI関連のアノテーションのスキャン高速化のため、Jandexインデックスを作成します。セクションに以下を追加します。

<plugin>
    <groupId>org.jboss.jandex</groupId>
    <artifactId>jandex-maven-plugin</artifactId>
    <version>1.0.6</version>
    <executions>
      <execution>
          <id>make-index</id>
          <goals>
              <goal>jandex</goal>
          </goals>
      </execution>
    </executions>
</plugin>

依存関係を追加すると、OpenAPIアノテーションを利用できます。アプリケーションの実行時にHelidon OpenAPIランタイム(およびMicroProfile 2.2に対するHelidonのその他のサポート)が存在するようにします。

<dependency>
    <groupId>org.eclipse.microprofile.openapi</groupId>
    <artifactId>microprofile-openapi-api</artifactId>
    <version>1.1.2</version>
</dependency
<dependency>
    <groupId>io.helidon.microprofile.bundles</groupId>
    <artifactId>helidon-microprofile-2.2</artifactId>
    <version>1.1.2</version>
</dependency>

Annotate the endpoints

OpenAPIのアノテーションをアプリケーションのエンドポイントに追加します。以下の例はシンプルなGETエンドポイントにOpenAPIアノテーションを付加しています。

@GET
@Operation(
    summary = "Returns a generic greeting",
    description = "Greets the user generically")
@APIResponse(
    description = "Simple JSON containing the greeting",
    content = @Content(
        mediaType = "application/json",
        schema = @Schema(implementation = GreetingMessage.class)))
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getDefaultMessage() {...}

@Operation@APIResponse は新規のアノテーションです。

Helidon SEの場合

SEにおけるOpenAPIのサポートは、基本的にMPと同じですが、SEはアノテーションの処理を含んでいません。そのため、OpenAPIはエンドポイントの情報を収集するためにアノテーションに依存できません。

その代わりに、SEで作成したアプリケーションにはアプリケーションのエンドポイントを記載したOpenAPIドキュメントの静的ファイルを含めることになるでしょう。

Update your pom.xml

依存関係を追加します。

<dependency>
     <groupId>io.helidon.openapi</groupId>
     <artifactId>helidon-openapi</artifactId>
     <version>1.1.2</version>
</dependency>

Register OpenAPISupport in your code

SEで作成したアプリケーションにはすでに以下のようなコードが含まれているはずです。OpenAPISupportを登録するために1行追加します。

Config config = Config.create();
...
return Routing.builder()
         .register(JsonSupport.create())
         .register(OpenAPISupport.create(config))
         .register(health)
         .register(metrics)
         .register("/greet", greetService)
         .build();

Add a static OpenAPI document file

Swaggerのようなツールを使って、アプリケーションのAPIを記述し、OpenAPIドキュメントファイルを生成しましょう。もしくは手作業でファイルを作成、編集することも可能です。ファイルができたら、プロジェクトのMETA-INF/openapi.ymlとして追加すれば、HelidonのOpenAPIサポート機能が自動検知してくれます(訳注:META-INF/openapi.yml以外には、META-INF/openapi.yamlもしくはMETA-INF/openapi.jsonとして追加できます)。

Access ths OpenAPI document

変更してアプリケーションをビルド後、実行してみましょう。自動的にサポートされる/openapiエンドポイントに対してGETリクエストを投げてください。上記のソースコードでエンドポイントにアノテーションで付加した情報を含む多数の情報を確認できます(訳注:OpenAPI 3ベースでした)。

paths:
  /greet:
    get:
      summary: Returns a generic greeting
      description: Greets the user generically
      responses:
        default:
          description: Simple JSON containing the greeting
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GreetingMessage'

What next?

これはほんの初歩的なものにすぎないので、例えばMPとSEのアプリケーションに、エンドポイントのOpenAPIモデルにプログラムで追加したり変更したり(つまりモデルのReaderとFilter)するコードを含めてみてもよいでしょう。

実のところ、HelidonのOpenAPIサポートでは、これらのソースすべてからのエンドポイント情報を組み合わせます。

  • 静的ファイル
  • アノテーション(MPのみ)
  • configurationの設定
  • model reader
  • model filter

Azure API Managementの外部キャッシュ (2)

このエントリは2019/06/19現在の情報に基づいています。将来の機能追加・廃止に伴い、記載内容との乖離が発生する可能性があります。また、外部キャッシュ機能は本日現在パブリック・プレビューです。

先日のエントリにおけるキャッシュ・ポリシーの設定情報を記載しておく。

Azure API Managementと外部キャッシュ (1)
https://logico-jp.io/2019/06/19/external-cache-for-azure-api-management-part1/

応答キャッシュ

応答キャッシュの場合、Query Parameterで指定した値はキーにできるが、Path Parameterで指定した値をキーにできない点に注意。以下の例では、inboundのcache-lookupでcodeというQuery Parameterをキーとして利用し、キャッシュに問い合わせしている。

キャッシュから取得
https://docs.microsoft.com/ja-jp/azure/api-management/api-management-caching-policies#GetFromCache
Get from cache
https://docs.microsoft.com/en-us/azure/api-management/api-management-caching-policies#GetFromCache

outboundのcache-storeでは、有効期限が3600秒、つまり1時間と指定しているだけで、値に対して何も指定をしていない。このことからも、応答キャッシュではレスポンスをそのままキャッシュに格納していることがわかる。

キャッシュに格納
https://docs.microsoft.com/ja-jp/azure/api-management/api-management-caching-policies#StoreToCache
Store to cache
https://docs.microsoft.com/en-us/azure/api-management/api-management-caching-policies#StoreToCache

<policies&gt;
    <inbound&gt;
        <base /&gt;
        <set-backend-service id="apim-generated-policy" backend-id="domestic" /&gt;
        <cache-lookup vary-by-developer="false" vary-by-developer-groups="false" downstream-caching-type="none" caching-type="external"&gt;
            <vary-by-query-parameter&gt;code</vary-by-query-parameter&gt;
        </cache-lookup&gt;
    </inbound&gt;
    <backend&gt;
        <base /&gt;
    </backend&gt;
    <outbound&gt;
        <base /&gt;
        <cache-store duration="3600" /&gt;
    </outbound&gt;
    <on-error&gt;
        <base /&gt;
    </on-error&gt;
</policies&gt;

値キャッシュ

先日のエントリでは、値キャッシュで応答キャッシュのような振る舞いをさせるために、以下のようなロジックを構成した。

  • inbound
    1. コードをPath Parameterから取得
    2. コードをキーとしてキャッシュに問い合わせる。
      1. 値がある場合、取得した値を使ってInboundパイプラインで応答を返す
      2. 値がない場合、バックエンドサービスを呼び出す
  • Outbound
    1. バックエンドの応答をキャッシュに文字列として格納。このとき、後続でもバックエンドサービスからの応答を利用できるよう、preserveContentフラグはONを指定
    2. 呼び出し側に応答を返す
<policies&gt;
    <inbound&gt;
        <base /&gt;
        <set-backend-service id="apim-generated-policy" backend-id="domestic" /&gt;
        <set-variable name="code" value="@(context.Request.MatchedParameters["code"])" /&gt;
        <cache-lookup-value key="@((string)(context.Variables["code"]))" default-value="" variable-name="response" caching-type="external" /&gt;
        <choose&gt;
            <when condition="@(context.Variables["response"] != "")"&gt;
                <return-response&gt;
                    <set-status code="200" /&gt;
                    <set-body&gt;@((string)context.Variables["response"])</set-body&gt;
                </return-response&gt;
            </when&gt;
        </choose&gt;
    </inbound&gt;
    <backend&gt;
        <base /&gt;
    </backend&gt;
    <outbound&gt;
        <base /&gt;
        <cache-store-value key="@((string)context.Variables["code"])" value="@(context.Response.Body.As<string&gt;(preserveContent: true))" caching-type="external" duration="3600" /&gt;
    </outbound&gt;
    <on-error&gt;
        <base /&gt;
    </on-error&gt;
</policies&gt;

Azure API Managementの外部キャッシュ (1)

このエントリは2019/06/19現在の情報に基づいています。将来の機能追加・廃止に伴い、記載内容との乖離が発生する可能性があります。また、外部キャッシュ機能は本日現在パブリック・プレビューです。

Azure API Managementのキャッシュ

Azure API Managementにはキャッシュ機構があり、応答した結果をキャッシュにためてレスポンスを向上させる仕組みが標準で備わっているが、外部キャッシュとして、Azure Cache for Redisを利用できる(Azureで提供しているものだけでなく、一般のRedisインスタンスも利用可能)。

Azure API Management で外部の Azure Cache for Redis を使用する
https://docs.microsoft.com/ja-jp/azure/api-management/api-management-howto-cache-external
Use an external Azure Cache for Redis in Azure API Management
https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-cache-external

外部キャッシュのメリット

以下のドキュメントに記載の通り、従量課金でAzure API Managementを利用すると、組み込みのキャッシュが利用できないが、外部キャッシュを使えばキャッシュ機構を利用できる。その他、キャッシュの構成をより細かく制御したり、利用中のAzure API Management の価格レベル以上のデータをキャッシュしたりでき、さらに、キャッシュ管理のライフサイクルをAPI Managementインスタンスのライフサイクルから切り離すことができる。そのため、メンテナンスやインスタンス移行時にもキャッシュを再作成しなくてすむ、といったメリットが考えられる。

Azure API Management レベルの機能に基づく比較
https://docs.microsoft.com/ja-jp/azure/api-management/api-management-features
Feature-based comparison of the Azure API Management tiers
https://docs.microsoft.com/en-us/azure/api-management/api-management-features

キャッシュ ポリシー

以下の2種類のポリシーがある。

  • 応答キャッシュポリシー (Response caching policies)
    • 有効期間(TTL)内の応答を保持、利用するしくみ
  • 値キャッシュポリシー (Value caching policies)
    • キーを使い値を格納、利用、削除できるしくみ
    • 特定のFragmentを格納することができる

API Management のキャッシュ ポリシー
https://docs.microsoft.com/ja-jp/azure/api-management/api-management-caching-policies
API Management caching policies
https://docs.microsoft.com/en-us/azure/api-management/api-management-caching-policies

構成

ドキュメント、というかチュートリアルの通り。以下には流れのみを記載。

  1. Azure Cache for Redisのインスタンスを作成
  2. (まだ作成していなければ)Azure API Managementインスタンスの作成
    • そこそこ時間がかかります…
  3. 1.で作成したCacheを2.で作成した(もしくは既存の)Azure API Managementインスタンスに関連付ける。
    • ドキュメントのスクリーンショットではあたかも2で作成したキャッシュインスタンスを選択するように見えるが、実際には「カスタム」しか選択できない。
    • 接続文字列は、Azure Cache for Redisの設定>アクセスキーにある接続文字列を指定(その他のインスタンスであれば該当する接続文字列を指定する)。
  4. 設定が終了したら保存をクリック

実際に使ってみる

簡単のため、国内の空港を3レターコードで取得するようなAPIを作成し、そのAPIを使うことにした。Redis Cacheへの格納を確認するため、Azure Portalから利用可能なRedisコンソールを使う。

応答キャッシュの場合

テスト前のキャッシュの状況は以下の通りで、キャッシュに何もない状態。

この段階で、FUK(福岡)を検索したところ、以下のようにキャッシュに追加されたことがわかる。

Redisコンソールで確認すると、データが格納されていることはわかるが、エンコードされていてよくわからない…。

簡単なRedisクライアントアプリケーションを作成して確認したところ、APIレスポンス自体をCacheしていた。

値キャッシュの場合

同じAPIでQuery Parameterをキーに、レスポンスのJSONを文字列としてキャッシュに格納することにした。最初はまだ何も入っていない状態。

コードとしてFUKを指定してAPIを呼ぶと、キャッシュにキーが追加されていることが確認できる。キーに対応する値が文字列として格納されていることも確認できる。

先ほどのRedisクライアントで内容を確認すると、レスポンス本体を文字列として保持していることを確認できた。

まとめ

外部キャッシュを簡単に利用可能であることを確認できた。また、応答キャッシュポリシーの場合、レスポンスがまるごと格納されていたが、値キャッシュの場合はほぼ指定した形式で格納されていたことがわかった(先頭にヘッダーと思しきものが付加されている)。

TTLはRedisの機能を使っているため、値キャッシュでその気になればTTLを-1にして永続化することも可能ではある。

値キャッシュは確かに開発の自由度が高いが、API Management外からのデータ更新は想定されていないことは留意すべきである。