Humanity

Edit the world by your favorite way

panic() するか non-nil error が返ったらロールバック用の処理を実行するユーティリティ構造体 Guard を作った

こんな感じに使う。

package main

import (
    "errors"
    "fmt"
)

func main() {
    fmt.Println("start")
    if err := f(); err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println("end")
}

func f() (err error) {
    fmt.Println("f() start")
    g := &Guard{}
    defer func() { err = g.Rollback(recover()) }()

    g.Add(func() {
        fmt.Println("rollback 1")
    })
    // if true {
    //     panic("panic!")
    // }
    g.Add(func() {
        fmt.Println("rollback 2")
    })

    // oops, invoke all rollback functions in reverse order...
    e := errors.New("something went wrong")
    if e != nil {
        err = g.Rollback(e)
        err = g.Rollback(e) // this won't call rollback functions twice!
        return
    }

    fmt.Println("f() end")
    return
}

// Guard invokes "rollback functions" if Rollback method received non-nil value
// (e.g. recover(), non-nil error).
//
// TODO: use mutex or something if the methods may be invoked in parallel!
type Guard struct {
    // rollback functions
    rbFuncs []func()
}

// Rollback rolls back if v is non-nil.
//
//   defer func() { err = g.Rollback(recover()) }()
//
//   // or
//
//   if e != nil {
//     err = g.Rollback(e)
//     err = g.Rollback(e) // this won't call rollback functions twice!
//     return
//   }
func (g *Guard) Rollback(v interface{}) error {
    var err error
    if e, ok := v.(error); ok {
        err = e
    } else if v != nil {
        err = fmt.Errorf("%s", v)
    }
    if err != nil {
        g.RollbackForcefully()
    }
    return err
}

// RollbackForcefully calls rollback functions in reverse order.
func (g *Guard) RollbackForcefully() {
    // Call rollback functions in reverse order
    for i := len(g.rbFuncs) - 1; i >= 0; i-- {
        g.rbFuncs[i]()
    }
    // Do not rollback twice
    g.rbFuncs = nil
}

// Add adds given rollback functions.
func (g *Guard) Add(f func()) {
    g.rbFuncs = append(g.rbFuncs, f)
}

そんなに行数無いので管理面倒になるしリポジトリは作ってない。 とりあえず Volt でちょうどこんな interface を実装した struct が沢山あって (インタプリタ的な DSL を実装中で、Value は全てのベースとなる型)

type Op interface {
  // ...

  Execute(ctx context.Context, args []Value) (ret Value, rollback func(), err error)
}

いちいちロールバック用の関数のスライス作って error や panic() の処理して… ってするのはかなり面倒になりそうだったので、構造体にまとめた。 本来の処理に注力できて良い感じに関心の分離ができてる感じ。

ちなみに Volt ではちょっと変更したバージョンを使ってて、 Rollback() で error を返す所を pkg/errorsWrap() 使うように変えてる。 エラーメッセージが分かりやすくなっていい感じになった (はず…確認できてないけど)。

Go の hashicorp/go-multierror 使ったら訳わからん挙動に遭遇してしまった

基本的な Go の挙動が理解できてないのかもしれないですが、なぜ以下のコードで (2) err != nil: true となるのかが分かりません。 誰か教えて… (g() がメソッドなのはなるべく元のコードと似せたせいです)

gist.github.com

追記

KoRoN さんとりんだんさんに教えてもらいました。

というわけで教えてもらった記事の最初に「Goのinterfaceの実体は参照と型情報のペア」と書いてありました。 Go 公式の FAQ にも書いてあります。 こっちのが私のコードより問題と解決策が一目で分かりやすかったので皆さんもハマる前にぜひ。

MongoDB で190万件あるコレクションに $regex で LIKE 検索する時にパフォーマンスの観点で気を付けること

控えめに150万件以上と言ったけど実際は190万件ぐらいだった。 以下の全てのクエリは 0 件を返す。 というか今見たら $and 使う必要はなかった。

// 89790ms
db.getCollection('colName').find({$and: [{name: /(あああ)/}, {status: 1}]})
// 1361ms
db.getCollection('colName').find({$and: [{name: /^(あああ)/}, {status: 1}]}).hint({name: 1})
// 4ms
db.getCollection('colName').find({$and: [{name: /^あああ/}, {status: 1}]}).hint({name: 1})
// 107386ms
db.getCollection('colName').find({$and: [{name: /^(あああ)/}, {status: 1}]})
// 86117ms
db.getCollection('colName').find({$and: [{name: /^あああ/}, {status: 1}]})

/(あああ)/ よりヒントなしの /^(あああ)/ が遅いのが謎。 ちなみに文字列検索した時は普通にインデックスが効くっぽいのですぐ返ってくる。

// 15ms
db.getCollection('colName').find({$and: [{name: "あああ"}, {status: 1}]})

公式ドキュメント

$regexIndex Use の項参照。

  • case insensitive なパターンを使うこと
  • ^\A で始めること
  • 先頭からのマッチの長さをなるべく短くする
    • 例えば /^a/, /^a.*/, /^a.*$/ だったら /^a/ が一番速い
    • [私見] 安易に .* とか書かずにどうしても必要なら non-greedy match にするといいかも (.*?)

あとドキュメントに書いてないけど冒頭の結果から分かるのは

  • () を使わない

もですかね。

TODO

$explain した結果も貼る。