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 ワーカーのジョブをスケジュールします。

note
このステージはオプションで(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を持っています。

note
このステージはオプションで、インポートにかかる時間を大幅に増やすことができます (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 すべてのマージリクエストに対してです。

ジョブごとに

  1. 特定のレコード内のすべての添付リンクを反復処理します。
  2. 添付ファイルをダウンロードします。
  3. 古いリンクを新しく生成されたGitLabへのリンクに置き換えます。
note
これはオプションのステージで、インポートにかかる時間を大幅に増やすことができます (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 呼び出しという制限があります。プロジェクトをインポートするのに必要なリクエスト数は、プロジェクトに参加しているユニークユーザー(イシュー作成者など)の数によって決まります。イシューページやコメントなど、その他のデータのインポートには通常数十リクエストしか必要ありません。

私たちは以下のようにすることで、レート制限に対処しています:

  1. 料金の上限に達した後、私たちはどちらかを選びます:
    • 制限レートがリセットされるまでジョブが実行されないように、自動的にジョブのスケジュールを変更します。
    • 複数の GitHub アクセストークンが API に渡された場合は、別の GitHub アクセストークンに移動します。
  2. GitHub ユーザーと GitLab ユーザーのマッピングを Redis にキャッシュします。

ユーザーキャッシュについての詳細は以下をご覧ください。

ユーザ・ルックアップのキャッシュ

GitHub のユーザーを GitLab のユーザーにマッピングする際には、(最悪の場合)ルックアップを行う必要があります:

  1. ユーザーのメールアドレスを取得するために API を一回呼び出します。
  2. 対応する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

ログ

  • GitLab 13.7 で導入されました
  • GitLab 14.1 で導入されたインポートオブジェクトの数。
  • Gitlab::GithubImport::Logger GitLab 14.2で導入
  • import_source GitLab 14.2 でimport_type名称変更

インポートの進捗は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 インポーターダッシュボードでは、フェッチされたオブジェクトの総数とインポートされたオブジェクトの総数を時系列で表示します。