REST API開発者はPOSTも冪等になるように設計して欲しいよねって言う話

こんにちは。
もう冬なのにこの国にはまだ蚊が出るんですよね…やってらんねえ〜〜。

さて、そろそろこの国に来て二年になるので、後一年くらいしたら北欧の方に移住しようかと考えています。
2月に長期休暇が取れそうなので、二週間くらい書けてエストニアを中心にヨーロッパの下見旅行に行こうかなと考えています。
エストニアの現地コミュニティに連絡をとったところ、何人か合ってくれそうな開発者がいたので、今からとても楽しみです。

あと、AWSのSolution Architect Professionalの勉強を始めました。
流石にそろそろとっておかないと色々厳しいので、この動画を使って勉強してます。

AWS Certified Solutions Architect (CSA) Professional: Exam | Udemy

超わかりやすい、焦る。
たぶんネイティブじゃないけど、英語もはっきりしてて、1.25 ~ 1.5倍速くらいで聞いてても全然入ってきます。 今年中に取れるかなぁ…来年までかかるかなぁ…とりあえずがんばります。

さて、今回は短めの記事です。

本題

分散型アプリの開発をしていて、別の開発者とPOSTの仕様で少し討論になったので、メモがわりに残しておきます。
このDiscussionのおかげで私はidempotentと言う単語を完全に記憶しました。

冪等性に関する簡単な説明

初学者向けにまず 冪等性(Idempotency) に関する説明です。
冪等性という言葉に関しては、Wikipediaの言葉を引用すると

ある操作を1回行っても複数回行っても結果が同じであることをいう概念である

とあります。

簡単に身近な例で例えると、

  • 汚れたお皿Aに洗うと言う行為を1回やる → 綺麗なお皿Aが残る
  • 汚れたお皿Aに洗うと言う行為を100回やる → 綺麗なお皿Aが残る

という感じで、洗うと言う行為はお皿に対して冪等な操作であると言えます。

一方、ポケットのビスケットを叩いて増やす歌を考えると

  • ポケットを叩く → ビスケットが2つ
  • もひとつ叩く → ビスケットが3つ

となり、叩くごとにポケットの中のビスケットは増えていくため、叩くと言う行為はポケットに対して冪等でない操作と言えます。

システム開発で例えると、

  • データベースのQUERY -> 何回叩いてもDBのレコード群の状態を変えない -> 冪等である

ですが、

  • INSERTをIncrement IDで流す -> 叩いた数だけレコードが作成される -> 冪等ではない

と言う感じになります。

この説明は全く厳密でないため、もっと詳しく知りたい方は @KyojiOsada さんの記事がとてもくわしく書かれており、とても参考になったので、ぜひそちらを読んでみてください。

参考記事: 冪等と安全に関する誤解

REST APIのメソッドごとの割り当て

詳しくは以前書いたこちらの記事に詳しく書きましたが、REST APIに置いて、リソースに対する操作は基本的に、HTTPのメソッドごとに処理を割り当てます。

そのうち、GET, PUT, DELETEなどのメソッドは、冪等性が保証されなくてはいけません。
(参考:REST API Tutorial)

一方で、POSTに関しては明確に

POST is NOT idempotent. (POSTは冪等ではない)

と言う風に記述されてます。

もうすこし上述のサイトのPostに関する部分を引用すると、

Generally – not necessarily – POST APIs are used to create a new resource on server. So when you invoke the same POST request N times, you will have N new resources on the server. So, POST is not idempotent.

要約すると、必須ではないが、一般的にPOSTは新しいリソースを作成するのに使用されるため、N回呼ばれればN個のリソースを作成されることが多いため、POSTは冪等ではない、と言う風に書かれています。

分散型アーキテクチャとWebAPIのエラーハンドリング

MSAなどサービス同士の疎結合を維持しながらWeb API経由でコミュニケーションをとるアーキテクチャを取る場合、クライアント側は以下のエラーの可能性を気にしながら実装を行う必要があります。

エラータイプ 代表的なステータス 対応例
即時の復旧が見込まれる一時的なServerエラー 503, 504, 509など 一定時間sleepさせたあとリトライする。
MQなどに流して復旧後にリトライ
復旧に長期間かかる(可能性のある)Serverエラー 500, 501, 502, 507など 処理を中断してロールバック
MQなどに流して復旧後にリトライ
ユーザにwarningを提示し復旧後に再度処理を流してもらう
リトライ可能なクライアントエラー 401, 407, 408, 429など 再認証したのちリトライ
一定時間sleepさせたあとリトライ
タイムアウトを伸ばしてリトライ
リトライ不可能なクライアントエラー 400, 403,405, 406など 入力をマスクしたのちlogに出力し開発者に通知
ユーザにwarningを提示し復旧後に再度処理を流してもらう
状況によっては無視できるクライアントエラー 404, 409など 既存オブジェクトを確認して同一ならスキップ(409)

※対応例やレスポンスのステータスコードはあくまで例示であり、通信先のサービスの仕様やビジネス要件に依存します。期待されるエラーやリトライ方法は鵜呑みにせず、自分のサービスや通信先の仕様や要件に合わせて柔軟に設計・実装してください。

また、WebApi経由の処理群はトランザクション管理が難しいため、処理群の中のAPIコールが1つでも失敗した場合、一連の処理を流し直したり、作成・編集された可能性のあるレコードを全てロールバックしたりと言う処理を行う必要があります。
(ここに関してはpub-subなどを利用した回避方法もあるのですが、それはアドベントカレンダーの記事で紹介します。多分。)

POSTが冪等性じゃないと困る理由

以上のように、Web APIコールで処理を行う設計の場合、多くのケースでAPIコールで失敗した場合ロールバックやリトライの処理を挟む必要があります。

単純にロールバックして処理を終える場合はまだいいのですが、リトライを行う際にPOSTが冪等性でない場合、APIコールごとにリソースが作成されるため、一旦すでに作成された可能性がある要素を削除して再度作り直すという処理が必要になり、リトライのコストが高くなります。

Web APIというのは使用者にとって使いやすく設計することが最も重要であり、リトライにたいするコストが高いAPIはいいAPIとは言えません。

POSTを冪等にするための実装

POSTを冪等にするための実装方法は、GOOGLEで検索するといくつか出てきますが、僕はPOSTを設計する際、よくResourceのIDを事前に指定して作成するようにしています。

例えば、

  
/api/docs/${docType}/${docId}

と言うRestfullなpathを設計した場合、一般的にはPOSTは一つ上の階層の/api/docs/${docType}/にたいして処理を割り当てることが多いですが、ここで/api/docs/${docType}/${docId}に対してもPOSTを割り当てられる用にします。

このような設計にした場合、

  • IDが重複しないこと
  • 重複した場合適切なHTTP Statusを返す事 -重複した場合、できるだけClient側でGetし直さずにエラーハンドリングができるようにすること

などを設計の段階で織り込んで置く必要があります。

IDのコンフリクト回避には、UUIDのversion4や、作成したいリソースのhashから作成したUUIDなどを使い、作成方法をAPI Docsなどに明示します。 ←ここ大事

作成したいリソースのHashから作成する場合、一意性を強固にするため、作成時間のTimestampと作成者のIDを必ず含めるようにしてhashを作成し、Pathに割り当てた上でPOSTしてもらいます。

また、エラーハンドリングを用意にするため、作成が成功した場合200を、已に作成されていた場合は409を返すようにします。
IDがConflictする可能性はほぼゼロと言っていいレベルで低いですが、409が返す時、一緒に作成者のIDのSHA256HashおよびTimestampを返す用に設計すれば、409が返ってきた際にもAPIの利用者は安心して作成をスキップすることができます。

また、僕がメンバーと議論してる時に大いに参考にさせていただいた、Saurav SinghさんのHow to achieve idempotency in POST method?では、headerにidempotentKeyを設定し、そのidempotentKeyをなんらかのStorageに保存するという方法が紹介されています。

個人的にはやや冗長に感じるためあまりモチベーションはわかないのですが、こういう方法もあるんだなといい勉強になりました。

まとめ

POSTは一般的に冪等性が担保しにくいメソッド、もしくは担保しなくていいメソッドという認識が一般的かもしれません。
しかし、POSTの冪等性をAPI開発者側が担保してやることで、リトライやロールバックが用意になり、利用者からはより使いやすいAPIになると僕は考えています。

通信先のサービスの一部のNodeが落ちていることや、リクエスト過多によるスケーリング中でタイムアウトしてしまうことなど、分散型では日常茶飯事です。

フォールトトレラントなAPIを提供するためにも、明確にPOSTの冪等性を担保することは有意義だと考えています。

もしこの記事がこれからAPIを設計しようとしている誰かの役に立てば幸いです。