$shibayu36->blog;

$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

golangのcontextのcancel伝播の仕組みを学ぶために自作してみた

並行プログラミングを学ぶ一環で、「Contextを完全に理解する」というテーマでGo Conference 2021 Autumnに登壇しました の記事を見つけ、contextのcancel伝播の実装方法が気になった。そこで自分でcontextのcancel部分だけを自作することで伝播の理解を深めてみた。

実装はこちらで、context.WithCancel的なものとcontext.WithDeadline的なものを実装している。またテストコードも用意している。

実装してみて面白いなと思ったのは、contextの実装は可能な限りgoroutineを起動しないようにうまく実装されているということ。

自作する前にgolangのcontextの実装を眺めていたが、最初はこの辺りを見て、子の側でgoroutineを起動して親のDone()を見ているのだなと思い込んでいた。しかし実際には、

  • 親となるcontextが全ての子を管理して親から子にcancelを伝えていくパターンである場合は、goroutineを起動せずに親に任せるというやり方を取っている。これにより無駄なgoroutineを起動する必要がない
    • この辺りでparentCancelCtxを見たり、afterFuncerかを見たりして、早期returnすることでgoroutineを起動しないようになっている
  • 親がそのようなパターンではない場合は、goroutineを起動して待つようにする。これによって独自にContext interfaceを実装しているケースでもうまくcancelを伝播させることができる

ということを知った。

なるほどと思ったので、cancel部分の自作バージョンでも以下のようにgoroutineを起動しないような仕組みになるように実装してみた。

標準のcontextはよく出来ていると実感した。また理解するには自作してみるのが一番深掘りできて良い。

リアルタイムに2次元位置を同期するサーバーのe2eテストを作った

clusterのリアルタイム通信サーバーの漸進的な進化のような仕組みを理解したいなと思い、手習い用にMQTT+Protocol Buffersを使ってリアルタイムに2次元位置を同期するサーバーを書いている。今回はリアルタイムに2次元位置を同期するサーバーでプレイヤーから弾を発射できるようにの続きで、サーバーが一定動いているか確かめるためのe2eテストを作ってみる。これまでの様子はこちら

作戦

まずe2eテストはメンテナンス性が悪い傾向にあるので、最低限必要なものだけにする。細かいロジックについてはcontrollerのテストや、gameロジックのテストでテストできているわけなので、e2eテストは次のような検証を行うものとした。

  • サーバーを起動でき、クライアントが接続できる
  • プレイヤーが移動したら全プレイヤーに通知される
  • アイテムの状態更新が全プレイヤーに通知される

実装

上記を確認するために、簡易的なテスト用クライアントを作成し、実際にサーバーを立ち上げてテストクライアントから接続&送信を行なってテストする。

具体的な実装は https://github.com/shibayu36/terminal-shooter/pull/16/files のようになった。。TestClientは接続を行い、serverからのPublishを受け取ったらメモリ上にメッセージを保持する。そしてサーバー起動後にTestClientを接続し、何らかの情報送信を行った後TestClientがメモリ上に保持したメッセージを検証する。

まとめ

今回はリアルタイムに2次元位置を同期するサーバーのe2eテストを作ってみた。実際にgoroutineでサーバーを立ち上げてみてテストクライアントで接続するだけで簡単にe2eテストが出来て良い。

git grepで除外パスを指定しやすくする

git grepにはさまざまな便利グッズがあるのだけど、どうやっても覚えられなくて困っていた。たまに使いたいものとして、特定ファイルは除外する方法があるが、この記法が覚えられない...

例えば、golangでテストファイルとgen/ディレクトリ以下にあるもの以外を検索したいなら、こういう書き方ができる。先頭のコロンも覚えられないし、!も覚えられない...

git grep hoge ':!*_test.go' ':!gen/'
# もしくは
git grep hoge ':(exclude)*_test.go' ':(exclude)gen/'

そういうことで自分が使いやすいようにgit grepをカスタマイズしてみた。 自分だったら --exclude *_test.go と書けるなら覚えられそうだなということで、こういう感じに。

git-grep-extend

#!/usr/bin/env bash

# git grepをさらに使いやすくするツール
#
# git-grep-extend:
#   Usage例:
#   git-grep-extend -i "fuga" --exclude "docs/" --exclude "vendor/"

# 検索パターンやオプション
grep_opts=()
# 除外パスを格納
exclude_opts=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --exclude)
      # 除外パスを指定できるオプション
      exclude_opts+=( ":(exclude)$2" )
      shift 2
      ;;
    *)
      # grepに渡す他のパラメータ(検索パターンやオプション)を格納
      grep_opts+=( "$1" )
      shift
      ;;
  esac
done

git grep "${grep_opts[@]}" "${exclude_opts[@]}"

これで例えば https://github.com/shibayu36/terminal-shooter からこんな検索ができて便利になった。

# case insensitive / bulletにマッチするが_bulletにはマッチしない / *_test.go以外のファイル / serverディレクトリ以下から検索
git grep-extend -i -e bullet --and --not -e '_bullet' --exclude '*_test.go' server