The Round

合同会社ナイツオの開発ブログ

[PR] 5分から相談できるGCP™ 開発コンサル!→こちら

GAE/Go トランザクション

注:古い記事の為、内容が最新ではない可能性がありますm(_ _)m

どうもこんにちわ!マツウラです。
今回はオペレーションを確実に実行するため用いるトランザクションについてです。
それではGo言語での使い方について見てゆきます。

参考:Go — Google Developers Transactions

App Engineではトランザクションがサポートされています。
トランザクションは完全に成功、または失敗することが保証された単一のオペレーションまたはオペレーションセットです。 アプリケーションは単一のトランザクションで複数のオペレーションや計算を実行することが可能です。

トランザクションの実行にはdatastore.RunInTransactionを使用します。
次の例は単純なリクエスト回数をカウントする際にトランザクションを用いたコードです。

c := appengine.NewContext(r)

key := datastore.NewKey(c, "Counter", "counter", 0, nil)
var count Counter
err := datastore.RunInTransaction(c, func(c appengine.Context) error {
    err := datastore.Get(c, key, &count)
    if err != nil && err != datastore.ErrNoSuchEntity {
        return err
    }
    count.Count++
    _, err = datastore.Put(c, key, count)
    return err
}, nil)

if err != nil {
    c.Errorf("Transaction failed: %v", err)
    return
}

fmt.Fprintf(w, "Current count: %d", count.Count)

Warning: 上記のサンプルはトランザクションを出来るだけ簡単に表現するために書かれたコードです。
カウンター操作のベストプラクティスでは無いので注意してください。

RunInTransactionがトランザクションのコミットを試み、成功した場合はnilが返されます。
nil以外のerror値を返した場合、データストアの変更は適用されず、RunInTransactionはそのエラーと同じエラーを返します。

次のような場合はエラーを返します。

  • 同一のエンティティグループで大量の同時変更が行われている
  • トランザクションのリソース制限を超えた
  • データストアの内部エラーが発生した

べき等

トランザクションでもべき等となるように注意して実装します。

トランザクションの競合によりコミットが失敗すると、RunInTransactionは関数をリトライします。

そのため、関数内で関数外のmapやsliceを操作しているような場合では、リトライのたびに値が変化してしまいます。 十分に注意してください。

トランザクションの制限

データストアは単一のトランザクション内で可能なことに制限を掛けています。

単一のトランザクションで複数のエンティティに対して適用する場合、属するエンティティグループが最大で5つまでに制限されています。
複数のエンティティグループを組み合わせてトランザクションを実行するにはTransactionOptionsが必要になります。

option := &datastore.TransactionOptions{XG: true}
err := datastore.RunInTransaction(c, func(c appengine.Context) error {
    ...
}, option)

Transactional taskをQueueに格納する

トランザクションの一部としてタスクをQueueに積むことが出来ます。
一度Queueに積まれてもTaskは即座に実行されるとは限らず、トランザクションのようにアトミックでもありません。
しかし、一度Queueに積まれればTaskは成功するまで再試行されます。
これはRunInTransaction関数で積まれた全てのTaskに適用されます。

Transactional taskは購入確認のEメール送信など、成功保証が必要な処理との組み合わせでも便利です。
トランザクションが成功した場合に限って、トランザクション外部のエンティティグループに変更をコミットするなど、トランザクションにデータストア操作を結びつけることが出来ます。

制限

単一トランザクションでタスクキューに5つ以上のtransactional tasksを積むことは出来ません。
また、transactional tasksはユーザーが指定した名前を持つことも出来ません。

datastore.RunInTransaction(c, func(c appengine.Context) error {
    t := &taskqueue.Task{Path: "/path/to/worker"}
    err := taskqueue.Add(c, t, "")
    if err != nil {
        return err
    }
    // ...
}, nil)

App EngineでのGo言語によるトランザクションの基本的な使い方などについてでした。

トランザクションを試している際に個人的にちょっと躓いた部分がありました。
親無しのエンティティを大量に用意してPutMultiで格納しようとしてエラーになったことです。

err := datastore.RunInTransaction(c, func(c appengine.Context) error {
    _, err := datastore.PutMulti(c, keys, entities)
    return err
}, &datastore.TransactionOptions{XG: true})

上記のコードを実行すると次のようなエラーが出力されました。 ERROR: Transaction failed: API error 1 (datastore_v3: BAD_REQUEST): operating on too many entity groups in a single transaction.

親を持たないエンティティはグループに属さないので関係無いと思っていたら、それぞれが独立したグループなんですねこれ。
勘違いしてました。

次はApp Engine DatastoreのGo言語環境で特有の箇所があれば、見てゆきたいと思います。