- 前提条件
- コードの構造
- アーキテクチャ概要
-
ステージ
- 1.リポジトリインポートワーカー
- 2.ステージ::インポートリポジトリワーカー
- 3.ステージ::ImportBaseDataWorker
- 4.ステージ::ImportPullRequestsWorker
- 5.ステージ::インポートワーカー
- 6.ステージ::ImportPullRequestsMergedByWorker
- 7.Stage::ImportPullRequestsReviewRequestsWorker
- 8.Stage::ImportPullRequestsReviewsWorker
- 9.ステージ::ImportIssuesAndDiffNotesWorker
- 10.ステージ::ImportIssueEventsWorker
- 11.ステージ::ImportNotesWorker
- 12.ステージ::ImportAttachmentsWorker
- 13.ステージ::ImportProtectedBranchesWorker
- 14.ステージ::FinishImportWorker
- ステージの進行
- インポートジョブ ID のリフレッシュ
- GitHub のレート制限
- ユーザ・ルックアップのキャッシュ
- ラベルとマイルストーンのマッピング
- ログ
- メトリクス・ダッシュボード
GitHub インポート開発者向けドキュメント
GitHub インポーターには二種類のインポートがあります:
- 逐次インポート。
import:github
Rake タスクで使用します。 - Paralle インポーター。他のすべてのタスクで使用されます。
この2つのインポートの違いは
- 逐次インポーターはすべての作業をシングルスレッドで行うので、デバッグ目的やRakeタスクに向いています。
- 並列インポーターはSidekiqを使用します。
前提条件
-
github_importer
およびgithub_importer_advance_stage
キューを処理する Sidekiq ワーカー (デフォルトで有効)。 - Octokit (GitHub API とのやりとりに使用)。
コードの構造
インポーターのコードベースは以下のディレクトリに分かれています:
-
lib/gitlab/github_import
このディレクトリには、リソースのインポートに使用されるクラスなど、ほとんどのコードが含まれています。 -
app/workers/gitlab/github_import
このディレクトリにはSidekiqワーカーが含まれます。 -
app/workers/concerns/gitlab/github_import
: このディレクトリには、様々なSidekiqワーカーによって再利用されるいくつかのモジュールが含まれています。
アーキテクチャ概要
GitHub プロジェクトがインポートされると、他のインポーターと同様にRepositoryImportWorker
ワーカーのジョブをスケジュールして実行します。しかし、他のインポーターとは異なり、必要な作業をすぐに実行するわけではありません。代わりに、作業は個別のステージに分けられ、各ステージは実行されるSidekiqジョブのセットで構成されます。各ステージの間に、現在のステージの作業がすべて完了したかどうかを定期的にチェックするジョブがスケジュールされ、完了するとインポートプロセスを次のステージに進めます。これを処理するワーカーはGitlab::GithubImport::AdvanceStageWorker
と呼ばれます。
ステージ
1.リポジトリインポートワーカー
このワーカーは、次のワーカーにジョブをスケジューリングすることで、インポート処理を開始します。
2.ステージ::インポートリポジトリワーカー
このワーカーはリポジトリと Wiki をインポートし、完了したら次のステージをスケジューリングします。
3.ステージ::ImportBaseDataWorker
このワーカーはラベル、マイルストーン、リリースなどのベースデータをインポートします。この作業は並列に実行する必要がないほど高速に実行できるため、シングルスレッドで実行されます。
4.ステージ::ImportPullRequestsWorker
このワーカーはすべてのプルリクエストをインポートします。プルリクエストごとにGitlab::GithubImport::ImportPullRequestWorker
ワーカーのジョブがスケジュールされます。
5.ステージ::インポートワーカー
このワーカーは、外部の共同作業者でない直接のリポジトリ共同作業者のみをインポートします。すべてのコラボレータに対して、Gitlab::GithubImport::ImportCollaboratorWorker
ワーカーのジョブをスケジュールします。
Gitlab::GithubImport::Settings
によって制御されます)、デフォルトで選択されています。6.ステージ::ImportPullRequestsMergedByWorker
このワーカーはプルリクエストの_マージ済み_ユーザー情報をインポートします。このワーカーは プルリクエスト一覧API はこの情報を提供しません。そのため、このステージではマージされたプルリクエストを個別に取得してこの情報をインポートする必要があります。Gitlab::GithubImport::PullRequests::ImportMergedByWorker
ジョブが、取得したプルリクエストごとにスケジュールされます。
7.Stage::ImportPullRequestsReviewRequestsWorker
このワーカーはプルリクエストのレビュアー割り当てをインポートします。それぞれのプルリクエストに対して、このワーカーは
- 割り当てられたすべてのレビュアー リクエストを取得します。
- フェッチされた各レビュー要求に対して
Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker
ジョブをスケジュールします。
8.Stage::ImportPullRequestsReviewsWorker
このワーカーはプルリクエストのレビューをインポートします。それぞれのプルリクエストに対して、このワーカーは
- レビューの全ページを取得します。
- 取得したレビューごとに
Gitlab::GithubImport::PullRequests::ImportReviewWorker
ジョブをスケジュールします。
9.ステージ::ImportIssuesAndDiffNotesWorker
このワーカーはすべてのイシューとプルリクエストのコメントをインポートします。すべてのイシューに対して、Gitlab::GithubImport::ImportIssueWorker
ワーカーのジョブをスケジュールします。プルリクエストのコメントについては、代わりにGitlab::GithubImport::DiffNoteImporter
ワーカーのジョブをスケジュールします。
このワーカーはイシューと差分ノートを並行して処理するので、別のステージをスケジュールして前のステージが完了するのを待つ必要はありません。
イシューはプルリクエストとは別にインポートされますが、これは「イシュー」API だけがイシューとプルリクエストの両方のラベルを含んでいるからです。イシューのインポートとラベルリンクの設定を同じワーカーで行うことで、API データを個別にクロールする必要がなくなり、プロジェクトのインポートに必要な API 呼び出しの回数が減ります。
10.ステージ::ImportIssueEventsWorker
このワーカーはすべてのイシューとプルリクエストイベントをインポートします。イベントごとに、Gitlab::GithubImport::ImportIssueEventWorker
ワーカーのジョブをスケジュールします。
イシューとプルリクエストのイベントをひとつのステージでインポートできるのは、GitHub API の特殊な仕組みのおかげです。GitHub では、イシューとプルリクエストはひとつのテーブルに格納されています。そのため、これらはグローバルに固有の ID を持っています:
- すべてのプルリクエストはイシューです。
- イシューはプルリクエストではありません。
そのため、イシューとプルリクエストの両方が、関連するほとんどのことについて共通のAPIを持っています。
Gitlab::GithubImport::Settings
で制御できます)。11.ステージ::ImportNotesWorker
このワーカーは、イシューとプルリクエストの通常のコメントをインポートします。コメントごとに、Gitlab::GithubImport::ImportNoteWorker
ワーカーのジョブをスケジュールします。
GitHub API はイシューとプルリクエストの両方に対してコメントを返すので、通常のコメントは最後にインポートしなければなりません。これは、すべてのイシューとプルリクエストがインポートされるのを待ってから通常のコメントをインポートしなければならないことを意味します。
12.ステージ::ImportAttachmentsWorker
このワーカーはMarkdown内にリンクされているノートの添付ファイルをインポートします。プロジェクト内の Markdown テキストを持つ各エンティティに対して、以下のジョブをスケジュールします:
-
Gitlab::GithubImport::Importer::Attachments::ReleasesImporter
のジョブをリリースごとにスケジュールします。 -
Gitlab::GithubImport::Importer::Attachments::NotesImporter
すべての音符に -
Gitlab::GithubImport::Importer::Attachments::IssuesImporter
すべてのイシューに対して。 -
Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter
すべてのマージリクエストに対してです。
ジョブごとに
- 特定のレコード内のすべての添付リンクを反復処理します。
- 添付ファイルをダウンロードします。
- 古いリンクを新しく生成されたGitLabへのリンクに置き換えます。
Gitlab::GithubImport::Settings
で制御します)。13.ステージ::ImportProtectedBranchesWorker
このワーカーは、保護されたブランチルールをインポートします。GitHub に存在するすべてのルールに対して、Gitlab::GithubImport::ImportProtectedBranchWorker
のジョブをスケジュールします。
各ジョブは GitHub と GitLab のブランチ保護ルールを比較し、最も厳しいルールを GitLab のブランチに適用します。
14.ステージ::FinishImportWorker
このワーカーは、いくつかのハウスキーピング (キャッシュのフラッシュなど) を実行し、インポートを完了としてマークすることで、インポート処理を完了します。
ステージの進行
ステージを進めるには2つの方法があります:
- ワーカーを直接次のステージにスケジューリングする方法。
-
Gitlab::GithubImport::AdvanceStageWorker
のジョブをスケジューリングし、現在のステージの作業がすべて完了したときにステージを進めます。
最初の方法は、すべての作業をシングルスレッドで行うワーカーにのみ使うべきで、それ以外のものにはAdvanceStageWorker
を使うべきです。
ジョブをスケジュールするとき、AdvanceStageWorker
にはプロジェクト ID、Redis キーのリスト、次のステージの名前が渡されます。Redisキー(Gitlab::JobWaiter
によって生成される)は、実行中のステージが完了したかどうかをチェックするために使用されます。ステージがまだ完了していない場合、AdvanceStageWorker
は自分自身を再スケジュールします。ステージが終了すると、AdvanceStageworker
はインポートJIDをリフレッシュし(これについては後述)、次のステージのワーカーをスケジュールします。
AdvanceStageWorker
スケジュールされるジョブの数を減らすために、このワーカーは、次のアクションを決定する前に、ジョブが完了するのを一時的に待ちます。小さなプロジェクトでは、これはインポート処理を少し遅くするかもしれませんが、システム全体への圧力を減らすことにもなります。
インポートジョブ ID のリフレッシュ
GitLab には、Gitlab::Import::StuckProjectImportJobsWorker
プロジェクトのインポートを定期的に実行し、24 時間以上経過した場合は失敗と Gitlab::Import::StuckProjectImportJobsWorker
みなすワーカーがあります。Gitlab::Import::StuckProjectImportJobsWorker
GitHub プロジェクトの場合、これはちょっとした問題になります。大規模なプロジェクトのインポートは、GitHub のレート制限 (詳細は後述します) の頻度によっては数時間かかる Gitlab::Import::StuckProjectImportJobsWorker
こともあります。
これを防ぐために、インポート処理の有効期限を定期的に更新します。これは、インポートジョブのJIDをデータベースに保存し、インポート処理中のさまざまなステージでこのJIDのTTLを更新することで機能します。これはProjectImportState#refresh_jid_expiration
を呼び出すことで実行されます。このTTLを更新することで、インポートが失敗とマークされないようにすることができます。
GitHub のレート制限
GitHub には、1 時間あたり 5,000 回の API 呼び出しという制限があります。プロジェクトをインポートするのに必要なリクエスト数は、プロジェクトに参加しているユニークユーザー(イシュー作成者など)の数によって決まります。イシューページやコメントなど、その他のデータのインポートには通常数十リクエストしか必要ありません。
私たちは以下のようにすることで、レート制限に対処しています:
- 料金の上限に達した後、私たちはどちらかを選びます:
- 制限レートがリセットされるまでジョブが実行されないように、自動的にジョブのスケジュールを変更します。
- 複数の GitHub アクセストークンが API に渡された場合は、別の GitHub アクセストークンに移動します。
- GitHub ユーザーと GitLab ユーザーのマッピングを Redis にキャッシュします。
ユーザーキャッシュについての詳細は以下をご覧ください。
ユーザ・ルックアップのキャッシュ
GitHub のユーザーを GitLab のユーザーにマッピングする際には、(最悪の場合)ルックアップを行う必要があります:
- ユーザーのメールアドレスを取得するために API を一回呼び出します。
- 対応するGitLabユーザーが存在するかどうかを確認するために2つのデータベースクエリ。一方のクエリは GitHub のユーザー ID を元にユーザーを見つけようとし、もう一方のクエリは GitHub の Email アドレスを元にユーザーを見つけようとします。
ユーザーのミスマッチを避けるため、GitHub Enterpriseからのインポート時にはGitHubユーザーIDによる検索は行われません。
この処理にはかなりのコストがかかるため、Redis に検索結果をキャッシュしています。検索したユーザーごとに 5 つのキーを保存します:
- GitHub のユーザー名とメールアドレスを対応させた Redis キー。
- GitHubのメールアドレスとGitLabユーザーIDをマッピングしたRedisキー。
- GitHubユーザーIDとGitLabユーザーIDをマッピングしたRedisキー。
- GitHubのユーザー名とETAGヘッダーをマッピングしたRedisキー。
- プロジェクトのメール検索が行われたかどうかを示す Redis キー。
2種類のルックアップをキャッシュします:
- GitLabユーザーIDが見つかったことを意味します。
- 負のルックアップ、GitLabユーザーIDが見つからなかったことを意味します。これをキャッシュすることで、GitLabデータベースに存在しないとわかっているユーザーに対して同じ作業を行うことを防ぎます。
これらのキーの有効期限は24時間です。正引っかかりのキャッシュを取得するときは、TTLを自動的にリフレッシュします。偽のルックアップのTTLはリフレッシュされません。
メールの検索で空または負の検索結果が返された場合、ヘッダーにキャッシュされたETAGを持つConditional Requestがプロジェクトごとに一度だけ行われます。条件付きリクエストは GitHub API のレート制限にカウントされません。
このキャッシュレイヤーのため、新しく登録されたGitLabアカウントが対応するGitHubアカウントとリンクされていない可能性があります。しかし、これはキャッシュされたキーの有効期限が切れたり、新しいプロジェクトがインポートされたりすると解決されます。
ユーザーキャッシュの検索はプロジェクト間で共有されます。つまり、インポートするプロジェクトの数が多ければ多いほど、GitHub API の呼び出しは少なくて済みます。
このコードは
lib/gitlab/github_import/user_finder.rb
lib/gitlab/github_import/caching.rb
ラベルとマイルストーンのマッピング
データベースへの負担を軽減するため、イシューやマージリクエストにラベルやマイルストーンを設定する際にデータベースへのクエリは行いません。代わりに、ラベルとマイルストーンをインポートするときにこのデータをキャッシュし、課題/マージリクエストに割り当てるときにこのキャッシュを再利用します。ユーザー検索と同様に、これらのキャッシュ・キーは24時間使用されないと自動的に失効します。
ユーザー・ルックアップ・キャッシュとは異なり、これらのラベルとマイルストーンのキャッシュはインポートされるプロジェクトにスコープされます。
このコードは
lib/gitlab/github_import/label_finder.rb
lib/gitlab/github_import/milestone_finder.rb
lib/gitlab/github_import/caching.rb
ログ
インポートの進捗はlogs/importer.log
ファイルで確認できます。関連するインポートはそれぞれ"import_type": "github"
と"project_id"
で記録されます。
最後のログエントリは、フェッチされインポートされたオブジェクトの数をレポートします:
{
"message": "GitHub project import finished",
"duration_s": 347.25,
"objects_imported": {
"fetched": {
"diff_note": 93,
"issue": 321,
"note": 794,
"pull_request": 108,
"pull_request_merged_by": 92,
"pull_request_review": 81
},
"imported": {
"diff_note": 93,
"issue": 321,
"note": 794,
"pull_request": 108,
"pull_request_merged_by": 92,
"pull_request_review": 81
}
},
"import_source": "github",
"project_id": 47,
"import_stage": "Gitlab::GithubImport::Stage::FinishImportWorker"
}
メトリクス・ダッシュボード
GitHub インポーターの健全性を評価するために、GitHub インポーターダッシュボードでは、フェッチされたオブジェクトの総数とインポートされたオブジェクトの総数を時系列で表示します。