はじめに
CAPの特にNode.js版に慣れ親しんだ開発者を読者に想定しています。
課題
SAP Cloud Application Programming Model(CAP)を利用してバックエンドアプリケーションを開発する際には、時間のかかる処理をバックグラウンドで実行したい場面があります。
具体的には、
SAP Job Scheduling ServiceからHTTPリクエストを介して呼び出されるジョブが考えられます。HTTPリクエスト経由で呼び出されるジョブで15秒以上かかる際には
非同期モードでジョブを呼び出す必要があり、非同期モードではジョブは呼び出された直後にHTTPステータスコード202を返し、完了した時点でジョブ側からSAP Job Scheduling Serviceにステータスの更新を行うような構成になります。
このようなケースではリクエストに対して即座に応答し実際の処理をバックグラウンドで行うために、CAP内の「Context」や「Transaction」といった概念を適切に扱うことが重要です。これを怠ると、ジョブから外部のAPI呼び出す際などにエラーが発生する可能性があるため注意が必要です。以下で発生し得るエラーについて具体的にご紹介します。
エラーが発生する例:
(*注意: ジョブ等は本来テクニカルユーザーでAPI呼び出しを行うべきで以下の例の様にPrincipal Propagationを利用することは推奨されません。要件上どうしても必要な場合にのみこの方法の利用を検討下さい。)
CAPで下記の様なOData Functionを定義し、ハンドラーの中でSAP S/4HANAのBusiness Partner一覧をOData API経由で取得します。但し、以下の点に注意してください。
- 呼び出し直後にステータスコード202を返却し、OData API呼び出しはsetImmediateで起動するコールバック内で実行する
- S/4 HANAのOData API呼び出しの際にはPrincipal Propagationを利用しfunctionを呼び出したユーザーに紐づくS/4 HANAのユーザーでBusiness Partnerを取得する
- "Authentication"が"Principal Propagation"に設定されているDestinationを利用する
service BackgroundjobService {
function errorFunction() returns {};
}
this.on("errorFunction", async (req) => {
setImmediate(async () => { // Initiate background job
// do some tasks...
const service = await cds.connect.to('API_BUSINESS_PARTNER');
const result = await service.run(SELECT.from("A_AddressEmailAddress").columns(["AddressID", "EmailAddress"]))
console.log(result);
})
return cds.context.http.res.status(202); // return 202 Accepted
})
上記の様な実装ではOData API呼び出し時点(service.run呼び出し時点)に以下の様なエラーが発生してしまいます。エラーの内容はユーザー情報が載ったJWT(リクエストのAuthorizationヘッダーに格納されている)がDestinationをSAP Destination Serviceに取得しに行く際に渡されていないという旨になっています。
Error: Error during request to remote service:
....
Failed to load destination. Caused by: No user token (JWT) has been provided. This is strictly necessary for 'PrincipalPropagation'.
Principal Propagationが設定されているDestinationを利用している際には、CAPはSAP Authentication and Trust Management Service(XSUAA)から取得しApprouterでAuthorizationヘッダーにセットされるJWTの値をDestination Serviceまで取りまわすことでどのユーザーがCAPのエンドポイントを叩いているかを判別するためJWTが適切に取りまわされる必要があります。また、同様のエラーは、Destination ServiceにJWTを取りまわす必要が有るOAuth2SAMLBearerAssertionが設定されているDestinationについても起こり得ます。
原因
JWTが適切に取りまわされていないのは、上記実装だとCAPのTransactionを適切に扱えていないことに起因します。
CAPにはTransactionと言う概念がありAPIを呼び出したユーザー情報やリクエストヘッダの保持、DBのトランザクション等の管理をしています。Transactionは基本的には自動でCAPにより管理されるため、ハンドラーが呼び出された時点で作られ、レスポンスが返された時点でcommit又はrollbackされそれ以降そのTransactionは利用出来なくなります。(
参考)
setImmediateで呼び出したコールバック内の処理は基本的にハンドラーから処理が脱しTransactionが利用出来なくなった後に実行されるため(*1)Transactionが持っているリクエストのヘッダー情報などがコールバック内で利用出来ずユーザー情報の取り回しに失敗して上記の様なエラーが発生します。
*1: そうでない場合も存在することに注意。setImmediateが呼ばれた後に、Javascript上の概念であるイベントループに挿入されるタスクとして分離される処理が有る場合にはsetImmediateで起動したコールバックの方が先に実行される可能性があります。具体的にはsetImmediateを呼び出した後にAPI呼び出し等を行いawaitする際など。その場合には上記コードはエラー無く動作します。
解決策
CAPが提供するcds.spawn というメソッドを利用します。cds.spawnは現在のTransactionから切り離した新しいTransactionで処理を実行することが出来ます。
this.on("workingFunction", async (req) => {
cds.spawn({headers: req.headers},async () => { // Initiate background job
// do some tasks...
const service = await cds.connect.to('API_BUSINESS_PARTNER');
const result = await service.run(SELECT.from("A_AddressEmailAddress").columns(["AddressID", "EmailAddress"]))
console.log(result);
})
return cds.context.http.res.status(202); // return 202 Accepted
})
cds.spawnの第一引数にコールバック内で利用するContext(=新たに生成されるTransactionにセットされる値)を設定します。指定しない場合呼び出し時点のContextの値を引き継ぎますが、Contextはtenant , user , locale のみを基本的に保持する(
参考)為それ以外のheader情報等は自動で渡されない為明示的に設定しています。
まとめ
本ブログではNode.js版SAP Cloud Application Programming Modelで実装したバックグラウンドアプリケーションでバックグラウンド処理を実装する際に起こる問題とその解決方法についてご紹介しました。
CAPにはユーザー情報等の取り回しを便利にするためのContextやTransactionと言った概念があり、普段はそれらの概念の事を意識せずに実装を進めていくことが出来ます。しかし一歩立ち入った機能を実装しようとすると本ブログで扱ったような問題に出会いある程度意識せざる負えなくなってきます。今回はContextやTransactionについて深く立ち入りませんでしたが今後のブログでより詳しくご紹介出来ればと考えています。