DynamoDBに実装されたトランザクション機能ですが、 前回は主に正常系の動作、及び使い方を紹介しました。
今回は異常系の挙動について整理してみたいと思います。
テーブルやLambdaは前回のを使いますので用意しておいてください。
トランザクションについて
まず前提として排他制御について説明しておきます。 排他制御には主に2パターンの方法があります。 楽観的排他制御とロックを取得するパターン(悲観的排他制御)です。
楽観的排他制御はSQLServerを使う時に比較的よく使われる方法で(筆者の体感) アイテム(行)毎にバージョン番号や最終更新日時を持たせておき、 その値が一緒であれば更新する方法です。
ロックを取得するパターンはOracleやMySQLでよく使われる方法で アイテム(行)のロックを取得し、誰も触れないような状態を作り上げてから更新する方法です。
DynamoDBのトランザクションは1番目の楽観的排他制御を採用しています。
1つめのテーブルで排他制御に引っかかった場合
前回、アイテムテーブルのitemCを無効(available=false)へ更新しました。
この状態でもう1度、「itemCが有効(available=true)な場合に無効にする」という処理を行ってみます。
Lambdaのコードも前回と同じで大丈夫です。
console.log('Loading function'); var AWS = require('aws-sdk'); var dynamoDb = new AWS.DynamoDB(); exports.handler = async (event, context) => { var data = await dynamoDb.transactWriteItems({ TransactItems: [ { Update: { TableName: 'test_items', Key: { itemId: { S: 'itemC' } }, ConditionExpression: 'available = :true', UpdateExpression: 'set available = :false, ' + 'ownedBy = :player', ExpressionAttributeValues: { ':true': { BOOL: true }, ':false': { BOOL: false }, ':player': { S: 'Mario' } } } }, { Update: { TableName: 'test_players', Key: { playerId: { S: 'Mario' } }, ConditionExpression: 'coins >= :price', UpdateExpression: 'set coins = coins - :price, ' + 'inventory = list_append(inventory, :items)', ExpressionAttributeValues: { ':items': { L: [{ S: 'itemC' }] }, ':price': { N: '30' } } } } ] }).promise(); return `Successfully`; };
ConditionExpression: 'available = :true', の部分が「有効な場合」に当たります。
実行してみると、既に無効なアイテムになっているのでエラーが発生するはずです。
errorTypeがTransactionCanceledExceptionになっているのは排他制御に引っかかったため、ロールバックしたことが分かります。
実際に使用する場合は例外処理を挟む必要があるので、次のようなコードになるでしょう。
console.log('Loading function'); var AWS = require('aws-sdk'); var dynamoDb = new AWS.DynamoDB(); exports.handler = async (event, context) => { try { var data = await dynamoDb.transactWriteItems({ TransactItems: [ { Update: { TableName: 'test_items', Key: { itemId: { S: 'itemC' } }, ConditionExpression: 'available = :true', UpdateExpression: 'set available = :false, ' + 'ownedBy = :player', ExpressionAttributeValues: { ':true': { BOOL: true }, ':false': { BOOL: false }, ':player': { S: 'Mario' } } } }, { Update: { TableName: 'test_players', Key: { playerId: { S: 'Mario' } }, ConditionExpression: 'coins >= :price', UpdateExpression: 'set coins = coins - :price, ' + 'inventory = list_append(inventory, :items)', ExpressionAttributeValues: { ':items': { L: [{ S: 'itemC' }] }, ':price': { N: '30' } } } } ] }).promise(); } catch (e) { if (e.name == 'TransactionCanceledException') { return `Already updated`; } throw e; } return `Successfully`; };
try〜catchで囲み、TransactionCanceledExceptionが発生していたら「既に更新済み」と判定しています。
2つめのテーブルで排他制御に引っかかった場合
アイテム1つめは成功したが、2つめの排他に引っかかった場合も見てみましょう。
itemCを有効にして、Marioのcoinsを20へ下げました(30のitemCは買えない状態)
コードは例外の中身を見たいので、当記事の最初のコードへ戻して実行してみます。
2つめの更新に失敗した場合はerrorMessageが[None, ConditionalCheckFailed]になっています。
排他制御としては更新対象1つめに引っかかってエラーになっても、 2つめに引っかかっても全体をリロードさせる処理にすることが多く、 更新対象をハンドリングすることはまず無いでしょう。
もし必要になった場合は、errorMessageを正規表現で判定してやるしか今の所ありません。
リソースが見つからない場合
テーブルが見つからない場合の動きも見てみましょう。 どちらかというと開発段階でよく見るエラーになると思います。
console.log('Loading function'); var AWS = require('aws-sdk'); var dynamoDb = new AWS.DynamoDB(); exports.handler = async (event, context) => { var data = await dynamoDb.transactWriteItems({ TransactItems: [ { Update: { TableName: 'test_itemsXXXXX', Key: { itemId: { S: 'itemC' } }, ConditionExpression: 'available = :true', UpdateExpression: 'set available = :false, ' + 'ownedBy = :player', ExpressionAttributeValues: { ':true': { BOOL: true }, ':false': { BOOL: false }, ':player': { S: 'Mario' } } } } ] }).promise(); return `Successfully`; };
TableNameを変な名前に変更してあります。 これを実行すると次のようになります。
ResourceNotFoundExceptionが発生しています。 項目名が違う場合もこのエラーになるのですが、 リージョンが違うためにテーブルが参照できなかった場合は、TransactionCanceledExceptionになるので注意です。
最後に
ひとまず私が実装してみてつまづいた点を中心に説明してみました。 使っていくと色々とつまづきポイントが出てくるとは思いますので、 その時はまたまとめたいと思います。