LoginSignup
3
2

More than 3 years have passed since last update.

GCPのSpannerで allow_commit_timestamp = true のフィールドに現在時刻を入れてはいけない

Posted at

はじめに

Spannerはスキーマに他のデータベース同様TIMESTAMP型がありますがオプションとして allow_commit_timestamp = true を指定でき、Spanner側の時計を基準としたタイムスタンプを打つことができます。

これによりアプリケーション側でセットした時刻に依存しない厳格な履歴を作成できますが使い方を間違えると書き込みに失敗するケースがあったので紹介します。

TL;DR

  • allow_commit_timestamp = true にしたフィールドに未来時刻を入れることはできない
  • このフィールドにOSから取得した現在時刻を入れると時刻ブレで未来時刻になり書き込みに失敗することがある
  • 代わりにプレースホルダーを使おう

検証

環境構築

以下のコマンドでSpannerを構築して下さい。gcloudの認証等は済んでいる前提でプロジェクト名は各々の環境に置き換えて下さい。

-- schema.sql として保存
CREATE TABLE users (
    id STRING(MAX) NOT NULL,
    created_at TIMESTAMP NOT NULL OPTIONS ( allow_commit_timestamp = true ),
) PRIMARY KEY (id);
$ gcloud --project myproject spanner instances create myinstance --config regional-asia-northeast1 --nodes 1 --description "myinstance"
Creating instance...done.
$ gcloud --project myproject spanner databases create mydatabase --instance myinstance
Creating database...done.
$ gcloud --project myproject spanner databases ddl update mydatabase --instance myinstance --ddl "`cat schema.sql`"
Schema updating...done.

よくない使い方

実際に未来の時刻を書き込んでみましょう。以下はGoでの例です。

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "cloud.google.com/go/spanner"
    "github.com/google/uuid"
)

type user struct {
    ID        string    `spanner:"id"`
    CreatedAt time.Time `spanner:"created_at"`
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run() error {
    ctx := context.Background()
    client, err := spanner.NewClient(ctx, "projects/myproject/instances/myinstance/databases/mydatabase")
    if err != nil {
        return err
    }
    defer client.Close()

    m, err := spanner.InsertStruct("users",
        &user{
            ID:        uuid.New().String(),
            CreatedAt: time.Now().Add(3 * time.Second), // 3秒未来の時刻を書き込む
        },
    )
    if err != nil {
        return err
    }

    t, err := client.Apply(ctx,
        []*spanner.Mutation{m},
    )
    if err != nil {
        return err
    }
    fmt.Println(t.UTC().Format(time.RFC3339Nano))

    return nil
}

CreatedAtのフィールドを見るとわかるように3秒未来の実行が指定されているのでFailedPreconditionが発生します。

今回は再現性を持たせるために手動で未来時刻を挿入しましたが、いつもの調子でうっかり time.Now() を入れるとコケたりコケなかったりといった厄介なバグが産まれます(サーバーの時刻SpannerのTrueTime となった時にだけコケる)。

$ go run main.go
spanner: code = "FailedPrecondition", desc = "Cannot write timestamps in the future 2021-01-21T15:29:08.542145Z > 2021-01-21T15:29:06.988814Z (current time) because the allow_commit_timestamp column option is set to true for column users.created_at, or for a corresponding shared key column in this table's interleaved table hierarchy."
exit status 1

正しい使い方

SpannerのCommitTimestampに合わせることを意味するプレースホルダーが用意されているのでそれを使いましょう。Goでは spanner.CommitTimestamp です。

先ほどのコードを以下のように修正して再度実行すると今度は正しく書き込みができ、且つAPIから返却されたCommitTimestampとCreatedAtに設定された時刻が正確に一致していることが確認できます。

- CreatedAt: time.Now().Add(3 * time.Second),
+ CreatedAt: spanner.CommitTimestamp,
$ go run main.go
2021-01-21T15:39:38.595873Z
$ gcloud --project myproject spanner databases execute-sql mydatabase --instance myinstance --sql "SELECT * FROM users"
id                                    created_at
a62b5ef1-730f-4ae1-991b-bd8ce14af1f7  2021-01-21T15:39:38.595873Z

参考文献

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2