0時に寝て何度か起きて7時に起きた。いつもなら日曜日は徹夜して翌日の早朝に出掛けるのが月曜日は行かなくて済むのでちょっと楽になった。

amqp091-go の context 制御

goroutine リークを検出するツールに uber-go/goleak がある。ずっと前から余裕のあるときに結合テストの導入しようという issue を作っていたものの、適当なタイミングがなかった。先週末に少し手が空いたので着手した。goleak は個別のテストメソッドにも TestMain にも両方に対応している。結合テストの TestMain に入れた方が保守コストが下がるのでそういった用途がよいのではないかと思う。

go の TestMain がこういうものかもしれないが、defer 文を使う終了処理があるとそのコードを直接 TestMain には実装できない。関数で wrap して m.Run() を実行した結果を返すようにしないといけない。そこに goleak を入れる場合、goleak.Cleanup を何もしない関数に置き換えて m.Run() の結果を返せばよいのではないかと思う。そして VerifyTestMain() は m.Run() を実行してからすぐに goroutine が動いていないかをチェックする。ここで結合テストを動かすための、環境構築のために http サーバーを goroutine で起動するとか、テストのための goroutine が動いているとそれも検出してしまうのでそれらの goroutine は無視できるよう、2つのオプションが用意されている。

  • IgnoreTopFunction: 明示的に無視してよい goroutine のトップ関数を指定する
  • IgnoreCurrent: オプションを登録した時点で稼働している goroutine を無視する

これらを踏まえて TestMain で goleak を使うと次のようなコードになった。しかし、おそらくこの使い方はあまりよくない。いくつか goroutine を無視する設定を追加したために、そこに意図しない goroutine リークが隠蔽されてしまう懸念がある。

func main(m *testing.M) int {
    defer myTearDown()

	var code int
	goleak.VerifyTestMain(
		m,
		goleak.Cleanup(func(exitCode int) {
			fmt.Println("skip goleak cleanup", exitCode)
			code = exitCode
		}),
		goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop"),
		goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"),
		goleak.IgnoreTopFunction("internal/poll.runtime_pollWait"),
		goleak.IgnoreCurrent(),
	)
	return code
}

func TestMain(m *testing.M) {
	os.Exit(main(m))
}

さらにこの調査をしているときに amqp091-go の api も context 受け取った方がシンプルでいいんじゃない?と思って提案の pr を送ってみた。context 使わなくても自前でキャンセルする api は提供されているため、開発者の考え方によってこの提案を拒否するのも妥当な判断だと思える。次のメジャーバージョンとか、互換性を維持しなくてよいタイミングから取り入れようという考え方もあるかもしれない。