InfoGrab DocsInfoGrab Docs

직접 전송을 통한 그룹 마이그레이션

요약

직접 전송을 사용하려면 GitLab 설치 환경이 GitLab IP 주소에서 접근 가능하고 공개 DNS 항목을 가지고 있어야 합니다. 직접 전송은 파일 내보내기를 사용한 그룹·프로젝트 마이그레이션이 발전한 방식입니다. 직접 전송에 새 관계나 파이프라인을 추가하는 방법은 직접 전송 임포터에 새 관계 추가를 참고합니다.

Note

직접 전송을 사용하려면 GitLab 설치 환경이 GitLab IP 주소에서 접근 가능하고 공개 DNS 항목을 가지고 있어야 합니다.

직접 전송은 파일 내보내기를 사용한 그룹·프로젝트 마이그레이션이 발전한 방식입니다. 프로젝트를 포함한 그룹 전체를 한 GitLab 인스턴스에서 다른 인스턴스로 사용자가 더 쉽게 마이그레이션하도록 하는 것이 목표입니다.

직접 전송에 새 관계나 파이프라인을 추가하는 방법은 직접 전송 임포터에 새 관계 추가를 참고합니다.

오프라인 전송은 소스 인스턴스와 대상 인스턴스가 네트워크로 서로 연결될 수 없을 때 사용하는 직접 전송의 변형입니다.

용어#

이 페이지 전반에서 다음 용어가 반복해서 쓰입니다.

용어 설명
Source instance 마이그레이션 대상 그룹 또는 프로젝트를 소유한 GitLab 인스턴스입니다.
Destination instance 그룹 또는 프로젝트가 마이그레이션되어 들어가는 GitLab 인스턴스입니다.
BulkImport BulkImport 레코드로 추적되는 하나의 마이그레이션 요청입니다. 하나의 요청이 여러 최상위 그룹이나 프로젝트를 포함할 수 있습니다.
Entity 마이그레이션이 예약된 단일 그룹 또는 프로젝트이며, BulkImports::Entity 레코드로 추적됩니다.
Portable 파이프라인이 임포트하는 관계를 소유한 대상 그룹 또는 프로젝트 레코드입니다(entity.group 또는 entity.project).
Relation 마이그레이션되는 데이터의 한 유형입니다. 예를 들어 이슈나 레이블이 있으며, import_export.yml에 정의되고 일반적으로 하나의 파이프라인이 처리합니다.
Tracker 하나의 엔터티에 대해 하나의 파이프라인 진행 상황을 추적하는 BulkImports::Tracker 레코드입니다.

직접 전송 코드는 대부분 BulkImports 모듈에 있으며 Import 모듈로 점진적으로 이전되고 있습니다. 직접 전송은 Gitlab::ImportExport 모듈에도 의존합니다.

설계 결정#

직접 전송은 관계마다 하나의 파이프라인을 실행해 소스 인스턴스에서 각 관계를 추출하고 대상 데이터베이스에 적재합니다. 파이프라인은 별도의 Sidekiq job으로 실행되므로 서로 독립적인 파이프라인은 병렬로 실행됩니다. 다만 일부 파이프라인은 다른 파이프라인이 먼저 임포트를 마쳐야 하므로, 파이프라인은 스테이지로 묶이며 앞선 스테이지의 모든 파이프라인이 끝난 뒤에야 실행됩니다. 최초 요청부터 각 파이프라인 실행까지 마이그레이션을 구동하는 Sidekiq 워커가 서로를 어떻게 호출하는지는 Sidekiq job 실행 계층 구조를 참고합니다.

파이프라인#

파이프라인은 ETL(추출, 변환, 적재) 아키텍처를 사용하며, 이 덕분에 코드가 더 명시적이고 따라가기·테스트하기·확장하기 쉬워집니다.

파이프라인 클래스는 어떤 추출기, 변환기, 로더를 사용하는지만 선언합니다. 추출-변환-적재 루프 자체는 구현하지 않으므로 그 루프는 한 곳에만 존재하면 됩니다. 이 루프는 공용 BulkImports::Pipeline::Runner concern에 들어 있습니다.

파이프라인은 extractor와 loader 클래스 메서드로 추출기와 로더를 지정하거나, 인스턴스 메서드 extract와 load를 구현해 지정합니다. 또한 추출된 각 항목에 선언 순서대로 실행되는 여러 transformer 클래스를 연결할 수 있습니다. 파이프라인이 인스턴스 메서드 transform도 구현한 경우에는 선언된 transformer 클래스보다 그 메서드가 먼저 실행됩니다. Runner#run은 한 페이지 분량의 항목에 대해 extract를 호출한 뒤 각 항목에 차례로 변환기와 로더를 실행하고, 그다음 페이지를 요청합니다.

일부 관계는 다른 관계가 먼저 임포트되어 있어야 하므로 파이프라인은 번호가 매겨진 스테이지로 묶이며, 이전 스테이지의 모든 파이프라인이 끝나야 다음 스테이지가 시작됩니다. BulkImports::Groups::Stage 와 BulkImports::Projects::Stage 는 그룹 또는 프로젝트 엔터티에서 어떤 파이프라인이 어느 스테이지에 실행되는지 정의하며, BulkImports::EntityWorker 는 한 스테이지의 모든 파이프라인 트래커를 큐에 넣은 뒤 다음 스테이지로 넘어갑니다.

소스 API 사용#

직접 전송은 처음에 모든 리소스를 GraphQL API나 REST API로 마이그레이션하도록 설계되었습니다. 이 방식에서는 API 속도 제한, 필요한 정보를 모두 노출하지 않는 엔드포인트, API 요청으로 인한 소스 인스턴스 과부하 등 몇 가지 문제가 발생했습니다. 그래서 이 방식은 폐기되었고, 대신 대부분의 리소스는 관계 내보내기로 전송합니다. 대상 인스턴스가 소스 인스턴스에 관계 내보내기를 요청하면 대개 NDJSON(줄바꿈으로 구분된 JSON) 파일이 생성되고, 대상 인스턴스가 그 파일을 내려받아 임포트하는 방식으로 파일 기반 임포터와 비슷합니다.

그 결과 API를 직접 쿼리해 데이터를 가져오는 파이프라인은 일부만 남아 있습니다.

대부분의 관계는 NDJSON으로 내보내져 NDJSON 파이프라인으로 임포트됩니다. uploads 나 lfs_objects 같은 다른 관계는 tar.gz 파일로 내보내집니다.

소스 인스턴스에서 데이터를 가져오는 데 사용되는 엔드포인트#

직접 전송은 관계에 따라 REST 또는 GraphQL로 소스 데이터를 가져옵니다.

소스 인스턴스의 REST 엔드포인트는 엔터티 탐색과 소규모 메타데이터를 처리합니다.

엔드포인트 용도
GET /metadata(GET /version으로 폴백) 소스 GitLab 버전을 확인하며, BulkImports::Clients::HTTP에서 호출됩니다.
GET /personal_access_tokens/self 개인 액세스 토큰의 스코프를 검증합니다.
GET /groups 사용자가 마이그레이션할 수 있는 최상위 그룹을 나열하며, BulkImports::GetImportableDataService에서 호출됩니다.
GET /groups/:id/subgroups 하위 그룹을 탐색하며, BulkImports::Groups::Extractors::SubgroupsExtractor에서 호출됩니다.
GET /groups/:id/badges 및 GET /projects/:id/badges 배지를 가져오며, BulkImports::Common::Extractors::RestExtractor에서 호출됩니다.
POST /groups/:id/export_relations 또는 POST /projects/:id/export_relations 소스 인스턴스에서 내보내기를 트리거하며, BulkImports::ExportRequestWorker에서 호출됩니다.
GET /groups/:id/export_relations/status 또는 GET /projects/:id/export_relations/status 내보내기 진행 상황을 폴링하며, BulkImports::ExportStatus가 래핑합니다.
GET /groups/:id/export_relations/download 또는 GET /projects/:id/export_relations/download 내보낸 파일을 내려받거나, batched와 batch_number 파라미터가 있으면 단일 배치를 내려받으며, Import::BulkImports::HttpFileDownloadStrategy에서 호출됩니다.

소스 인스턴스의 /api/graphql에 대한 GraphQL 쿼리는 NDJSON 파일로 내보내는 대신 직접 요청하는 편이 비용이 적은 엔터티 수준 속성과 소규모 컬렉션을 가져옵니다.

쿼리 용도
GetGroupQuery 및 GetProjectQuery 그룹과 프로젝트의 기본 속성을 가져옵니다.
GetProjectsQuery 그룹의 프로젝트를 나열합니다.
GetRepositoryQuery 및 GetSnippetRepositoryQuery Git 데이터를 전송하는 데 사용되는 리포지터리 및 스니펫 리포지터리 메타데이터를 가져옵니다.
GetMembersQuery 그룹 및 프로젝트 멤버를 가져옵니다.

관계 마이그레이션#

멱등성#

직접 전송 파이프라인 job은 많은 레코드를 임포트하므로 완료까지 오래 걸릴 수 있고, 그래서 job 이 중간에 중단되어 Sidekiq 이 재시도하기도 합니다. job 이 다시 실행될 때 중복 항목이 생기지 않도록, 각 항목은 처리 시점에 캐시되고 이미 캐시에 있으면 건너뜁니다.

캐싱 전략은 두 가지입니다.

전략 설명
BulkImports::Pipeline::HexdigestCacheStrategy 데이터의 hexdigest 표현을 캐시합니다.
BulkImports::Pipeline::IndexCacheStrategy 파이프라인에서 마지막으로 처리한 항목의 인덱스를 캐시합니다.

NDJSON 파이프라인#

대부분의 관계 데이터(이슈, 머지 리퀘스트, 레이블, 마일스톤, CI 리소스 등)는 BulkImports::NdjsonPipeline concern으로 전송되며, 이 concern은 BulkImports::Common::Pipelines 아래의 파이프라인 클래스와 그룹·프로젝트 전용 파이프라인 네임스페이스에 포함됩니다. 각 파이프라인은 ETL 설계의 추출, 변환, 적재 단계를 따릅니다.

관계를 한 번만 정의하면 되도록, 직접 전송은 자체 관계 트리를 따로 두지 않습니다. BulkImports::FileTransfer::BaseConfig 와 그 하위 클래스인 ProjectConfig 및 GroupConfig 는 기존 프로젝트 및 그룹 파일 기반 임포터가 사용하는 것과 같은 import_export.yml 파일을 Gitlab::ImportExport::Config 와 Gitlab::ImportExport::AttributesFinder 를 통해 읽어 들입니다. 이 때문에 import_export.yml에 추가한 관계는 파일 기반 임포터와 직접 전송 양쪽에서 사용할 수 있게 되며, NdjsonPipeline#relation_definition(import_export_config.top_relation_tree(relation))과 제외·포함 속성 목록도 같은 YAML 트리에서 나옵니다. ID, HTML 캐시, 그 밖의 제외 키를 제거하는 등의 속성 정제는 공용 Gitlab::ImportExport::AttributeCleaner 를 거치며, 이 클래스는 두 임포터 모두에서 Gitlab::ImportExport::Base::RelationFactory#parsed_relation_hash가 사용합니다.

중첩 관계#

관계의 NDJSON 한 줄에는 중첩된 연관이 포함될 수 있습니다. 예를 들어 노트와 승인이 중첩된 머지 리퀘스트 줄이 있습니다. Gitlab::ImportExport::Base::RelationFactory 는 평면 해시 하나에서 객체 하나만 만들기 때문에, 재귀는 NdjsonPipeline#deep_transform_relation! 에서 일어납니다. 이 메서드는 import_export.yml의 관계 트리를 아래에서 위로 순회합니다. 트리의 각 하위 관계 키마다 중첩된 해시나 배열로 먼저 재귀해 들어가 이를 생성된 ActiveRecord 객체로 바꾼 뒤에야 상위 해시를 생성 대상으로 넘깁니다. 상위 객체가 만들어질 시점에는 자식이 이미 실제 ActiveRecord 인스턴스이므로, RelationFactory#existing_or_new_object는 연관을 따로 설정하는 단계 없이 자식을 상위의 연관 라이터에 직접 할당합니다.

예를 들어 import_export.yml은 award_emoji를 notes 아래에, notes를 merge_requests 아래에 중첩합니다. 머지 리퀘스트 하나에 대한 NDJSON 한 줄은 깊게 중첩된 평면 해시 하나로 들어옵니다.

{
  "title": "Fix flaky spec",
  "notes": [
    {
      "note": "Nice catch!",
      "award_emoji": [
        { "name": "thumbsup" }
      ]
    }
  ]
}

deep_transform_relation!은 이 해시를 아래에서 위로 순회하므로, 가장 안쪽 관계가 먼저 생성되고 가장 바깥쪽 관계가 마지막에 생성됩니다.

Mermaid 다이어그램 (6줄)
소스 코드 보기
flowchart TD
    accTitle: Bottom-up build order for a nested relation
    accDescr: award_emoji builds first from its flat hash, then note builds from a hash whose award_emoji key already holds the built AwardEmoji objects, then merge_request builds last from a hash whose notes key already holds the built Note objects.
mr["3. merge_request"] --> note["2. note"]
note --&gt; emoji["1. award_emoji"]</code></pre></details></div>

note가 생성될 시점에는 award_emoji 키가 해시 대신 AwardEmoji 객체를 담고 있고, merge_request가 생성될 시점에는 notes 키가 해시 대신 Note 객체를 담고 있습니다. 덕분에 RelationFactory#existing_or_new_object가 각 자식을 상위의 연관 라이터에 직접 할당할 수 있습니다.

노트의 author_id 같은 중첩된 사용자 참조도 같은 방식으로 해석됩니다. deep_transform_relation! 은 모든 노드에 대해 create_import_source_users를 호출해 식별자마다 플레이스홀더 Import::SourceUser가 존재하도록 하고, load가 트리 전체를 영속화한 뒤 push_placeholder_references가 중첩된 모든 객체를 순회하며 Import::PlaceholderReferences::PushService 로 재할당을 등록합니다. 사용자 기여 매핑을 참고합니다.

예외 처리#

잘못된 속성을 가진 이슈처럼 레코드 하나가 잘못되었다고 해서 프로젝트의 다른 모든 이슈 임포트까지 실패하지는 않습니다. 그래서 BulkImports::Pipeline::Runner 는 NDJSON 항목을 하나씩 처리하며 실패를 파이프라인 단위가 아니라 항목 단위로 격리합니다.

  • retriable?가 true를 반환하는 BulkImports::NetworkError, 또는 SourceUserMapper의 잠금이나 중복 사용자 오류는 BulkImports::RetryPipelineError가 됩니다. BulkImports::PipelineWorker가 이를 잡아 해당 예외의 재시도 지연 후 파이프라인 job을 다시 큐에 넣으며, 재시도 횟수는 BulkImports::NetworkError::MAX_RETRIABLE_COUNT 까지입니다.
  • 단일 항목을 추출·변환·적재하는 중에 발생한 그 밖의 예외는 잡혀서 BulkImports::Failure로 기록되고, 처리는 다음 항목으로 이어집니다. 엔터티 마이그레이션 전체는 파이프라인 클래스가 abort_on_failure!로 표시된 경우에만 중단됩니다.
  • RelationObjectSaver는 최상위 레코드를 먼저 저장하므로, 유효성 검증에 실패한 컬렉션 레코드처럼 중첩된 하위 관계가 잘못되거나 실패해도 상위 레코드는 실패하지 않습니다. 이후 NdjsonPipeline#load는 RelationObjectSaver가 잘못되었거나 실패한 것으로 수집한 하위 관계를 각각 별도의 BulkImports::Failure로 기록하며, 파이프라인은 실패시키지 않습니다.

UI의 마이그레이션 상세 정보에 표시되는 것이 바로 BulkImports::Failure 레코드입니다.

배치#

이전 버전의 직접 전송은 관계를 단일 NDJSON 파일로 내보냈습니다. 각 job 이 관계의 더 작은 조각만 처리하도록 이후에 배치가 도입되었고, 현재 NDJSON으로 내보내지는 관계는 대부분 배치로 처리됩니다. 그래서 같은 파이프라인 클래스가 하나의 관계에 대해 처음부터 끝까지 한 번 실행되는 대신, 배치마다 한 번씩 여러 번 실행될 수 있습니다.

Sidekiq job 실행 계층 구조#

앞 절에서 설명한 워커들은 상당히 깊은 사슬 구조로 서로를 큐에 넣으므로, 마이그레이션이 멈췄을 때는 어떤 워커가 어떤 워커를 큐에 넣는지 알아 두면 도움이 됩니다. 다음 다이어그램은 각 인스턴스에서의 사슬 구조를 보여 줍니다.

대상 인스턴스에서는 다음과 같습니다.

Mermaid 다이어그램 (23줄)
소스 코드 보기
flowchart TD
    subgraph s1["Main"]
        BulkImportWorker -- Enqueue itself --> BulkImportWorker
        BulkImportWorker --> BulkImports::ExportRequestWorker
        BulkImports::ExportRequestWorker --> BulkImports::EntityWorker
        BulkImports::EntityWorker -- Enqueue itself --> BulkImports::EntityWorker
        BulkImports::EntityWorker --> BulkImports::PipelineWorker
        BulkImports::EntityWorker --> Import::LoadPlaceholderReferencesWorker
        BulkImports::PipelineWorker -- Enqueue itself --> BulkImports::PipelineWorker
        BulkImports::EntityWorker --> BulkImports::PipelineWorkerA["BulkImports::PipelineWorker"]
        BulkImports::EntityWorker --> BulkImports::PipelineWorkerA1["..."]
    BulkImportWorker --&gt; BulkImports::ExportRequestWorkerB["BulkImports::ExportRequestWorker"]
    BulkImports::ExportRequestWorkerB --&gt; BulkImports::PipelineWorkerBB["..."]
end

subgraph s2["Batched pipelines"]
    BulkImports::PipelineWorker --&gt; BulkImports::PipelineBatchWorker
    BulkImports::PipelineWorker --&gt; BulkImports::PipelineBatchWorkerA["..."]
    BulkImports::PipelineBatchWorker -- Enqueue itself --&gt; BulkImports::PipelineBatchWorker
    BulkImports::PipelineBatchWorker --&gt; BulkImports::FinishBatchedPipelineWorker
    BulkImports::FinishBatchedPipelineWorker -- Enqueue itself --&gt; BulkImports::FinishBatchedPipelineWorker
end</code></pre></details></div>
Mermaid 다이어그램 (4줄)
소스 코드 보기
flowchart TD
  subgraph s1["Cron"]
    BulkImports::StaleImportWorker
  end

소스 인스턴스에서는 다음과 같습니다.

Mermaid 다이어그램 (14줄)
소스 코드 보기
flowchart TD
    subgraph s1["Main"]
        BulkImports::RelationExportWorker
        BulkImports::RelationExportWorker --> BulkImports::UserContributionsExportWorker
        BulkImports::UserContributionsExportWorker -- Enqueue itself --> BulkImports::UserContributionsExportWorker
    end
subgraph s2["Batched relations"]
    BulkImports::RelationExportWorker --&gt; BulkImports::RelationBatchExportWorker
    BulkImports::RelationExportWorker --&gt; BulkImports::RelationBatchExportWorkerA["..."]
    BulkImports::RelationBatchExportWorker -- Enqueue itself --&gt; BulkImports::RelationBatchExportWorker
    BulkImports::RelationExportWorker --&gt; BulkImports::FinishBatchedRelationExportWorker
    BulkImports::FinishBatchedRelationExportWorker -- Enqueue itself --&gt; BulkImports::FinishBatchedRelationExportWorker
end</code></pre></details></div>

사용자 기여 매핑#

직접 전송은 사용자 기여 매핑을 지원합니다. 이를 통해 임포트된 레코드는 임포트 완료 후 실제 사용자를 지정할 수 있을 때까지 플레이스홀더 사용자에게 귀속됩니다.

핵심 컴포넌트#

  • BulkImports::Common::Pipelines::MembersPipeline은 대부분의 소스 사용자 레코드와 플레이스홀더 레코드를 생성하는 파이프라인입니다. 사용자 기여는 일반적으로 그룹 및 프로젝트 멤버에게서 나오기 때문입니다. 이 파이프라인의 변환기인 BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer는 Import::SourceUserMapper를 호출해 소스 멤버의 속성을 대상 인스턴스의 사용자에 매핑합니다. 다만 기여를 하기 위해 반드시 멤버여야 하는 것은 아니고 임포트된 관계 데이터에 이름이나 사용자명 같은 식별 정보가 없을 수 있으므로, 소스 사용자는 이런 정보 없이 다른 관계 파이프라인에서 생성될 수 있습니다.
  • Import::BulkImports::SourceUsersAttributesWorker는 멤버가 아닌 기여자의 소스 사용자 정보를 백필합니다. 마이그레이션 시작 시점에 BulkImports::CreateService의 BulkImportWorker 바로 앞에서 처음 호출됩니다. Import::BulkImports::SourceUsersAttributesWorker는 각 엔터티의 최상위 네임스페이스에 대해 Import::BulkImports::UpdateSourceUsersService를 호출하고, 대량 임포트가 완료될 때까지 5분마다 자신을 다시 큐에 넣습니다.
  • Import::BulkImports::UpdateSourceUsersService는 현재 마이그레이션과 연결된 Import::SourceUser 레코드 중 source_name 이나 source_username 이 없는 레코드를 조회한 다음, 소스 인스턴스에 GraphQL 요청을 보내 누락된 이름과 사용자명을 가져오고, 마지막으로 정보가 누락된 소스 사용자를 업데이트합니다.

핵심 클래스#

클래스 용도
BulkImports::Pipeline::Context 파이프라인에 source_user_mapper와 source_ghost_user_id를 제공합니다.
BulkImports::CreateService 사용자 매핑을 활성화하는 진입점입니다.
BulkImports::NdjsonPipeline ImportExport::Base::RelationFactory로 관계를 영속화하기 전에 Import::SourceUser 레코드를 생성하고, 플레이스홀더 참조가 필요한 관계에 대해 관계 임포트 중 그 참조를 캐시합니다.
BulkImports::Common::Pipelines::MembersPipeline 사용자 기여는 일반적으로 그룹 및 프로젝트 멤버에게서 나오므로 대부분의 소스 사용자 레코드와 플레이스홀더 레코드를 생성합니다
BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer Import::SourceUserMapper를 호출해 소스 멤버의 속성을 대상 인스턴스의 사용자에 매핑합니다. 다만 기여를 하기 위해 반드시 멤버여야 하는 것은 아니고 임포트된 관계 데이터에 이름이나 사용자명 같은 식별 정보가 없을 수 있으므로, 소스 사용자는 이런 정보 없이 다른 관계 파이프라인에서 생성될 수 있습니다
Import::BulkImports::SourceUsersMapper ImportExport::Base::RelationFactory가 소스 사용자 ID를 대상 사용자 ID에 매핑할 수 있도록 해시와 비슷한 인터페이스를 제공합니다. 이를 통해 파일 기반 임포트가 플레이스홀더 사용자 매핑을 지원하지 않더라도 직접 전송과 파일 기반 임포트 모두 ImportExport::Base::RelationFactory를 사용할 수 있습니다.
Import::BulkImports::SourceUsersAttributesWorker 멤버가 아닌 기여자의 소스 사용자 정보를 백필합니다. 마이그레이션 시작 시점에 BulkImports::CreateService의 BulkImportWorker 바로 앞에서 처음 호출됩니다. 각 엔터티의 최상위 네임스페이스에 대해 Import::BulkImports::UpdateSourceUsersService를 호출하고, 대량 임포트가 완료될 때까지 5분마다 자신을 다시 큐에 넣습니다.
Import::BulkImports::UpdateSourceUsersService 현재 마이그레이션과 연결된 Import::SourceUser 레코드 중 source_name 이나 source_username 이 없는 레코드를 조회한 다음, 소스 인스턴스에 GraphQL 요청을 보내 누락된 이름과 사용자명을 가져오고, 마지막으로 정보가 누락된 소스 사용자를 업데이트합니다.

고스트 사용자 처리#

고스트 사용자는 플레이스홀더로 매핑할 실제 기여자가 아닙니다. 두 인스턴스 모두 원 작성자가 더 이상 존재하지 않는 콘텐츠의 대체자로 같은 방식으로 사용합니다. 그래서 직접 전송은 소스 인스턴스의 고스트 사용자를 별도로 처리합니다. BulkImports::SourceInternalUserFinder는 소스 인스턴스에 GraphQL 요청을 보내 GitLab 고스트 사용자명을 가진 비인간 사용자를 조회하고(구현 시점에는 고스트 사용자를 위한 별도의 API 요청이 없었습니다) 소스 고스트 사용자 ID를 캐시합니다.

소스 고스트 사용자를 만나면 다음과 같이 처리합니다.

  • 기여는 대상 인스턴스의 고스트 사용자에게 직접 귀속됩니다
  • 플레이스홀더 사용자는 생성되지 않습니다

플레이스홀더 사용자 생성 건너뛰기#

approvals, builds, ci_pipelines, events 같은 일부 관계는 소스 인스턴스에 더 이상 존재하지 않는 사용자를 참조할 수 있고 이를 잡아낼 외래 키 제약도 없기 때문에 플레이스홀더 사용자 생성을 건너뜁니다. BulkImports::NdjsonPipeline::IGNORE_PLACEHOLDER_USER_CREATION을 참고합니다.

직접 전송을 통한 그룹 마이그레이션

GitLab v19.4
원문 보기

요약

직접 전송을 사용하려면 GitLab 설치 환경이 GitLab IP 주소에서 접근 가능하고 공개 DNS 항목을 가지고 있어야 합니다. 직접 전송은 파일 내보내기를 사용한 그룹·프로젝트 마이그레이션이 발전한 방식입니다. 직접 전송에 새 관계나 파이프라인을 추가하는 방법은 직접 전송 임포터에 새 관계 추가를 참고합니다.

Note

직접 전송을 사용하려면 GitLab 설치 환경이 GitLab IP 주소에서 접근 가능하고 공개 DNS 항목을 가지고 있어야 합니다.

직접 전송은 파일 내보내기를 사용한 그룹·프로젝트 마이그레이션이 발전한 방식입니다. 프로젝트를 포함한 그룹 전체를 한 GitLab 인스턴스에서 다른 인스턴스로 사용자가 더 쉽게 마이그레이션하도록 하는 것이 목표입니다.

직접 전송에 새 관계나 파이프라인을 추가하는 방법은 직접 전송 임포터에 새 관계 추가를 참고합니다.

오프라인 전송은 소스 인스턴스와 대상 인스턴스가 네트워크로 서로 연결될 수 없을 때 사용하는 직접 전송의 변형입니다.

용어#

이 페이지 전반에서 다음 용어가 반복해서 쓰입니다.

용어 설명
Source instance 마이그레이션 대상 그룹 또는 프로젝트를 소유한 GitLab 인스턴스입니다.
Destination instance 그룹 또는 프로젝트가 마이그레이션되어 들어가는 GitLab 인스턴스입니다.
BulkImport BulkImport 레코드로 추적되는 하나의 마이그레이션 요청입니다. 하나의 요청이 여러 최상위 그룹이나 프로젝트를 포함할 수 있습니다.
Entity 마이그레이션이 예약된 단일 그룹 또는 프로젝트이며, BulkImports::Entity 레코드로 추적됩니다.
Portable 파이프라인이 임포트하는 관계를 소유한 대상 그룹 또는 프로젝트 레코드입니다(entity.group 또는 entity.project).
Relation 마이그레이션되는 데이터의 한 유형입니다. 예를 들어 이슈나 레이블이 있으며, import_export.yml에 정의되고 일반적으로 하나의 파이프라인이 처리합니다.
Tracker 하나의 엔터티에 대해 하나의 파이프라인 진행 상황을 추적하는 BulkImports::Tracker 레코드입니다.

직접 전송 코드는 대부분 BulkImports 모듈에 있으며 Import 모듈로 점진적으로 이전되고 있습니다. 직접 전송은 Gitlab::ImportExport 모듈에도 의존합니다.

설계 결정#

직접 전송은 관계마다 하나의 파이프라인을 실행해 소스 인스턴스에서 각 관계를 추출하고 대상 데이터베이스에 적재합니다. 파이프라인은 별도의 Sidekiq job으로 실행되므로 서로 독립적인 파이프라인은 병렬로 실행됩니다. 다만 일부 파이프라인은 다른 파이프라인이 먼저 임포트를 마쳐야 하므로, 파이프라인은 스테이지로 묶이며 앞선 스테이지의 모든 파이프라인이 끝난 뒤에야 실행됩니다. 최초 요청부터 각 파이프라인 실행까지 마이그레이션을 구동하는 Sidekiq 워커가 서로를 어떻게 호출하는지는 Sidekiq job 실행 계층 구조를 참고합니다.

파이프라인#

파이프라인은 ETL(추출, 변환, 적재) 아키텍처를 사용하며, 이 덕분에 코드가 더 명시적이고 따라가기·테스트하기·확장하기 쉬워집니다.

파이프라인 클래스는 어떤 추출기, 변환기, 로더를 사용하는지만 선언합니다. 추출-변환-적재 루프 자체는 구현하지 않으므로 그 루프는 한 곳에만 존재하면 됩니다. 이 루프는 공용 BulkImports::Pipeline::Runner concern에 들어 있습니다.

파이프라인은 extractor와 loader 클래스 메서드로 추출기와 로더를 지정하거나, 인스턴스 메서드 extract와 load를 구현해 지정합니다. 또한 추출된 각 항목에 선언 순서대로 실행되는 여러 transformer 클래스를 연결할 수 있습니다. 파이프라인이 인스턴스 메서드 transform도 구현한 경우에는 선언된 transformer 클래스보다 그 메서드가 먼저 실행됩니다. Runner#run은 한 페이지 분량의 항목에 대해 extract를 호출한 뒤 각 항목에 차례로 변환기와 로더를 실행하고, 그다음 페이지를 요청합니다.

일부 관계는 다른 관계가 먼저 임포트되어 있어야 하므로 파이프라인은 번호가 매겨진 스테이지로 묶이며, 이전 스테이지의 모든 파이프라인이 끝나야 다음 스테이지가 시작됩니다. BulkImports::Groups::Stage 와 BulkImports::Projects::Stage 는 그룹 또는 프로젝트 엔터티에서 어떤 파이프라인이 어느 스테이지에 실행되는지 정의하며, BulkImports::EntityWorker 는 한 스테이지의 모든 파이프라인 트래커를 큐에 넣은 뒤 다음 스테이지로 넘어갑니다.

소스 API 사용#

직접 전송은 처음에 모든 리소스를 GraphQL API나 REST API로 마이그레이션하도록 설계되었습니다. 이 방식에서는 API 속도 제한, 필요한 정보를 모두 노출하지 않는 엔드포인트, API 요청으로 인한 소스 인스턴스 과부하 등 몇 가지 문제가 발생했습니다. 그래서 이 방식은 폐기되었고, 대신 대부분의 리소스는 관계 내보내기로 전송합니다. 대상 인스턴스가 소스 인스턴스에 관계 내보내기를 요청하면 대개 NDJSON(줄바꿈으로 구분된 JSON) 파일이 생성되고, 대상 인스턴스가 그 파일을 내려받아 임포트하는 방식으로 파일 기반 임포터와 비슷합니다.

그 결과 API를 직접 쿼리해 데이터를 가져오는 파이프라인은 일부만 남아 있습니다.

대부분의 관계는 NDJSON으로 내보내져 NDJSON 파이프라인으로 임포트됩니다. uploads 나 lfs_objects 같은 다른 관계는 tar.gz 파일로 내보내집니다.

소스 인스턴스에서 데이터를 가져오는 데 사용되는 엔드포인트#

직접 전송은 관계에 따라 REST 또는 GraphQL로 소스 데이터를 가져옵니다.

소스 인스턴스의 REST 엔드포인트는 엔터티 탐색과 소규모 메타데이터를 처리합니다.

엔드포인트 용도
GET /metadata(GET /version으로 폴백) 소스 GitLab 버전을 확인하며, BulkImports::Clients::HTTP에서 호출됩니다.
GET /personal_access_tokens/self 개인 액세스 토큰의 스코프를 검증합니다.
GET /groups 사용자가 마이그레이션할 수 있는 최상위 그룹을 나열하며, BulkImports::GetImportableDataService에서 호출됩니다.
GET /groups/:id/subgroups 하위 그룹을 탐색하며, BulkImports::Groups::Extractors::SubgroupsExtractor에서 호출됩니다.
GET /groups/:id/badges 및 GET /projects/:id/badges 배지를 가져오며, BulkImports::Common::Extractors::RestExtractor에서 호출됩니다.
POST /groups/:id/export_relations 또는 POST /projects/:id/export_relations 소스 인스턴스에서 내보내기를 트리거하며, BulkImports::ExportRequestWorker에서 호출됩니다.
GET /groups/:id/export_relations/status 또는 GET /projects/:id/export_relations/status 내보내기 진행 상황을 폴링하며, BulkImports::ExportStatus가 래핑합니다.
GET /groups/:id/export_relations/download 또는 GET /projects/:id/export_relations/download 내보낸 파일을 내려받거나, batched와 batch_number 파라미터가 있으면 단일 배치를 내려받으며, Import::BulkImports::HttpFileDownloadStrategy에서 호출됩니다.

소스 인스턴스의 /api/graphql에 대한 GraphQL 쿼리는 NDJSON 파일로 내보내는 대신 직접 요청하는 편이 비용이 적은 엔터티 수준 속성과 소규모 컬렉션을 가져옵니다.

쿼리 용도
GetGroupQuery 및 GetProjectQuery 그룹과 프로젝트의 기본 속성을 가져옵니다.
GetProjectsQuery 그룹의 프로젝트를 나열합니다.
GetRepositoryQuery 및 GetSnippetRepositoryQuery Git 데이터를 전송하는 데 사용되는 리포지터리 및 스니펫 리포지터리 메타데이터를 가져옵니다.
GetMembersQuery 그룹 및 프로젝트 멤버를 가져옵니다.

관계 마이그레이션#

멱등성#

직접 전송 파이프라인 job은 많은 레코드를 임포트하므로 완료까지 오래 걸릴 수 있고, 그래서 job 이 중간에 중단되어 Sidekiq 이 재시도하기도 합니다. job 이 다시 실행될 때 중복 항목이 생기지 않도록, 각 항목은 처리 시점에 캐시되고 이미 캐시에 있으면 건너뜁니다.

캐싱 전략은 두 가지입니다.

전략 설명
BulkImports::Pipeline::HexdigestCacheStrategy 데이터의 hexdigest 표현을 캐시합니다.
BulkImports::Pipeline::IndexCacheStrategy 파이프라인에서 마지막으로 처리한 항목의 인덱스를 캐시합니다.

NDJSON 파이프라인#

대부분의 관계 데이터(이슈, 머지 리퀘스트, 레이블, 마일스톤, CI 리소스 등)는 BulkImports::NdjsonPipeline concern으로 전송되며, 이 concern은 BulkImports::Common::Pipelines 아래의 파이프라인 클래스와 그룹·프로젝트 전용 파이프라인 네임스페이스에 포함됩니다. 각 파이프라인은 ETL 설계의 추출, 변환, 적재 단계를 따릅니다.

관계를 한 번만 정의하면 되도록, 직접 전송은 자체 관계 트리를 따로 두지 않습니다. BulkImports::FileTransfer::BaseConfig 와 그 하위 클래스인 ProjectConfig 및 GroupConfig 는 기존 프로젝트 및 그룹 파일 기반 임포터가 사용하는 것과 같은 import_export.yml 파일을 Gitlab::ImportExport::Config 와 Gitlab::ImportExport::AttributesFinder 를 통해 읽어 들입니다. 이 때문에 import_export.yml에 추가한 관계는 파일 기반 임포터와 직접 전송 양쪽에서 사용할 수 있게 되며, NdjsonPipeline#relation_definition(import_export_config.top_relation_tree(relation))과 제외·포함 속성 목록도 같은 YAML 트리에서 나옵니다. ID, HTML 캐시, 그 밖의 제외 키를 제거하는 등의 속성 정제는 공용 Gitlab::ImportExport::AttributeCleaner 를 거치며, 이 클래스는 두 임포터 모두에서 Gitlab::ImportExport::Base::RelationFactory#parsed_relation_hash가 사용합니다.

중첩 관계#

관계의 NDJSON 한 줄에는 중첩된 연관이 포함될 수 있습니다. 예를 들어 노트와 승인이 중첩된 머지 리퀘스트 줄이 있습니다. Gitlab::ImportExport::Base::RelationFactory 는 평면 해시 하나에서 객체 하나만 만들기 때문에, 재귀는 NdjsonPipeline#deep_transform_relation! 에서 일어납니다. 이 메서드는 import_export.yml의 관계 트리를 아래에서 위로 순회합니다. 트리의 각 하위 관계 키마다 중첩된 해시나 배열로 먼저 재귀해 들어가 이를 생성된 ActiveRecord 객체로 바꾼 뒤에야 상위 해시를 생성 대상으로 넘깁니다. 상위 객체가 만들어질 시점에는 자식이 이미 실제 ActiveRecord 인스턴스이므로, RelationFactory#existing_or_new_object는 연관을 따로 설정하는 단계 없이 자식을 상위의 연관 라이터에 직접 할당합니다.

예를 들어 import_export.yml은 award_emoji를 notes 아래에, notes를 merge_requests 아래에 중첩합니다. 머지 리퀘스트 하나에 대한 NDJSON 한 줄은 깊게 중첩된 평면 해시 하나로 들어옵니다.

{
  "title": "Fix flaky spec",
  "notes": [
    {
      "note": "Nice catch!",
      "award_emoji": [
        { "name": "thumbsup" }
      ]
    }
  ]
}

deep_transform_relation!은 이 해시를 아래에서 위로 순회하므로, 가장 안쪽 관계가 먼저 생성되고 가장 바깥쪽 관계가 마지막에 생성됩니다.

Mermaid 다이어그램 (6줄)
소스 코드 보기
flowchart TD
    accTitle: Bottom-up build order for a nested relation
    accDescr: award_emoji builds first from its flat hash, then note builds from a hash whose award_emoji key already holds the built AwardEmoji objects, then merge_request builds last from a hash whose notes key already holds the built Note objects.
mr["3. merge_request"] --&gt; note["2. note"]
note --&gt; emoji["1. award_emoji"]</code></pre></details></div>

note가 생성될 시점에는 award_emoji 키가 해시 대신 AwardEmoji 객체를 담고 있고, merge_request가 생성될 시점에는 notes 키가 해시 대신 Note 객체를 담고 있습니다. 덕분에 RelationFactory#existing_or_new_object가 각 자식을 상위의 연관 라이터에 직접 할당할 수 있습니다.

노트의 author_id 같은 중첩된 사용자 참조도 같은 방식으로 해석됩니다. deep_transform_relation! 은 모든 노드에 대해 create_import_source_users를 호출해 식별자마다 플레이스홀더 Import::SourceUser가 존재하도록 하고, load가 트리 전체를 영속화한 뒤 push_placeholder_references가 중첩된 모든 객체를 순회하며 Import::PlaceholderReferences::PushService 로 재할당을 등록합니다. 사용자 기여 매핑을 참고합니다.

예외 처리#

잘못된 속성을 가진 이슈처럼 레코드 하나가 잘못되었다고 해서 프로젝트의 다른 모든 이슈 임포트까지 실패하지는 않습니다. 그래서 BulkImports::Pipeline::Runner 는 NDJSON 항목을 하나씩 처리하며 실패를 파이프라인 단위가 아니라 항목 단위로 격리합니다.

  • retriable?가 true를 반환하는 BulkImports::NetworkError, 또는 SourceUserMapper의 잠금이나 중복 사용자 오류는 BulkImports::RetryPipelineError가 됩니다. BulkImports::PipelineWorker가 이를 잡아 해당 예외의 재시도 지연 후 파이프라인 job을 다시 큐에 넣으며, 재시도 횟수는 BulkImports::NetworkError::MAX_RETRIABLE_COUNT 까지입니다.
  • 단일 항목을 추출·변환·적재하는 중에 발생한 그 밖의 예외는 잡혀서 BulkImports::Failure로 기록되고, 처리는 다음 항목으로 이어집니다. 엔터티 마이그레이션 전체는 파이프라인 클래스가 abort_on_failure!로 표시된 경우에만 중단됩니다.
  • RelationObjectSaver는 최상위 레코드를 먼저 저장하므로, 유효성 검증에 실패한 컬렉션 레코드처럼 중첩된 하위 관계가 잘못되거나 실패해도 상위 레코드는 실패하지 않습니다. 이후 NdjsonPipeline#load는 RelationObjectSaver가 잘못되었거나 실패한 것으로 수집한 하위 관계를 각각 별도의 BulkImports::Failure로 기록하며, 파이프라인은 실패시키지 않습니다.

UI의 마이그레이션 상세 정보에 표시되는 것이 바로 BulkImports::Failure 레코드입니다.

배치#

이전 버전의 직접 전송은 관계를 단일 NDJSON 파일로 내보냈습니다. 각 job 이 관계의 더 작은 조각만 처리하도록 이후에 배치가 도입되었고, 현재 NDJSON으로 내보내지는 관계는 대부분 배치로 처리됩니다. 그래서 같은 파이프라인 클래스가 하나의 관계에 대해 처음부터 끝까지 한 번 실행되는 대신, 배치마다 한 번씩 여러 번 실행될 수 있습니다.

Sidekiq job 실행 계층 구조#

앞 절에서 설명한 워커들은 상당히 깊은 사슬 구조로 서로를 큐에 넣으므로, 마이그레이션이 멈췄을 때는 어떤 워커가 어떤 워커를 큐에 넣는지 알아 두면 도움이 됩니다. 다음 다이어그램은 각 인스턴스에서의 사슬 구조를 보여 줍니다.

대상 인스턴스에서는 다음과 같습니다.

Mermaid 다이어그램 (23줄)
소스 코드 보기
flowchart TD
    subgraph s1["Main"]
        BulkImportWorker -- Enqueue itself --> BulkImportWorker
        BulkImportWorker --> BulkImports::ExportRequestWorker
        BulkImports::ExportRequestWorker --> BulkImports::EntityWorker
        BulkImports::EntityWorker -- Enqueue itself --> BulkImports::EntityWorker
        BulkImports::EntityWorker --> BulkImports::PipelineWorker
        BulkImports::EntityWorker --> Import::LoadPlaceholderReferencesWorker
        BulkImports::PipelineWorker -- Enqueue itself --> BulkImports::PipelineWorker
        BulkImports::EntityWorker --> BulkImports::PipelineWorkerA["BulkImports::PipelineWorker"]
        BulkImports::EntityWorker --> BulkImports::PipelineWorkerA1["..."]
    BulkImportWorker --&gt; BulkImports::ExportRequestWorkerB["BulkImports::ExportRequestWorker"]
    BulkImports::ExportRequestWorkerB --&gt; BulkImports::PipelineWorkerBB["..."]
end

subgraph s2["Batched pipelines"]
    BulkImports::PipelineWorker --&gt; BulkImports::PipelineBatchWorker
    BulkImports::PipelineWorker --&gt; BulkImports::PipelineBatchWorkerA["..."]
    BulkImports::PipelineBatchWorker -- Enqueue itself --&gt; BulkImports::PipelineBatchWorker
    BulkImports::PipelineBatchWorker --&gt; BulkImports::FinishBatchedPipelineWorker
    BulkImports::FinishBatchedPipelineWorker -- Enqueue itself --&gt; BulkImports::FinishBatchedPipelineWorker
end</code></pre></details></div>
Mermaid 다이어그램 (4줄)
소스 코드 보기
flowchart TD
  subgraph s1["Cron"]
    BulkImports::StaleImportWorker
  end

소스 인스턴스에서는 다음과 같습니다.

Mermaid 다이어그램 (14줄)
소스 코드 보기
flowchart TD
    subgraph s1["Main"]
        BulkImports::RelationExportWorker
        BulkImports::RelationExportWorker --> BulkImports::UserContributionsExportWorker
        BulkImports::UserContributionsExportWorker -- Enqueue itself --> BulkImports::UserContributionsExportWorker
    end
subgraph s2["Batched relations"]
    BulkImports::RelationExportWorker --&gt; BulkImports::RelationBatchExportWorker
    BulkImports::RelationExportWorker --&gt; BulkImports::RelationBatchExportWorkerA["..."]
    BulkImports::RelationBatchExportWorker -- Enqueue itself --&gt; BulkImports::RelationBatchExportWorker
    BulkImports::RelationExportWorker --&gt; BulkImports::FinishBatchedRelationExportWorker
    BulkImports::FinishBatchedRelationExportWorker -- Enqueue itself --&gt; BulkImports::FinishBatchedRelationExportWorker
end</code></pre></details></div>

사용자 기여 매핑#

직접 전송은 사용자 기여 매핑을 지원합니다. 이를 통해 임포트된 레코드는 임포트 완료 후 실제 사용자를 지정할 수 있을 때까지 플레이스홀더 사용자에게 귀속됩니다.

핵심 컴포넌트#

  • BulkImports::Common::Pipelines::MembersPipeline은 대부분의 소스 사용자 레코드와 플레이스홀더 레코드를 생성하는 파이프라인입니다. 사용자 기여는 일반적으로 그룹 및 프로젝트 멤버에게서 나오기 때문입니다. 이 파이프라인의 변환기인 BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer는 Import::SourceUserMapper를 호출해 소스 멤버의 속성을 대상 인스턴스의 사용자에 매핑합니다. 다만 기여를 하기 위해 반드시 멤버여야 하는 것은 아니고 임포트된 관계 데이터에 이름이나 사용자명 같은 식별 정보가 없을 수 있으므로, 소스 사용자는 이런 정보 없이 다른 관계 파이프라인에서 생성될 수 있습니다.
  • Import::BulkImports::SourceUsersAttributesWorker는 멤버가 아닌 기여자의 소스 사용자 정보를 백필합니다. 마이그레이션 시작 시점에 BulkImports::CreateService의 BulkImportWorker 바로 앞에서 처음 호출됩니다. Import::BulkImports::SourceUsersAttributesWorker는 각 엔터티의 최상위 네임스페이스에 대해 Import::BulkImports::UpdateSourceUsersService를 호출하고, 대량 임포트가 완료될 때까지 5분마다 자신을 다시 큐에 넣습니다.
  • Import::BulkImports::UpdateSourceUsersService는 현재 마이그레이션과 연결된 Import::SourceUser 레코드 중 source_name 이나 source_username 이 없는 레코드를 조회한 다음, 소스 인스턴스에 GraphQL 요청을 보내 누락된 이름과 사용자명을 가져오고, 마지막으로 정보가 누락된 소스 사용자를 업데이트합니다.

핵심 클래스#

클래스 용도
BulkImports::Pipeline::Context 파이프라인에 source_user_mapper와 source_ghost_user_id를 제공합니다.
BulkImports::CreateService 사용자 매핑을 활성화하는 진입점입니다.
BulkImports::NdjsonPipeline ImportExport::Base::RelationFactory로 관계를 영속화하기 전에 Import::SourceUser 레코드를 생성하고, 플레이스홀더 참조가 필요한 관계에 대해 관계 임포트 중 그 참조를 캐시합니다.
BulkImports::Common::Pipelines::MembersPipeline 사용자 기여는 일반적으로 그룹 및 프로젝트 멤버에게서 나오므로 대부분의 소스 사용자 레코드와 플레이스홀더 레코드를 생성합니다
BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer Import::SourceUserMapper를 호출해 소스 멤버의 속성을 대상 인스턴스의 사용자에 매핑합니다. 다만 기여를 하기 위해 반드시 멤버여야 하는 것은 아니고 임포트된 관계 데이터에 이름이나 사용자명 같은 식별 정보가 없을 수 있으므로, 소스 사용자는 이런 정보 없이 다른 관계 파이프라인에서 생성될 수 있습니다
Import::BulkImports::SourceUsersMapper ImportExport::Base::RelationFactory가 소스 사용자 ID를 대상 사용자 ID에 매핑할 수 있도록 해시와 비슷한 인터페이스를 제공합니다. 이를 통해 파일 기반 임포트가 플레이스홀더 사용자 매핑을 지원하지 않더라도 직접 전송과 파일 기반 임포트 모두 ImportExport::Base::RelationFactory를 사용할 수 있습니다.
Import::BulkImports::SourceUsersAttributesWorker 멤버가 아닌 기여자의 소스 사용자 정보를 백필합니다. 마이그레이션 시작 시점에 BulkImports::CreateService의 BulkImportWorker 바로 앞에서 처음 호출됩니다. 각 엔터티의 최상위 네임스페이스에 대해 Import::BulkImports::UpdateSourceUsersService를 호출하고, 대량 임포트가 완료될 때까지 5분마다 자신을 다시 큐에 넣습니다.
Import::BulkImports::UpdateSourceUsersService 현재 마이그레이션과 연결된 Import::SourceUser 레코드 중 source_name 이나 source_username 이 없는 레코드를 조회한 다음, 소스 인스턴스에 GraphQL 요청을 보내 누락된 이름과 사용자명을 가져오고, 마지막으로 정보가 누락된 소스 사용자를 업데이트합니다.

고스트 사용자 처리#

고스트 사용자는 플레이스홀더로 매핑할 실제 기여자가 아닙니다. 두 인스턴스 모두 원 작성자가 더 이상 존재하지 않는 콘텐츠의 대체자로 같은 방식으로 사용합니다. 그래서 직접 전송은 소스 인스턴스의 고스트 사용자를 별도로 처리합니다. BulkImports::SourceInternalUserFinder는 소스 인스턴스에 GraphQL 요청을 보내 GitLab 고스트 사용자명을 가진 비인간 사용자를 조회하고(구현 시점에는 고스트 사용자를 위한 별도의 API 요청이 없었습니다) 소스 고스트 사용자 ID를 캐시합니다.

소스 고스트 사용자를 만나면 다음과 같이 처리합니다.

  • 기여는 대상 인스턴스의 고스트 사용자에게 직접 귀속됩니다
  • 플레이스홀더 사용자는 생성되지 않습니다

플레이스홀더 사용자 생성 건너뛰기#

approvals, builds, ci_pipelines, events 같은 일부 관계는 소스 인스턴스에 더 이상 존재하지 않는 사용자를 참조할 수 있고 이를 잡아낼 외래 키 제약도 없기 때문에 플레이스홀더 사용자 생성을 건너뜁니다. BulkImports::NdjsonPipeline::IGNORE_PLACEHOLDER_USER_CREATION을 참고합니다.