UUID機能の利用法と実装について

WebPayでは、エラーが発生したときなどに同一のリクエストを複数回おくったために、おなじリクエストが複数回処理されてしまうことを防ぐために、UUIDを利用してリクエストを識別する仕組みを採用しています。 この仕組みの利用法については過去のQiitaの投稿でも触れていますが、本稿ではあらためてUUIDによるリクエスト識別機能を紹介するとともに、この機能をどのように実装しているかをサンプルコードで紹介していきます。

リクエスト識別機能の利用法と挙動について

本機能は、記事執筆時点で次のオブジェクトを作成するときに利用できます。

  • 課金(Charge)
  • 顧客(Customer)
  • トークン(Token)
  • 定期課金(Recursion)

課金の作成のドキュメントには、UUIDが次のように説明されています。

uuid: 任意 デフォルトはnull

RFC4122に準拠したUUID(例:“f81d4fae-7dec-11d0-a765-00a0c91e6bf6”)を設定すると、同じUUIDを持つリクエストが複数回送信されたとき、24時間の間に高々一度だけ処理がおこなわれることを保証します。以前の同じUUIDを持つリクエストで作成済みの課金がある場合は、それを通常の作成時と同じように返却します。

具体的には次のように使用します(curlによる例)。

1
2
3
4
5
6
7
8
9
10
11
curl "https://api.webpay.jp/v1/charges" \
-u "test_secret_eHn4TTgsGguBcW764a2KA8Yd": \
-d "amount=400" \
-d "currency=jpy" \
-d "card[number]=4242-4242-4242-4242" \
-d "card[exp_month]=11" \
-d "card[exp_year]=2014" \
-d "card[cvc]=123" \
-d "card[name]=KEI KUBO" \
-d "description=UUIDを利用した課金" \
-d "uuid=f81d4fae-7dec-11d0-a765-00a0c91e6bf6"

仮にこのリクエストが通信エラーなどで失敗した場合、UUIDなしではどこで失敗したのかを調査し、再度リクエストすべきかどうかを判断する必要がありました。UUIDでリクエストを識別可能にしたことで、上のリクエストをそのまま再度実行することができます。

1度目のリクエストがサーバ内では正常に処理されていたけれどもレスポンスデータが受信できなかったなどの理由で通信に失敗した場合は、2度目のリクエストでは1度目のリクエストで作成された課金オブジェクトが返却されます。 1度目のリクエストがサーバに到達していなかった場合、2度目のリクエストはサーバにとって最初のリクエストとなるので、通常通り処理がおこなわれ、課金が作成されて返却されます。 このようにして、多重課金などの予期せぬ動作を防ぐことができます。

UUIDはリクエストを識別するための識別子なので、別のリクエストを送信するときはかならず異なるUUIDを利用する必要があります。 記事執筆時点では、WebPayはRFC4122で定義されたUUIDとして有効なフォーマットを取っていればいかなる値でも受け入れます。 適切なライブラリを利用するなどして、毎回異なるUUIDがつかわれるようにプログラミングをおこなってください。

UUIDがレスポンスの同一性を担保するのは24時間までで、それ以降はおなじUUIDをつけてリクエストをおこなっても再度処理される場合があります。 24時間以上の期間をあけてのおなじUUIDでのリクエストは、おなじレスポンスになることもありますが、保証しておりませんのでご注意ください。

実装方法

このようなUUIDによるリクエスト識別ですが、実際にサーバサイドのプログラミングのタスクとして考えると、次のような性質があります。

  • 複数のオブジェクトで利用したい
  • リクエストのタイミングとして複数のケースを考慮する必要がある
    • 1度目のリクエストが正常終了してから2度目のリクエストが来た場合
    • 1度目のリクエストが異常終了してから2度目のリクエストが来た場合
    • 1度目のリクエストを処理中に2度目のリクエストが来た場合
    • 1度目のリクエストがこなかった場合
  • 付加的な機能なので、メインのロジックへの干渉を最小限にしたい

WebPayではこの機能を実装するために、次のコードで紹介するようなメカニズムを作成しました。 (このコードは説明のためのサンプルであり、実際の実装とは異なります)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class RequestIdentifier < ActiveRecord::Base
  attr_accessible :uuid, :entity, :request_params

  serialize :request_params
  belongs_to :entity, polymorphic: true

  def self.find_existing(uuid, request_params)
    existing = self.find(uuid: uuid)
    if existing
      if existing.request_params == request_params
        existing.entity
      else
        raise "異なるリクエストがおなじUUIDで送信されました"
      end
    end
  end

  def self.save_uuid_with_entity(uuid, request_params, entity)
    transaction do
      entity.save!
      begin
        self.create!(uuid: uuid, request_params: request_params, entity: entity)
      rescue => e
        raise NotUniqueUUID.new("UUIDの保存に失敗しました")
      end
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ChargesController < ApplicationController
  def create
    charge_params = filter_params(params)
    if existing = RequestIdentifier.find_existing(params[:uuid], charge_params)
      return render json: existing, status: :created
    end
    charge = Charge.create_by_api_request(charge_params)
    RequestIdentifier.save_uuid_with_entity(params[:uuid], charge_params, charge)
  rescue => NotUniqueUUID
    if existing = RequestIdentifier.find_existing(params[:uuid], charge_params)
      return render json: existing, status: :created
    else
      raise # エラーハンドラでエラーレスポンスをかえす
    end
  end
end

実際にはWebPayのAPIではリクエストパラメタにおうじて複数の分岐が発生し、複雑な処理をおこなうので、コントローラはもっと複雑になっています。 しかしUUIDに関係するコードは冒頭と末尾に集中しているので、この部分だけをコピー・ペーストすることで複数のコントローラに簡単に適用することができました。 blockをつかうなどいくつかの方法を検討しましたが、どのコントローラでも使える最もシンプルな実装は上にあげたようなものでした。

複数のリクエストが成立してしまうと不利益が発生するというのは頻繁に出現するケースだと思います。 そのようなAPIを実装する場合、ぜひUUIDによるリクエストの識別をとりいれ、ユーザの利便性を向上していきたいものです。 このサンプルがその一助となれば幸いです。