gumadesu

日々の学びをアウトプット

冪等性とはなにか、なぜ必要か

はじめに

StripeやAmazonPayなどの決済API をみていると「冪等性(idempotency)」という概念を見かけます。
なんとなく、参照系じゃなくて更新系のAPIで必要なんでしょ?って思ってるんですが、自信を持って説明できない。
改めて冪等性とはなにか、なぜ必要なのかを理解していく。

TL;DR

  • 冪等性とは「ある操作を1回行っても複数回行っても結果が同じであることをいう概念」
  • 冪等性の導入を検討したほうがよいAPIは、以下のような場合
    • 意図しない複数回の実行を避けたいケース
      • (例)ユーザボタンを連打し、複数回の決済処理が実行される可能性がある
    • ネットワークエラーが発生した場合、クライアント側が処理の成否を正しく判断出来ない場合
      • (例) ネットワークタイムアウトにより1回目の処理結果が受け取れなかったため、2回目のリトライをしてよいか判断しづらい

冪等性とはなにか

wikipedia によると、冪等性とは以下のような意味を持つらしい。

数学において、冪等性(べきとうせい、英: idempotence 「巾等性」とも書くが読み方は同じ)は、大雑把に言って、ある操作を1回行っても複数回行っても結果が同じであることをいう概念である。

もともとは数学の概念なんですね…。 身近な例でわかりやすく例えてみましょう。

冪等性がない例

リモコンの停止/再生ボタン

f:id:kashiwaguma-hiro:20210710152624p:plain

  • 映像が再生中の場合に押すと、停止する。
  • 映像が停止中の場合の押すと、再生する。
  • つまり、ボタンを押すたびに結果が変わる。

冪等性がある例

エレベータの階数ボタン

  • エレベーターに乗る時、7F行きのボタンを1度押すとエレベータを呼ぶことが出来る。
  • 7F行きのボタンを連打しても、早く呼ぶことはできないので押しても効果は変わらない。

バイクのセルスターター

f:id:kashiwaguma-hiro:20210710153249p:plain

  •  エンジンが停止している状態でセルスタータを押すとエンジンがかかる。
  • 既にエンジンが掛かっている状態でセルスターターを押しても、エンジンが止まらないので、何度押しても結果は変わらない。

身近にも冪等性がどうやら潜んでいるらしいとわかりましたね。 でも、なんで冪等性って必要なんでしょうか?

冪等性はなぜ必要?

私の解釈では、冪等性があると「便利だから」だと考えられます。
エレベータの階数ボタンを連打することで早く呼び出せると嬉しいですが、みんながエレベータ連打バトルをやりだしたらどうでしょうか?公平性を欠きそうで、必ずしも便利と言えなさそうですね。
バイクのセルスタータを押すたびにエンジンがついたり消えたりしたら、押し続けるタイミングが非常にシビアになり、圧倒的に不便になりますね。

逆に、冪等性がなくていいものは、冪等性がなくても不便じゃないものだと言えそうです。

APIにおける冪等性

最初の疑問に戻りまして「なぜStripe、AmazonPayのAPIに冪等性があるか」についてですが、これも「あったほうが便利だから」と考えられそうです。 冪等性が必要かどうかはケースバイケースなので、どういう場合に必要か考えてみましょう。

どんなAPIで冪等性が必要?

Stripeのドキュメントを見てみましょう。

べき等

Stripe API は、GET および DELETE リクエストのべき等性を保証しているため、再試行しても安全です。特定量の断続的なエラーが予期されるため、クライアントには、失敗したリクエストをサーバと調整する方法が必要であり、べき等性がそのメカニズムをもたらします。べき等キーを含めると、POST リクエストがべき等性を持ち、API は重複操作を避けるために必要な記録を行います。
 
(中略)
 
たとえば、支払いを作成するリクエストがネットワーク接続エラーのために応答しない場合には、クライアントは、同じべき等キーを使用してリクエストを再試行すれば、重複した支払いが作成されることはありません。

Stripeの場合は HTTP Methodが GET, DELETEの場合は冪等性を保証しているので、冪等性は不要そうですが、 POSTについては冪等性がないため冪等キーによって重複実行を保証 していますね。

HTTP Methodによって、冪等性の要否が変わるのか?

POSTであれば、どんなAPIも冪等性は必要なのでしょうか?
他の例も考えてみましょう。

...

ある会員制サイトではメールアドレスをIDとして利用しており、会員登録のAPIが以下のようなパラメータだとします。

curl POST \
   -H "Content-Type:application/json" \
   -d \
'{
    "name":"kashiwaguma-hiro",
    "email":"sample@example.com",
    "password":"xxxxxxxxxx",
    "password_confirm":"xxxxxxxxxx",
}' \
 'https://api.example.com/v1/register'

会員登録APIを全く同じパラメータで複数回実行する場合、1回目は成功しますが、2回目以降は「すでにemailが登録されています」とエラーで会員登録が失敗しそうです。 このケースだけだと、HTTP MethodがPOSTだからといって冪等性が必要だとは言い切れなさそうです。

ネットワークエラーへの配慮

もしも、ネットワークが不安定な場合はどうでしょうか? 先程の例で、1回目のリクエストでネットワークタイムアウトが発生して結果を受け取れなかったとします。
リトライするために2回目も同じパラメータでリクエストしたら「すでにemailが登録されています」となり会員登録に失敗します。

このとき、クライアント側は「1回目は実は成功していた」か、「本当に別の人が先にメールを登録してしまった」かを区別することが出来ません。

...

冪等性はこのようなケースでも有効に働きます。
冪等性のあるAPIでは、同じ冪等性キーのリクエストが来た場合、1回目と同じレスポンスをするような実装になっています。
そのため、1回目のリクエストでネットワークタイムアウトになりレスポンスを受け取れなくても、2回目に同じ冪等性キーでリクエストすることで1回目の実行結果を参照することができるのです。
何度実行しても、最初のリクエスト結果を参照できるので、クライアント側としては「より便利に扱うこと」ができそうですね。

実は標準化が進んでいる「冪等性」

この冪等性のしくみですが、 RFCにて規格を標準化するため、インターネットドラフトが公開されています。

datatracker.ietf.org

冪等性キーの仕様や、重複したリクエストが来た場合などの対処についても書かれていますので、ご興味があれば見てみてください。

最後に

チームメンバーから「冪等性ってなんで必要なの?」って聞かれても、それとなく説明できるようになった気がします。
また気になることがあれば自己流ですがまとめていこうと思います。

テンションが下がった時、やる気が出ない時にやること

最近、社内でネガティブなイベントが多くて仕事に集中できない、やる気が出ないと感じるケースがあった。
後で見返して活用するために、どう対処しているかをまとめておく。

場所を移動する、体を動かす

自席に座って黙々と作業すると気分の切り替えが上手くいかないので、コンビニに飲み物を買いにいったり、気分転換のため少し散歩するなどで気分転換を図る。

だが、自分の場合は何もしないと悪いイメージが浮かんできちゃうので、あまり効果がないと感じる。

起こった事象に対して、深堀りして考える

マインドマップなどでその事象に対しての自分なりの整理を行う。
整理、咀嚼することで落ち着けるかと思いきや、ネガティブ思考に入ってしまい悪いことばかり思い浮かんでくるので、すぐにやるのは駄目っぽい。
自身の気力が回復したタイミングで行うと良い。

手を動かす系の作業をやる

設計、スケジュールを引くとか考える系の作業をやらない。
ネガティブなイベントがどうしても気になってしまい、思考がまとまらないので効率が最悪。

あまり頭を使わないコーディング、テスト、雑務など手を動かす系の作業を行うことで、気を紛らわすようにしている。

そんなに効果がないけど、まだ働かなきゃいけない時間のときにやってる。

好きな音楽などを聞いてテンションを上げる

無理やり切り替える方法。
好きな音楽を爆音できいたりして、意識をそらすことに務める。

効果的な場合とそうじゃない場合がある。

人と会話(雑談)をする

弊社の場合はフルリモートワークだが作業はペアプロやモブプロをやることが多い。 また、デイリースクラム後に雑談が好きなメンバーがいるため、リモートワークにしては人との雑談頻度は高い状態といえる。 家で仕事をしてる人は家族と雑談したりコミュニケーションするだけでも違う。

少し乗り気じゃなくても、人と雑談することでかなり気が紛れると感じた。

かなり効果的だったので、おすすめ。

仕事を切り上げる

集中できないときに無理やりやってもいいことは無いので、さっさと諦めて翌日頑張るのもアリ。

まとめてみての感想

  • 美味しいものを食べるとかは、自分にはあんまり効果的じゃない気がする。 もしかして、食に興味が薄い?
  • 筋トレや運動をするなど、もっとアクティブに体を動かすことを検討してもよさそう。

GithubでREADME.mdへ、gifをサイズ指定して表示させる

GithubのREADME.mdにはgifで操作をわかりやすく説明しているOSSを見かけます.
github.com

それと同じことをやりたくなったので、調べてみました.

tl;dr

GIPHY Captureでgif画像を撮影

GIPHY Capture. The GIF Maker

GIPHY Capture. The GIF Maker

  • Giphy, Inc.
  • ビデオ
  • 無料
apps.apple.com

HTMLタグで画像を読み込み、サイズを指定
<img src="画像パス" width="横幅(px)">

gif画像を撮影する

色々方法がありますが、操作が直感的で保存も簡単な GIPHY Captureがおすすめです!

GIPHY Capture. The GIF Maker

GIPHY Capture. The GIF Maker

  • Giphy, Inc.
  • ビデオ
  • 無料
apps.apple.com

キャプチャ後に編集して保存する際、保存後のサイズを Calculate Size で確認し、範囲や画質を選択するとよいです. (アップロードサイズ制限があるため) f:id:kashiwaguma-hiro:20200128223438p:plain

Githubにgifを載せる

Markdownは以下のようにして画像を埋め込めますが

![テキスト](画像URL)

サイズ指定したい場合はHTMLのimgタグで指定できます.

<img src="画像URL" width="横幅(px)">

画像URL は相対パスで指定可能です. gif 画像を resource/demo.gif とした場合、以下のように読み込みます.

<img src="resource/demo.gif" width="400">

画像サイズは、vscodeなどのMarkdownプレビュー機能がついているエディタでやるのがおすすめです. f:id:kashiwaguma-hiro:20200128225106p:plain

完成

以下が完成品です.

github.com

想像していたよりも簡単にできるので、よければ試してみてください.

参考

GitHubの画像貼り付けでサイズを指定する方法 - dackdive's blog

GitへProxy経由で接続する

認証なしProxyの場合

gitのproxy設定を行いましょう.

$ git config --global http.proxy http://proxy_host:proxy_port
$ git config --global https.proxy http://proxy_host:proxy_port

認証ありProxyの場合

Proxyのドメイン名の前に認証情報(user:pass と @マーク)を付与する.

$ git config --global http.proxy http://user:pass@proxy_host:proxy_port
$ git config --global https.proxy http://user:pass@proxy_host:proxy_port

※ユーザ認証情報 @ が含まれる場合、該当文字はURLエンコードした値(%40)を設定しましょう.

< http://hoge@fuga.com:pass@proxy_host:proxy_port
---
> http://hoge%40fuga.com:pass@proxy_host:proxy_port

特定のドメインのみでProxyを有効にする方法

特定のドメイン(https://example.com/)のみ、Proxyを通したい場合は http.https://example.com.proxy といった具合にURLをキーとして指定する.

$ git config --global http.https://example.com/.proxy http://user:pass@proxy_host:proxy_port

うまく行かない場合

設定値の確認

~/.gitconfig をみて設定が意図通りうまく出来てるかチェックしましょう.

$ cat ~/.gitconfig

...
[http]
        proxy = http://user:pass@proxy_host:proxy_port
[https]
        proxy = http://user:pass@proxy_host:proxy_port
[http "https://example.com/"]
        proxy = http://user:pass@proxy_host:proxy_port
[https "https://example.com/"]
        proxy = http://user:pass@proxy_host:proxy_port

設定値を削除したい場合

--unset サブコマンドを使えばよい.

$ git config --global --unset http.proxy

参考にしたサイト

git-scm.com

qiita.com

お手軽にDatadogのアラートをLINEで受け取れるようにする

弊社では Datadog でアプリやインフラを監視しています. 今までアラートの通知はSlack経由だったが、意外と気づきにくいという問題がありました.

普段から連絡手段にLINEを利用していたので、LINEへアラートを飛ばすことが出来ないかと調べてみました.
すると、意外と簡単に実現できたので、方法を残してきます.

構成

全体の構成はこんな感じです. f:id:kashiwaguma-hiro:20191214231049p:plain

実現するためにやることは以下のとおりです.

  1. LINE Channelを作成
  2. 開発メンバーがLINE Channelを友だち登録
  3. アクセストークンを発行し、ブロードキャストメッセージ送信できるようにする
  4. Datadogからアラート発生時にブロードキャストメッセージを送信するように設定

順に説明していきます.

LINE Channelを作成

個人用のLINEアカウントがある前提です. 今回は個人用として作成しました.

まずは LINE Developers にログイン

f:id:kashiwaguma-hiro:20191213222611p:plain

ログインしたら Providorを作る. f:id:kashiwaguma-hiro:20191213223032p:plain

プロバイダ名はよしなにつけましょう. 今回は「アラート通知」とします. f:id:kashiwaguma-hiro:20191213223210p:plain

作成に成功し、次のような画面に遷移します.
Messaging APIを選択. f:id:kashiwaguma-hiro:20191213223603p:plain

色々入力項目がありますが、よしなに入力しましょう. 注意点は Channel typeを「Messaging API」とすることぐらいです. f:id:kashiwaguma-hiro:20191213224343p:plain

Channelが作成できると、お友達登録用のQRコードが作成されます.

f:id:kashiwaguma-hiro:20191213225604p:plain

開発メンバーがLINE Channelを友だち登録

先程のQRコードを使って、LINEのお友達追加から登録しましょう. f:id:kashiwaguma-hiro:20191213230050p:plain f:id:kashiwaguma-hiro:20191213230211p:plain

問題なく友達になれましたね 🙆

アクセストークンを発行し、ブロードキャストメッセージ送信できるようにする

Messagening APIbradcast-message を使います.
これはお友達全員にメッセージを送信することが出来る機能です.

リクエストサンプルを見るとわかりますが、リクエストにはchannel access tokenが必要になります.
channel access tokenは先程のQRコードを表示している画面から issue してください.

f:id:kashiwaguma-hiro:20191213232053p:plain

発行できたトークンを使って以下のリクエストを投げてみましょう.

~ ❯❯❯  curl -v -X POST https://api.line.me/v2/bot/message/broadcast \
-H 'Content-Type:application/json' \
-H 'Authorization: Bearer XXXXXXXXXX' \
-d '{
    "messages":[
        {
            "type":"text",
            "text":"Hello, world1"
        },
        {
            "type":"text",
            "text":"Hello, world2"
        }
    ]
}'
Note: Unnecessary use of -X or --request, POST is already inferred.

~~~ 省略 ~~~

< HTTP/1.1 200 OK
< Server: nginx
< Date: Fri, 13 Dec 2019 14:29:01 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< x-line-request-id: ca9024fc-5ace-4410-a743-fd8230c58e41
< x-content-type-options: nosniff
< x-xss-protection: 1; mode=block
< cache-control: no-cache, no-store, max-age=0, must-revalidate
< pragma: no-cache
< expires: 0
< x-frame-options: DENY
< 
* Connection #0 to host api.line.me left intact
{}* Closing connection 0

リクエスト結果は「200 OK」でした LINEを見ると、無事通知を受け取ることが出来てますね
f:id:kashiwaguma-hiro:20191213233053p:plain

Datadogからアラート発生時にブロードキャストメッセージを送信するように設定

今回はDatadogの例でお届けします.
まずIntegrationから Webhook を作ります. f:id:kashiwaguma-hiro:20191213235024p:plain

URLは curl実行したときのURL、 Payloadは Request Body、 Custom Headersには AuthrizationHeader の値を設定します. f:id:kashiwaguma-hiro:20191214004322p:plain

Custom HeadersはJSON形式でないと形式エラーになるので注意しましょう. Request Bodyについては Datadogの変数 を使ってお好みの値を設定しましょう.

設定が終わったら Datadog Monitorに移動してMonitorを作ります(作り方は割愛).
異常検知の通知先として、さきほど作成したWebhookを設定します.
f:id:kashiwaguma-hiro:20191214004557p:plain

最後に通知されるか、テストのアラートを発砲します.
Test Notification -> Alert, Alert Recoveryを選択 -> Run Test
f:id:kashiwaguma-hiro:20191214005152p:plain

設定がうまく行っていれば、LINE Botから無事通知がきます 🎉🎉🎉 f:id:kashiwaguma-hiro:20191214005625p:plain

手順は以上となります.

構成で意識したところ

一番意識したのは「動かし続けるのに手間がかからなようにすること」です.
ChatOpsなどもそうですが、よかれと思って作った運用補助サービスが、管理者不在担った途端にツール運用自体がメンバーの負担になることが往々にして起こりうるので、それを避けたかったのが理由です.

管理が必要なインフラを持たない

できるだけ自分たちでインフラの面倒を見ないで済む構成にしました.
最初は LINE Channelからのwebhookを受けるために Herokuで構築しようとしました.
ですが、 Heroku で無料運用するためには、定期的にリクエストを送信してインスタンスが死なないようにする必要があります.
いくら無料とはいえ 運用のための運用が増えることが嫌だった ので、自前でインフラを用意しなくてよい構成としました.

アラートの受信設定変更が、簡単であること

アラートを受け取るメンバーを追加・削除するといった運用もめんどくさいとおもってました.
なので LINE Channelのお友達機能を利用しました.
これを利用することで、アラートを受け取りたいメンバーは勝手にお友達になればいいし、チームを離脱するときはお友達を解除すればいいですよね?

ハマったところ

DatadogのWebhookでCustom HeadersはJSON指定じゃないとダメという点でした.
実は、DatadogのEventsからWebhookエラーは確認することが出来ます.
f:id:kashiwaguma-hiro:20191214010244p:plain もし設定してもうまく通知されない場合は、Eventsを見てみるとヒントが得られるかもしれません.

終わりに

Slackだった通知をLINEにしたことで、よりアラートに気づきやすい状況を作ることが出来ました.
MessagingAPIのブロードキャストを使っているので、開発メンバーの増減があったとしても、メンバーがLINE Channelの登録・解除をすればいいだけなので手間が少ないです.
設定時間は、慣れていれば1時間程度でできますので、興味がある方はぜひ試してみてください.