Event Meshを使わずに非同期な連携を実現する方法

## 1. はじめに
この記事は [SAP Advent Calendar 2022](https://adventar.org/calendars/7484) の12月1日分の記事として執筆しています。

BTPで非同期な連携を実現する方法といえば[Event Mesh](https://qiita.com/tami/items/727fe127158eea39fd3e)が有名です。Integrasion Suiteの一部として紹介されることの多いEvent Meshですが、「サービス」としてのIntegrasion Suiteには含まれていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/11c09e07-d728-fb2b-0716-0ef5061a71bb.png)
図：[SAP Integration Suite – Overview, Recent Innovations, and Road Map [IN203]](https://go3.events.sap.com/sapteched/hybrid/2022/reg/flow/sap/saptech2022/sapteched2022catalog/page/catalog/session/1661198036478001EaEZ)のセッション資料より抜粋

[SAP Discovery Center](https://discovery-center.cloud.sap/search/integration%20suite)で見ると、独立したサービスとして提供されていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/25733df2-33ee-29e4-bdfa-c5554af961cd.png)
つまり、**Integrasion SuiteをサブスクライブしているだけではEvent Meshは使えず、使うためには追加のお金がかかる**ということです。そこでこの記事では、Event Meshを使わずに非同期な連携を実現する方法を実現する方法を紹介します。

## 2. Event Meshを使わずに非同期な連携を実現するには
前提：Integration Suiteをサブスクライブしていること

今回はおなじみのCloud Integration (CPI)を使って非同期な連携を実装します。CPIにはJMSキューというものが用意されており、Event Meshのキューと同じようにメッセージをためることができます。下図の**Integration Flow1**はJMSキューにメッセージを格納するところまでを行い、送信側にすぐ結果を返します。**Integration Flow2**はキューにメッセージが格納されると起動し、受信側へメッセージを送信します。これによって非同期な連携が実現できます。

キューからメッセージを取り出す[JMS Sender Adapter](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/161791b8cb98485ba00d81efa4197a49.html)はフローがエラーで終了した場合、成功するまで一定間隔でリトライします。これにより、受信側が一時的に利用不可であっても確実にメッセージを届けることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/51f58ff0-a25b-0875-2abe-7e528ea50954.png)
図：[Use Case for JMS](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/5d2c32fb336340bcb060d8d15997888d.html)より引用

## 3. 実装
ここではInboundとOuboundという2つのフローを実装します。Inboundは送信側からメッセージを受け取りキューに格納します。Outboundはキューからメッセージを取り出し、受信側に届けます。

### 3.1. 事前準備
受信側として、CAPで簡単なサービスを作成しました。

```data-model.cds
namespace receiver;
using { cuid, managed } from '@sap/cds/common';

entity Messages: cuid, managed{
  message  : String;
}
```

```receiver-service.cds
using receiver as db from '../db/data-model';

service ReceiverService {
    entity Messages as projection on db.Messages;
}
```
以下のようなリクエストを投げると、HANA Cloudにレコードが格納されます。

```
POST http://<host>/receiver/Messages
Content-Type: application/json

{
    "message": "hello"
}
```

### 3.2. Inboundのフロー
Inboundのフローでは、受け取ったメッセージをキューに格納し、送信側にレスポンスを返します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d8f56565-1839-f83f-0529-bdaf087621b1.png)

#### 3.2.1. フローの設定
##### ①HTTPS Sender Adapter
Addressに任意のパスを指定します。呼び出しを簡単にするため、CSRF Protectedのチェックは外しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/708cfab0-3d0f-dda1-bf87-9fa1200b0a17.png)

##### ②JMSキューにメッセージを格納 [JMS Receiver Adapter]
Queue Nameに任意の名前を設定し、他はデフォルトのままとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4a2f5275-da0a-5467-92ae-4a0090ef08be.png)

なお、JMSキューにメッセージを格納するための"Store Message"には[Send](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/9b83f10c882f44988a2856ceb8515b3c.html)というコンポーネントを使っています。HTTPSアダプターなどを使う場合は[Request Reply](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/dc39fdd4a44d4b9a9eabb56f49434250.html)を使いますが、JMSキューへの格納はリプライを返さないためSendを使います。

##### ③レスポンスの設定 [Content Modifier]
送信側に結果を返すため、ヘッダとボディを設定します。ボディを空で返すのでヘッダにステータスコード204 (No Content)を設定します。CPIでステータスコードを設定するには、"CamelHttpResponseCode"を使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2fdbdd8d-19d3-e669-34ef-9428a44c4cb8.png)
ボディはTypeを"Constant"とし、中身は空にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d1dae9cd-e4dc-4be0-877e-021c69a8409c.png)

#### 3.2.2. デプロイ
デプロイするとキューが登録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5a62d4d8-d7a9-40b2-9278-1bb66a382b09.png)
この時点ではキューは空です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/35011dbd-9c9e-658e-a8e9-b786e19dcf67.png)

#### 3.2.3. テスト
Postmanからリクエストを送信します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/52e84aa3-a4ba-e0a7-5e05-1d171fba612c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/50946a5a-f748-afcf-4c4d-8666c293851f.png)
ステータス204 (No Content)で返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/65fdc7b0-4161-16be-016c-964174d95c5c.png)
キューにメッセージが格納されています。まだ受信側のキューがないので処理されずに溜まっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3fe688e8-ad4b-24a6-d066-9c8a7e3b138e.png)

### 3.3. Outboundのフロー
Outboundのフローでは、キューからメッセージを取り出し、事前準備で作成したODataサービスに送信します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7c7214f5-306a-b80f-accc-361fb9e22d5b.png)

#### 3.3.1. フローの設定
##### ①キューからメッセージを取得 [JMS Sender Adapter]
Inboundのフローで指定したのと同じキュー名を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/1cc06964-af0c-fbad-8ddd-dca42cbdde56.png)
 
##### ②ヘッダの設定 [Content Modifier]
ヘッダにContent-Type (application/json)を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/78cecb98-a79e-dd42-8b42-d29fe8d25769.png)

##### ③受信側にメッセージを送信 [HTTPS Receiver Adapter]
受信側のODataサービスにメッセージを送信します。OData V4 AdapterではXML形式のメッセージしか送れないため、HTTPSアダプターを使用します。Addressに受信側のODataサービスのURIを、Request Headersに②で設定したContent-Typeを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6d619672-a576-304c-68a0-23d7eff086cd.png)

#### 3.3.2. デプロイ
デプロイするとたまっていたメッセージが処理され、キューからメッセージが消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8d285e02-abe8-2ad9-c40e-cb6f2826abc4.png)
ODataサービスを確認すると、データが登録されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2361e933-c1b8-6405-d797-f36998983c3b.png)


## 4. エラー時の動作
受信側が利用不可だった場合の動作について確認してみます。

### 4.1. JMSアダプターの設定
エラー時の動作を決めるパラメータはいくつかあります。

#### 4.1.1. Inbound
Inboundで使用するアダプターでは以下の2つのパラメータが関連します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c34d815f-dbcc-0675-4cce-714f520aeb33.png)

- **Retention Threshold for Alerting (in d)**：この日数を超えてメッセージがキューに残っている場合にアラートを発生させる
- **Expiration Period (in d)**：この日数を超えてメッセージが残っている場合にメッセージを削除する

参考：[Configure the JMS Receiver Adapter](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/79edc04c91574a6bb8c15ae3e1a27b03.html)

#### 4.1.2. Outbound
Outboundで使用するアダプターでは、RETRY DETAILSにあるパラメータが関連します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/42658987-ed62-d017-91e5-25f3498a2b1a.png)

- **Retry Interval (in m)**：リトライまでの待ち時間（分）を指定する
- **Exponential Backoff**：チェックをつけた場合、リトライが失敗するたびに待ち時間が2倍になる
- **Maximum Retry Interval (in m)**：リトライまでの最大の待ち時間（分）を指定する（Exponential Backoffを設定した場合に設定）
- **Dead-Letter Queue**：メッセージ処理がout-of-memory errorで停止したときにメッセージがブロックされ、その後2回送信が試みられる。ブロックされたメッセージはMessage Locksタイルからロックを解除できる

参考： [Configure the JMS Sender Adapter](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/161791b8cb98485ba00d81efa4197a49.html)

### 4.2. 動作確認
Retention Threshold for Alertingを1日、Expiration Period (in d)を2日に設定して動作確認してみます。想定では以下の動きになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8baa554f-9ccd-aac5-e313-099181032ff6.png)

- エラー発生直後から1分後、2分後…とリトライし、リトライ間隔が60分になった以降は60分おきにリトライする
- キューに格納されてから1日後にアラートが発生する（どのような形で？）
- キューに格納されたから2日後にキューから削除される

#### 4.2.1. 設定
InboundのJMS Adapterの設定を変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/1ee30c75-2781-f6c9-e5be-36041d727d7b.png)
Outboundのフローでエラーを発生させるため、CAPのサービスを停止しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/21883433-15c9-c99d-7a52-79bc1f4ba8cf.png)

#### 4.2.2. 結果
##### エラー発生直後
エラー発生直後、キューのステータスはFailedになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6a2d2f70-3e1d-59f9-9beb-bb5184ebb1d6.png)

処理結果のログを見るとすでに複数回リトライされています。ただ、リトライ間隔が1分どころではありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a7e65b67-977f-5e81-0ab4-109430f67b70.png)
よく見ると、1回のリトライで2つの処理が行われています。リトライ間隔は初回は約44秒と短めですが、以降は2分、4分と2倍になっていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ccd53ad6-3399-cf07-7f9b-8877dbff006c.png)
さらに時間が経過するとリトライ間隔は8分、16分と伸びていき、最後は60分で固定されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b9a9169e-569f-ad4e-2446-7ef3357939ed.png)

##### 1日後
キューにメッセージが格納されてから24時間経過しました。相変わらず60分ごとにリトライが実行されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0e8f8b4c-b4d4-f5b1-0526-d42af4ac8ac1.png)
どこかにアラートが届くのかと思っていましたが、そのようなことはありませんでした。
[ヘルプ](https://help.sap.com/docs/CLOUD_INTEGRATION/368c481cd6954bdfa5d0435479fd4eaf/cdcce24f484a41c08ab46d12ab666451.html)によると、

- メッセージの登録日時にRetention Threshold for Alertingを足した日時がDue Atに設定される
- Due Atを過ぎた場合、メッセージのステータスは"Overdue"になる

ということでしたが、Due Atを過ぎてもステータスはFailedのままでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8c5ac6db-717f-ff98-8281-339016be4c99.png)

ステータスの件は謎ですが、Retention Threshold for Alertingはアラートを発生させるのではなく、キューに期限をつけて期限が切れたキューがないかを運用者がチェックするためのものであるようです。

##### 2日後
キューからメッセージが削除されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/70ffae41-27b5-e506-9387-e67116d028a9.png)

## 5. おわりに
この記事では、CPIのJMSアダプターを使って非同期な連携を実現する方法を紹介しました。一方のEvent MeshにはS/4HANAとの連携ができることや、CAPと簡単に接続ができるなどのメリットがあります。よって、実際にはやりたいことによって使い分けになるだろうと思われます。
私は個人的にはCPIが好きで、調べていくと割となんでもできてしまうところに魅力を感じます。今後もCPIが使えるケースを見つけていきたいと思います。