RESTful APIで非同期処理を扱うデータモデリング:状態管理と通知パターン
はじめに
多くのウェブアプリケーションやサービスでは、リクエストを受け取ってから完了までに時間がかかる処理が存在します。例えば、大量データのインポート、複雑なレポート生成、外部システムとの連携などです。これらの処理を同期的に行ってしまうと、クライアントは処理が完了するまで待機する必要があり、ユーザー体験の低下やタイムアウトの問題を引き起こす可能性があります。
このような場合、処理を非同期で行い、APIとしてはリクエストを受け付けたことを即座にクライアントに返す設計が採用されます。RESTful APIで非同期処理を扱う際には、処理の進行状況をどのように表現し、結果をどのようにクライアントに伝えるかが重要な設計課題となります。ここでは、非同期処理を組み込んだRESTful APIのデータモデリングについて、具体的なアプローチと考え方を解説します。
RESTful APIと非同期処理の課題
RESTful APIは通常、クライアントからのリクエストに対して、サーバーが即座に処理を行い、その結果をレスポンスとして返す「同期的な」モデルに基づいています。しかし、非同期処理では、リクエストを受け付けた時点では最終的な結果が出ていません。このため、以下のような課題が生じます。
- 状態の管理: サーバー側で進行中の処理(ジョブ)の状態(実行中、完了、失敗など)をどのように管理し、クライアントに公開するか。
- 結果の取得: 処理が完了した後、クライアントはどのようにしてその結果を取得するのか。
- 通知: 処理が完了したことをサーバーからクライアントにどのように通知するのか。
- エラーハンドリング: 非同期処理中にエラーが発生した場合、それをどのようにクライアントに伝えるのか。
これらの課題を解決するためには、非同期処理自体を一つの「リソース」として捉え、その状態や結果をデータモデルとして適切に設計する必要があります。
非同期処理のデータモデリングの基本
非同期処理をRESTful APIに組み込む際の基本的な考え方は、実行された非同期処理を一つのリソースとして表現することです。このリソースは、一般的に「ジョブ」や「タスク」などと呼ばれます。
ジョブリソースの定義
非同期処理を表すジョブリソースは、その処理を一意に識別するための情報、現在の状態、そして最終的な結果やエラーへの参照を持つべきです。基本的な属性としては以下が考えられます。
id
: ジョブを一意に識別するID。status
: ジョブの現在の状態(例:PENDING
,PROCESSING
,COMPLETED
,FAILED
,CANCELLED
)。これはEnum型や固定文字列で表現します。created_at
: ジョブが作成された日時。updated_at
: ジョブの状態が最後に更新された日時。resource_url
(またはinput_data
): 何を対象とした処理なのか、あるいは処理の入力データへの参照。result_url
(またはresult_data
): 処理が成功した場合の、結果データへの参照、あるいは結果データそのもの。error
: 処理が失敗した場合の、エラー情報。
非同期処理開始時のフローとレスポンス
クライアントが非同期処理をサーバーに依頼する場合、通常は以下のような流れになります。
- クライアントが特定のエンドポイントにリクエスト(例:
POST /reports
でレポート生成を依頼)を送信します。 - サーバーは非同期処理を開始(キューに入れるなど)し、即座にレスポンスを返します。
- このレスポンスには、開始された非同期処理を表すジョブリソースの情報を含めることが一般的です。最低限、そのジョブのIDと現在の状態、そして後から状態や結果を確認するためのURLを含めます。
リクエスト例:
POST /reports/generate
Content-Type: application/json
{
"reportType": "salesSummary",
"startDate": "2023-01-01",
"endDate": "2023-12-31"
}
レスポンス例 (202 Accepted):
{
"job_id": "report-job-12345",
"status": "PENDING",
"status_url": "/jobs/report-job-12345"
}
ここでステータスコードに 202 Accepted
を使用するのは適切です。これは、リクエストは受理されたものの、処理は完了していないことを意味します。レスポンスボディには、後続処理に必要な情報(ジョブID、状態確認用URL)を含めます。
非同期処理の結果取得と通知パターン
非同期処理が開始された後、クライアントがその結果を知るための主なパターンは以下の2つです。
- ポーリング (Polling): クライアントが定期的にサーバーに問い合わせてジョブの状態を確認する方式。
- コールバック/Webhook: サーバー側で処理が完了した際に、事前に指定されたクライアント側のURLに通知を送信する方式。
1. ポーリングパターン
ポーリングパターンでは、クライアントは最初に受け取ったジョブIDまたは状態確認用URLを使用して、定期的にジョブリソースを取得します。
状態確認のリクエスト例:
GET /jobs/report-job-12345
状態確認のレスポンス例 (ジョブ実行中):
{
"job_id": "report-job-12345",
"status": "PROCESSING",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T10:05:00Z",
"status_url": "/jobs/report-job-12345"
}
状態確認のレスポンス例 (ジョブ完了):
{
"job_id": "report-job-12345",
"status": "COMPLETED",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T10:15:00Z",
"status_url": "/jobs/report-job-12345",
"result_url": "/reports/sales-summary-2023" // 結果リソースへのURL
}
ジョブが完了(COMPLETED
)ステータスになったら、クライアントは result_url
を見て、結果を取得するための別のリクエストを送信します。
結果取得のリクエスト例:
GET /reports/sales-summary-2023
結果取得のレスポンス例:
{
"period": "2023",
"totalSales": 123456789,
"items": [ ... ] // レポートデータ本体
}
ポーリングパターンのメリット・デメリット:
- メリット: 実装が比較的シンプル。クライアント側で polling 間隔を制御できる。
- デメリット: サーバーへの定期的な不要なリクエストが発生する可能性がある。リアルタイム性に欠ける。ポーリング間隔の設計が難しい(短すぎるとサーバー負荷増、長すぎると結果取得が遅れる)。
2. コールバック/Webhookパターン
Webhookパターンでは、クライアントは非同期処理を開始する際に、結果通知を受け取りたいURL(コールバックURL)をサーバーに伝えます。サーバーは処理完了後、そのURLに対してHTTPリクエストを送信して結果を通知します。
コールバックURLを指定して非同期処理を開始するリクエスト例:
POST /reports/generate
Content-Type: application/json
{
"reportType": "salesSummary",
"startDate": "2023-01-01",
"endDate": "2023-12-31",
"callbackUrl": "https://client.example.com/api/webhook/report-status"
}
レスポンス例 (202 Accepted):
{
"job_id": "report-job-67890",
"status": "PENDING" // callbackUrl を指定した場合は status_url は不要な場合も
}
サーバーからクライアントへのWebhook通知例:
サーバーは処理完了後、指定された callbackUrl
へPOSTリクエストを送信します。ペイロードには、完了したジョブの情報を含めます。
POST https://client.example.com/api/webhook/report-status
Content-Type: application/json
{
"job_id": "report-job-67890",
"status": "COMPLETED",
"result_url": "/reports/sales-summary-2023", // 結果リソースへのURL
"timestamp": "2024-01-20T10:20:00Z"
}
クライアントは、この通知を受け取ったら、必要に応じて result_url
から結果の詳細を取得します。
Webhookパターンのメリット・デメリット:
- メリット: リアルタイムに近い通知が可能。サーバーへの不要なポーリングが不要。
- デメリット: クライアント側で外部からのHTTPリクエストを受け付けられる環境が必要。通知の信頼性(ネットワークエラー、クライアント側のダウン)を考慮する必要がある(リトライ機構など)。サーバー側で通知管理の実装が複雑になる。セキュリティ(通知が正規のものかどうかの検証、署名など)に注意が必要。
どちらのパターンを選択するかは、アプリケーションの要件(リアルタイム性、クライアントの能力、システム構成など)によって異なります。両方をサポートする場合もあります。
状態遷移とエラーハンドリングのデータモデリング
状態遷移
ジョブリソースの status
は、非同期処理の進行に伴って変化します。一般的な状態遷移は以下のようになります(例)。
PENDING
: 処理待ち(キュー投入済みなど)PROCESSING
: 処理実行中COMPLETED
: 処理成功FAILED
: 処理失敗CANCELLED
: 処理キャンセル
ジョブリソースの表現において、status
フィールドはEnum型やあらかじめ定義された文字列セットとしてモデル化します。これにより、クライアントはステータスの取りうる値を予測できます。
エラーハンドリング
非同期処理が失敗した場合(status: FAILED
)、ジョブリソースにはその原因を示すエラー情報を含めるべきです。これは error
フィールドとしてネストされたオブジェクトで表現すると良いでしょう。
error
オブジェクトの例:
"error": {
"code": "REPORT_GENERATION_FAILED", // エラータイプを識別するコード
"message": "Failed to generate report due to data processing error.", // 人間が読めるメッセージ
"details": { // 詳細な情報(オプション)
"processingStep": "data aggregation",
"internalErrorCode": "ERR1001"
}
}
クライアントはジョブの状態が FAILED
になった際に、この error
オブジェクトを解析して、エラーの原因を特定したり、ユーザーに適切なメッセージを表示したりできます。
考慮事項
- 冪等性: 非同期処理の開始リクエスト自体を冪等にする必要があるか検討します。例えば、同じパラメータで複数回リクエストした場合、新しいジョブを都度作成するのか、既存のジョブを返すのかなどを設計します。
- タイムアウトとキャンセル: 非同期処理にはタイムアウトを設定し、長すぎる実行を防ぐべきです。また、進行中のジョブをキャンセルするAPI (
DELETE /jobs/{job_id}
) を提供することも検討します。これらの操作もジョブリソースの状態遷移としてモデル化されます(例:CANCELLED
ステータス)。 - ペイロードサイズ: 非同期処理の結果データが大きい場合、結果そのものをジョブリソースに含めるのではなく、別のリソースとして切り出し、ジョブリソースからはそのリソースへの参照(
result_url
)を持つようにします。これはデータモデリングの観点からも、リソースの関心を分離する良い設計です。 - セキュリティ: Webhookを使用する場合、通知が正規のサーバーから送られたものであることを検証するために、署名などのメカニズムを導入すべきです。これは通知ペイロードの一部として署名関連のデータを含めるデータモデリングに影響します。
まとめ
RESTful APIで非同期処理を扱う場合、単に処理をバックグラウンドで実行するだけでなく、その「処理自体」を一つのリソースとして適切にデータモデリングすることが重要です。ジョブリソースを導入し、その状態、結果、エラーを明確に表現することで、クライアントは処理の進行状況を追跡し、結果を安全かつ効率的に取得できるようになります。
ポーリングとWebhookは結果通知の主要なパターンであり、それぞれにメリットとデメリットがあります。アプリケーションの特性に応じて最適なパターンを選択するか、両方を組み合わせて提供することを検討してください。
非同期処理のデータモデリングは、APIの使いやすさ、信頼性、そしてスケーラビリティに大きく影響します。ここで解説した基本的な考え方やパターンを参考に、ご自身のAPI設計に役立てていただければ幸いです。