お仕事は昨日の続きでリクエスト途中のサービス終了したときに web api ハンドラーの処理が途中でキャンセルされ、そのために内部のデータが不整合になることがわかった。データを不整合にしないため mongodb のトランザクション の仕組みを調べてデバッグしたりしていた。echo のミドルウェアで Unit of work を実装できないかをプロトタイプ実装していた。
基本は次のように callback 関数を渡せばトランザクションに失敗したときにリトライもしてくれて便利なのだけど、いろいろ api サーバーの既存アーキテクチャの都合もあってこれは使えないことがわかった。
午後から LT大会 with カメラマン に参加してきた。副業でカメラマンをやろうとしている方が LT 中にプロフィール写真になるようなものを撮ってくれるという。会社紹介などのスライドで使えるかもしれないと思って参加してきた。趣味で4-5年カメラを勉強しているというだけあって、機材や知識も備えていてよかったと思う。LT のネタとして先月お仕事でやっていた go のテスト改善の一環でカバレッジについての紹介をしてきた。サンプルコードは次になる。
先日 深夜にバイナリのビルド・起動調査 をしたのに、最終的にはそんなことしなくてもよかった。デフォルトではパッケージ外のディレクトリにあるテストのカバレッジを計測しないことから普通にはできないと考えていた。しかし、調べたら次の SO で解決法をみつけた。結論としては -coverpkg ./... のように go test で実行している結合テストに対してオプションを指定するだけでよかった。
res, err :=doHTTPRequest(body, UserPath, http.MethodGet,"")if res.StatusCode != http.StatusOK {
t.Errorf("expected to get %d, but got %d", http.StatusOK, res.StatusCode)return}var actual entry.User
if err :=convertBody(res,&actual); err !=nil{
t.Errorf("failed to convert: %s", err)return}
このバイナリを使って起動した api server に対してリクエストを呼び出すことでカバレッジを計測してくれる。結合テストからバイナリ起動した api server に対してリクエストしたときに GOCOVERDIR に中間データが追加されていく。結合テストを完了したら最終的なカバレッジの統計情報を生成する。

$ go tool covdata textfmt -i=./tests/coverage -o coverage.out
textfmt のヘルプをみたら同じディレクトリじゃなくてもよいみたい。
$ go tool covdata textfmt -help
...
Examples:
go tool covdata textfmt -i=dir1,dir2 -o=out.txt
merges data from input directories dir1+dir2
and emits text format into file 'out.txt'
$ go test ./tests/...
# example.com/tsets/mypackage
package example.com/tsets/mypackage_test
imports example.com/tests/util: build constraints exclude all Go files in path/to/tests/util
FAIL example.com/tests/mypackage [setup failed]
type Controller struct{
ctx context.Context
cancel context.CancelFunc
wg *sync.WaitGroup
mu sync.Mutex
}func(c *Controller)Register()(context.Context,*sync.WaitGroup){
c.mu.Lock()defer c.mu.Unlock()
c.wg.Add(1)
ctx := context.WithValue(c.ctx, ctxKey{},0)return ctx, c.wg
}func(c *Controller)Stop(){
c.cancel()}func(c *Controller)Wait()<-chanstruct{}{
c.mu.Lock()defer c.mu.Unlock()
quit :=make(chanstruct{},1)gofunc(){
c.wg.Wait()
slog.Debug("completed to wait group in batch.Controller")
quit <-struct{}{}}()return quit
}funcNewController(parent context.Context)*Controller {
ctx, cancel := context.WithCancel(parent)return&Controller{
ctx: ctx,
cancel: cancel,
wg:new(sync.WaitGroup),}}
parent の context でキャンセルさせることもできるし、この controller の api からキャンセルさせることもできる。
この controller を使うアプリケーションのコードは次のようになる。
c := batch.NewController(ctx)func(){
ctx, wg := c.Register()defer wg.Done()for{// do somethingselect{case<-ctx.Done():
slog.Debug("canceled","err", ctx.Err())returndefault:}}}()
timer := time.NewTimer(10* time.Second)select{case<-timer.C:
slog.Debug("expected to complete waiting, but occur timeout")case<-c.Wait():
slog.Debug("completed to wait")}