- ディープダイブ
- 初心者向けガイド
- Git の新機能の開発
- Gitaly関連のテストの失敗
- レガシーRuggedコード
TooManyInvocationsError
エラー- リクエスト・カウント
- ローカルで変更されたバージョンのGitalyを使ったテストの実行
- 機能フラグによるRPCのラッピング
- テストでの Praefect の使用
- Gitalyが使用するGitリファレンス
Gitaly開発ガイドライン
GitalyはGitLab Rails、Workhorse、GitLab Shellで使用される高レベルのGit RPCサービスです。
ディープダイブ
2019年5月、Bob Van LanduytはGitalyプロジェクトに関するDeep Dive(GitLabチームメンバー限定:https://gitlab.com/gitlab-org/create-stage/-/issues/1
)を開催しました。このディープダイブでは、Ruby開発者としてGitalyプロジェクトに貢献する方法や、将来Gitalyプロジェクトで働く可能性のある人たちとドメイン固有の知識を共有しました。
このページは 録画はYouTubeで、スライドはGoogleスライドと PDFでご覧いただけます。
このディープダイブで扱った内容はすべて GitLab 11.11 時点でのもので、具体的な内容は変更されているかもしれませんが、それでも入門編としては十分なものです。
初心者向けガイド
GitalyリポジトリのGitaly貢献のための初心者ガイドを読むことから始めましょう。Gitalyのセットアップ方法、Gitalyのさまざまなコンポーネントとその機能、テストスイートの実行方法について説明されています。
Git の新機能の開発
Gitのデータを読み書きするには、Gitalyにリクエストする必要があります。つまり、lib/gitlab/git
でまだ利用できないデータが必要な新機能を開発する場合は、Gitaly に変更を加えなければなりません。
gitlab
リポジトリのどこにも、ディスクアクセス経由で Git リポジトリに触れるような新しいコード(たとえば、Rugged、git
、rm -rf
)があってはなりません。Gitリポジトリに直接アクセスする必要があるものはすべてGitalyで実装し、RPCを介して公開しなければなりません。
Gitalyで新機能を開発する場合、その新機能を使おうとするGitLabへの変更を別のマージリクエストにし、Gitalyのマージ直後にマージする方が簡単なことがよくあります。こうすることで、マージされる前に変更をテストすることができます。
- 変更したバージョンのGitalyでGitLabのテストを実行する手順は以下をご覧ください。
- GDK で
gdk install
を実行し、gdk restart
を使って GDK を再起動すると、ローカルで変更した Gitaly バージョンを開発に使うことができます。
Gitaly関連のテストの失敗
Gitalyの問題でテスト・スイートが失敗する場合、最初のステップとして、実行してみてください:
rm -rf tmp/tests/gitaly
RSpecテスト中、Gitalyインスタンスはgitlab/log/gitaly-test.log
にログを書き込みます。
レガシーRuggedコード
GitalyはすべてのGitアクセスを処理できますが、GitLabの顧客の多くはまだNFSの上でGitalyを実行しています。Git呼び出しのためのレガシーなRugged実装は、N+1 Gitaly呼び出しやその他の理由により、Gitaly RPCよりも高速かもしれません。詳細はイシューをご覧ください。
GitLabがこれらの非効率性のほとんどを取り除くか、Gitデータに対するNFSの使用が廃止されるまで、最もよく使われるRPCのいくつかのRugged実装は機能フラグによって有効にすることができます:
rugged_find_commit
rugged_get_tree_entries
rugged_tree_entry
rugged_commit_is_ancestor
rugged_commit_tree_entry
rugged_list_commits_by_oid
便利な Rake タスクを使えば、これらのフラグを一括して有効にも無効にもできます。有効にするには
bundle exec rake gitlab:features:enable_rugged
無効にするには
bundle exec rake gitlab:features:disable_rugged
このコードのほとんどはlib/gitlab/git/rugged_impl
ディレクトリにあります。
TooManyInvocationsError
エラー
開発中やテスト中に、Gitlab::GitalyClient::TooManyInvocationsError
エラーが発生することがあります。GitalyClient
では、1回のRailsリクエストまたはSidekiq実行でGitalyが30回以上呼び出された場合にこのエラーを発生させることで、潜在的なn+1問題に対するブロックを試みています。
一時的な対策として、GITALY_DISABLE_REQUEST_LIMITS=1
をエクスポートしてエラーを抑制してください。これにより、開発者環境でのn+1検出が無効になります。
GitLab CEまたはEEリポジトリでイシューを作成して、問題を報告してください。Gitaly ~performance ~"technical debt "というラベルを含めてください。イシューには、TooManyInvocationsError
の完全なスタックトレースとエラーメッセージが含まれていることを確認してください。 また、可能であれば、失敗した既知のテストも含めてください。
n+1 問題の原因を特定してください。これは通常、配列の各要素に対して Gitaly が呼び出されるループです。問題の切り分けができない場合は、Gitalyチームのメンバーまでご連絡ください。
ソースが見つかったら、次のようにallow_n_plus_1_calls
ブロックで囲みます:
# n+1: link to n+1 issue
Gitlab::GitalyClient.allow_n_plus_1_calls do
# original code
commits.each { |commit| ... }
end
コードがこのブロックにラップされた後、このコード・パスは n+1 検出から除外されます。
リクエスト・カウント
コミットやその他のGitデータは、Gitalyを通してフェッチされます。これらのフェッチは、データベースと同じようにバッチ処理することができます。これにより、クライアントやGitaly自体のパフォーマンスが向上し、ユーザーにとっても良いものとなります。パフォーマンスを安定させ、パフォーマンスの後退を防ぐために、Gitalyのコールをカウントし、コールカウントをテストすることができます。これには、:request_store
フラグを設定する必要があります。
describe 'Gitaly Request count tests' do
context 'when the request store is activated', :request_store do
it 'correctly counts the gitaly requests made' do
expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10)
end
end
end
ローカルで変更されたバージョンのGitalyを使ったテストの実行
通常、GitLab CE/EEテストでは、GITALY_SERVER_VERSION
. GITALY_SERVER_VERSION
NETファイルで指定したバージョンで固定されたtmp/tests/gitaly
、Gitalyのローカルクローンを使用します。GITALY_SERVER_VERSION
この GITALY_SERVER_VERSION
ファイルは、リポジトリのカスタムコミットを使うためのブランチやSHAもサポートしています。
GITALY_SERVER_VERSION
のフォーマットは Omnibus の構文に合わせられました。=revision
はサポートされなくなり、Git参照(ブランチやSHA)としてファイルの内容を評価します。セマンティックバージョンにマッチした場合のみ、v
を付加します。変更されたバージョンのGitalyに対してローカルでテストを実行したい場合は、tmp/tests/gitaly
をシンボリックリンクに置き換えることができます。これは、rspec
を実行するたびに Gitaly を再インストールする必要がないため、より高速です。
このディレクトリにconfig.toml
とpraefect.config.toml
のファイルがあることを確認してください。config.toml.example
からconfig.toml
を、config.praefect.toml.example
からpraefect.config.toml
をコピーすることができます。コピー後、正しいパスを指すように編集してください。
rm -rf tmp/tests/gitaly
ln -s /path/to/gitaly tmp/tests/gitaly
テストを実行する前に、必ずmake
を Gitaly の内部ディレクトリで実行してください。そうしないと、Gitalyの起動に失敗します。
テストを実行する間にローカルのGitalyに変更を加えた場合は、手動でmake
を再度実行する必要があります。
CIテストは、ローカルで変更したGitalyのバージョンを使用しないことに注意してください。CIでカスタムGitalyバージョンを使用するには、このセクションの冒頭で説明したように、GITALY_SERVER_VERSION
を更新する必要があります。
あなたの変更がフォーク上に存在する場合など、別のGitalyリポジトリを使用するには、テストを実行するときにGITALY_REPO_URL
環境変数を指定することができます:
GITALY_REPO_URL=https://gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb
Gitalyのフォークが非公開の場合、デプロイトークンを生成してURLに指定することができます:
GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb
CI/CDでGitalyのカスタムリポジトリを使用するには、例えばGitLabフォークが常にあなた自身のGitalyフォークを使用するようにしたい場合、CI/CD変数として GITALY_REPO_URL
。
Gitaly RPCクライアントのローカルで修正したバージョンを使用します。
新しいエンドポイントを追加したり、既存のエンドポイントに新しいパラメータを追加するなど、RPCクライアントに変更を加える場合は、Gitaly protobuf仕様のガイドに従ってください。次に
- Gitalyの
tools/protogem
ディレクトリでbundle install
。 -
Gitalyのルート・ディレクトリからRPCクライアントgemをビルドします:
BUILD_GEM_OPTIONS=--skip-verify-tag make build-proto-gem
-
Gitalyの
_build
ディレクトリで、新しく作成した.gem
ファイルを解凍し、gemspec
:gem unpack gitaly.gem && gem spec gitaly.gem > gitaly/gitaly.gemspec
-
Railsの
Gemfile
のgitaly
行を以下のように変更します:gem 'gitaly', path: '../gitaly/_build'
- 変更したRPCクライアントを使用するには、
bundle install
。
新しい変更を試したい場合は、ステップ 2 ~ 5 を毎回再実行してください。
機能フラグによるRPCのラッピング
機能フラグの後ろにGitalyの新機能をゲートする手順は以下の通りです。
Gitaly
-
パッケージ・スコープのフラグ名を作成します:
var findAllTagsFeatureFlag = "go-find-all-tags"
-
featureflag
パッケージを使用して、コード内にスイッチを作成します:if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) { // go implementation } else { // ruby implementation }
-
Prometheus メトリクスを作成します:
var findAllTagsRequests = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "gitaly_find_all_tags_requests_total", Help: "Counter of go vs ruby implementation of FindAllTags", }, []string{"implementation"}, ) func init() { prometheus.Register(findAllTagsRequests) } if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) { findAllTagsRequests.WithLabelValues("go").Inc() // go implementation } else { findAllTagsRequests.WithLabelValues("ruby").Inc() // ruby implementation }
-
テストにヘッダを設定します:
import ( "google.golang.org/grpc/metadata" "gitlab.com/gitlab-org/gitaly/internal/featureflag" ) //... md := metadata.New(map[string]string{featureflag.HeaderKey(findAllTagsFeatureFlag): "true"}) ctx = metadata.NewOutgoingContext(context.Background(), md) c, err = client.FindAllTags(ctx, rpcRequest) require.NoError(t, err)
GitLab Rails
機能フラグを設定してRailsコンソールでテストします:
Feature.enable('gitaly_go_find_all_tags')
フラグの名前とRailsコンソールで使われるフラグに注意してください。両者には違いがあります (ダッシュがアンダースコアに置き換えられ、名前のプレフィックスが変更されています)。すべてのフラグのプレフィックスは必ずgitaly_
にしてください。
GDKでのテスト
フラグが正しく設定され、Gitalyに入ることを確認するために、GDKを使用してインテグレーションをチェックすることができます:
- フラグの状態は観測可能でなければなりません。これを確認するには、Prometheusのメトリクスを取得することで有効にする必要があります:
- GDKのルートディレクトリに移動します。
- Gitaly用に適切なブランチがチェックアウトされていることを確認してください。
-
make gitaly-setup
で再コンパイルし、gdk restart gitaly
でサービスを再起動します。 - セットアップが実行されていることを確認してください:
gdk status | grep praefect
. - どの設定ファイルが使用されているか確認してください:
cat ./services/praefect/run | grep praefect
-config
フラグの値 - 設定ファイルの
prometheus_listen_addr
のコメントを解除し、gdk restart gitaly
を実行します。
- フラグがまだ有効になっていないことを確認してください:
- プロジェクトの作成、コミットの送信、履歴の参照など、変更をトリガーするために必要なアクションを実行してください。
-
現在のメトリクスのリストに、機能フラグの新しいカウンターがあることを確認します:
curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
- 新しい機能フラグのメトリクスがインクリメントされたら、新しい機能を有効にできます:
- GDKのルートディレクトリに移動します。
-
Railsコンソールを起動します:
bundle install && bundle exec rails console
-
機能フラグのリストを確認します:
Feature::Gitaly.server_feature_flags
無効にする必要があります
"gitaly-feature-go-find-all-tags"=>"false"
。 -
有効にしてください:
Feature.enable('gitaly_go_find_all_tags')
- Railsコンソールを終了し、プロジェクトの作成、コミットの送信、履歴の参照など、変更をトリガするために必要なアクションを実行します。
-
メトリクスを観察して、その機能がオンになっていることを確認します:
curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
テストでの Praefect の使用
テストでの Praefect のデフォルトでは、メモリ内選出ストラテジーを使用します。このストラテジーは非推奨であり、本番環境では使用されません。主に、単体テスト用に使用されます。
より現代的な選挙戦略では、PostgreSQLデータベースとの接続が必要です。テストを実行する際にはこの挙動はデフォルトでは無効になっていますが、GITALY_PRAEFECT_WITH_DB=1
を環境に設定することで有効にすることができます。
そのためには、PostgreSQLを起動し、データベースを作成しておく必要があります。GDK を使用している場合は、次のようにして設定します:
- データベースを起動します:
gdk start db
- GDKから環境を読み込みます:
eval $(cd ../gitaly && gdk env)
- データベースを作成します:
createdb --encoding=UTF8 --locale=C --echo praefect_test
Gitalyが使用するGitリファレンス
GitalyはGitLabにGitサービスを提供するために多くのGitリファレンス(refs)を使用しています。
標準的なGitリファレンス
これらの標準Gitリファレンスは、GitLabが(Gitalyを通して)あらゆるGitリポジトリで使用します:
-
refs/heads/
.ブランチに使われます。git branch
ドキュメントを参照してください。 -
refs/tags/
.タグに使用。git tag
のドキュメントを参照してください。
GitLab固有のリファレンス
これらのGitLab固有のリファレンスは、GitLabによって(Gitalyを通して)独占的に使用されています:
-
refs/keep-around/<object-id>
.パイプラインジョブやマージリクエストがあるコミットへの参照。object-id
は、パイプラインが実行されたコミットを指します。 -
refs/merge-requests/<merge-request-iid>/
.マージは2つのヒストリをマージします。このref名前空間は、その下の以下のrefを使用してマージに関する情報を追跡します:-
head
.マージリクエストの現在のHEAD
。 -
merge
.マージリクエストのコミット。すべてのマージリクエストはrefs/keep-around
の下にコミットオブジェクトを作成します。 -
マージトレインが有効な場合:
train
.マージトレインをコミットします。
-
-
refs/pipelines/<pipeline-iid>
.パイプラインへの参照。パイプラインのコミットオブジェクトIDを格納するために一時的に使用されます。 -
refs/environments/<environment-slug>
.環境へのデプロイが行われたコミットへの参照。 -
refs/heads/revert-<source-commit-short-object-id>
.変更を戻したときに作成されたコミットのオブジェクトIDへの参照。