今日のバドミントン練習はエアシャトルでリフティングを60分した。連続最大回数は191回だった。もう少しで200回だったのに残念。木曜日は睡眠をたくさんとって疲れは少し取れたし、安定的に50回前後は続くようになりつつも、100回までに失敗してしまう。今日は100回を超えたのが2回だけだった。ラケットのスィートスポットでとらえたときにきれいに真上にあがる感覚が楽しい。うまくいくときは数回は続く。それが自然にできるときとそうじゃないときの違いを私は制御できてなくて言語化もできない。

エアシャトルとメイビスにおけるリフティングの違いを比べてみると、メイビスの方が打ち上げて落ちてくるときにあまり回転せずコルクが下を向く傾向が多いようにみえる。エアシャトルの方がコルクが重い分、縦方向に回転し始めるとその回転が止まらず、回転しているからラケット面でとらえるのが難しくなる。だからエアシャトルの方がメイビスよりもリフティングが難しいといえる。シャトルを高く打ち上げると、落下してくる距離が長くなりその回転が落ち着く傾向があるからリフティングしやすくなるのではないかと仮説を考えた。伸び悩みかもしれないし、地道に練習を継続するときかもしれない。

exec とエントリーポイントのスクリプト

コンテナを起動して stop すると SIGTERM が送られる。そのときに api サーバーでシグナルの処理をしているのに、気付いたらシグナル処理が行われずタイムアウトするようになっていた。デフォルトでは10秒でタイムアウトして強制終了となる。なぜシグナルを捕捉しなくなったかを調査したら、あるときサーバーの起動前に前処理が必要になってエントリーポイントをシェルスクリプトにしていた。そのときに exec しないと、シェルスクリプトのプロセスに対してシグナルが送られるため、api サーバーがシグナルを検知できなくなるという副作用があることに気付いた。これまでも exec を使うとプロセス ID は変更されないという知識を知っていたが、それがどういう状況で役に立つかを理解できていなかった。シグナルを用いた同期処理に exec が役に立つ状況があることを学んだ。修正は次の1行のみ。

--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -2,4 +2,4 @@
 ...
 ... (pre process)
 ...
-./bin/api "$@"
+exec ./bin/api "$@"

go test からバイナリをビルドしてサーバーを起動する

先日 結合テスト向けカバレッジ計測の調査 をした成果を使って実際に go test からカバレッジ計測のカスタマイズを施したバイナリをビルドしてサーバー起動するコードを書いてみた。やや手間取ったが、一通り動いてカバレッジを計測できた。例えば、単体テストのカバレッジを計測するための makefile のターゲットは次のようになる。

GO_COVER_DIR:=$(CURDIR)/tests/coverage

coverage:
	@mkdir -p $(GO_COVER_DIR)
	go test -tags=integration -race -cover ./... -covermode atomic -args -test.gocoverdir=$(GO_COVER_DIR)

go は fork ができない。fork の代わりに exec を使う。How do I fork a go process? に go の goroutine のスケジューリングと fork は相性が悪くてうまく動かないということが背景だと説明されている。それはともかく exec を使ってもサーバープロセスを非同期に起動できたのでそのスニペットを書いておく。

binaryPath, err := buildBinary()
if err != nil {
	return 1
}
args := []string{
	"-verbose",
	"-port",
	strconv.Itoa(ServerPort),
}

r, w := io.Pipe()
go func() {
	s := bufio.NewScanner(r)
	for s.Scan() {
		fmt.Println(s.Text())
	}
}()

cmd := exec.Command(binaryPath, args...)
cmd.Stdout = w
if err := cmd.Start(); err != nil {
	slog.Error("failed to start api server", "err", err)
	return 1
}
defer func() {
	if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
		slog.Error("failed to terminate the api process", "err", err)
	}
	if s, err := cmd.Process.Wait(); err != nil {
		slog.Error("failed to wait terminating the api process", "err", err)
	} else {
        w.Close()
		slog.Info("completed to terminate the api process", "s", s.String())
	}
}()

// サーバーに対するテストを実行

サーバープロセスの標準出力のログを io.Pipe を使って出力することもできる。exec で生成したプロセスに対してもシグナルを送ったり終了を待つこともできる。デバッグしている分にはこれで意図したように制御できた。この知見は将来的に役に立つ気がする。0時過ぎから調査を再開して4時前ぐらいまでやっていた。少しはまって時間はかかったものの、久しぶりに集中してデバッグしていた。