ネットワークロックサービス
便宜上勝手に名付けた。排他制御を持たないリソースへのアクセス競合を防ぐため、ネットワーク上にロックサービスを用意する。既存のリソースを更新する場合、最初にネットワークロックの取得を試みて、その後読み出し→更新→ロック解放と処理を行う。ロック取得に失敗した場合は後でリトライする。もしロック取得したプログラムが落ちてしまう場合は解放されないロックが残ってしまう。この問題を最小限にする目的でロックにタイムアウトの機能を持たせる。時間が経過したロックは無視して、再取得できるようにする。
アーキテクチャ
AWSの LambdaとAPI Gateway、そして DynamoDB を使ったサーバレスアーキテクチャで組み立てる。
サーバ(EC2)を立てる必要がなく楽ちん。
Lambda Function は JavaScript (node.js)でコーディングする。
ロック取得に対する排他制御
ロックを保管管理するストレージ(DB)が排他的な操作に対応しているかどうかが鍵になる。今回の用途を考えると RDSのような大げさなものは使いたくない。またLambdaから容易に操作できるものということで、 DynamoDBを採用する。最小構成で使えば安価に運用できるのも魅力的。この DynamoDBについて排他制御を調べいるとまったく同じことを考えている人が居た。参考になる。
DynamoDBをロックマネージャーとして使う
DynamoDBではアイテム(レコード)作成時の前提条件を定義することができて、これが成り立たないと更新処理を失敗させることができる。この前提条件に、これから操作を行いたいリソースのIDの非存在を使えば、排他制御が可能になる。
アイテム作成実行(情報:リソースID)
前提条件:リソースIDの非存在
もし前提条件が
満たされる場合:
リソースIDを主キーとするアイテムの作成成功
満たされない場合(既にリソースIDが存在する場合):
アイテムの作成失敗(エラー)
さらにアイテムにタイムアウト時刻を入れて、前提条件に時刻も盛り込めばタイムアウトが働くようになる。最終的にはこんな感じ。
アイテム作成実行(情報:リソースID、タイムアウト時刻)
前提条件:リソースIDの非存在 OR タイムアウト時刻 < 現在時刻
もし前提条件が
満たされる場合:
リソースIDを主キーとするアイテムの作成成功
満たされない場合(既にリソースIDが存在する場合):
アイテムの作成失敗(エラー)
なお expectedは Deprecated扱いの様で ConditionExpression が推奨らしい。
設定
設定の流れは次の通り。※細かいところは省く。
1. DynamoDB テーブル作成
2. Lambda Function 作成
先ほどの排他制御をJavaScriptで実装する。1つの関数でロック取得とロック解放に対応する。
ロック取得のところだけ抜粋するとこんな感じ。
dynamo.putItem({ "TableName" : tableName, "Item" : { "hash" : {"S" : hash}, "expirationTime" : {"N" : expirationTime.toString()}, // "created" : {"S" : now.toString()} }, "ConditionExpression" : "expirationTime < :nowTime OR attribute_not_exists(#hash)", "ExpressionAttributeNames" : { "#hash" : "hash", }, "ExpressionAttributeValues": { ":nowTime": {"N": nowTime.toString()}, }, }, function(err, data) { :
ConditionExpression の箇所がキモ。DynamoDBは予約語が多くて、それを回避するために #の別名を定義したり、プレースホルダとして :を定義したりと、慣れていないとハマる。他にも ”N"は数値だが、文字列で数値を渡さないと行けないとか...。
3. API Gateway作成
作成したLambda FunctionをAPI経由で呼び出せるようにする。GETでロック取得、DELETEでロック解放としてみた。
API Keyを有効にし、URLのクエリパラメータ "hash"を受け取るようにする。
hashパラメータの内容をJSON形式に変換して Lambdaへ渡す(Body Mapping Templatesで定義)。GET、DELETE共に同じ設定を行う。Lambda Functionではプログラム内で HTTP メソッドを元に処理を分岐させている。
設定が終わったらデプロイする。
デプロイ先の環境を複数作成できる。デフォルトでは prod(恐らくproductionの略)が選べる。
今回 API 認証を有効にしたので忘れずに API Keyを作成して、上記で作った API(と環境 prod)に紐付けておく。
API Gateway でデプロイするとAPIを利用するためのエンドポイント(URL)が環境ごとに用意される。このURLとAPI Key(文字列)を使ってアプリからこの APIを呼び出す。
- - - -
しばらく iOSアプリからこのネットワークロックを使ってみることにする。
おわり。