Support transferring security export tables (dep list + vuln) on org group transfer
What does this MR do and why?
Add organization transfer support for 8 tables related to dependency list exports and vulnerability exports, and move vulnerability_exports transfer from UsersService auto-discovery to a dedicated SecurityExportsService.
dependency_list_exports requires no transfer work — its num_nonnulls(group_id, organization_id, project_id) = 1 constraint means the organization is derived through the group or project sharding key. vulnerability_exports was previously auto-discovered by UsersService; this MR explicitly skips it there and transfers it via SecurityExportsService instead, keeping it co-located with its child tables on the gitlab_sec database.
Architecture
Phase 1 (synchronous, gitlab_main_org transaction):
- Updates 3 upload partition tables scoped by
model_idto only affect records belonging to the transferring group hierarchy - Nullifies
organization_idon 3 state tables via batchedupdate_allto fire BEFORE UPDATE triggers, which re-readorganization_idfrom their now-updated parent upload - State table updates are scoped to the exact upload IDs just transferred (via
transfer_uploads_and_fire_triggerhelper), preventing unintended updates to pre-existing records in the target organization
Phase 2 (async, gitlab_sec via SecurityExportsWorker):
- Updates
vulnerability_exports,dependency_list_export_parts, andvulnerability_export_partsviaSecurityExportsService - Batches group and project IDs from
gitlab_maininto Ruby arrays to avoid cross-database subqueries withgitlab_sec - For each group/project batch: transfers
vulnerability_exportsdirectly, then iterates parentdependency_list_exportsandvulnerability_exportsto collect export IDs and transfer their parts - Worker uses
concurrency_limit -> { 1 },deduplicate :until_executed, anddefer_on_database_health_signal :gitlab_sec
Tables changed to supported
| Table | Schema | Phase |
|---|---|---|
dependency_list_export_parts |
gitlab_sec |
Phase 2 (async) |
dependency_list_export_part_uploads |
gitlab_main_org |
Phase 1 (sync) |
dependency_list_export_part_upload_states |
gitlab_main_org |
Phase 1 (trigger) |
vulnerability_export_parts |
gitlab_sec |
Phase 2 (async) |
vulnerability_export_uploads |
gitlab_main_org |
Phase 1 (sync) |
vulnerability_export_upload_states |
gitlab_main_org |
Phase 1 (trigger) |
vulnerability_export_part_uploads |
gitlab_main_org |
Phase 1 (sync) |
vulnerability_export_part_upload_states |
gitlab_main_org |
Phase 1 (trigger) |
Query plans
ee/app/services/ee/organizations/transfer/groups_service.rb
Phase 1 — ID lookups (gitlab_sec)
.where(group_id: descendant_group_ids).or(::Dependencies::DependencyListExport.where(project_id: descendant_project_ids))
SELECT dependency_list_exports pluck (cold: 1.0 ms / hot: 0.8 ms)
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE ("dependency_list_exports"."group_id" IN (9970)
OR "dependency_list_exports"."project_id" = 278964).where(dependency_list_export_id: dep_list_export_ids).pluck(:id)
SELECT dependency_list_export_parts pluck (cold: 0.7 ms / hot: 0.8 ms)
SELECT "dependency_list_export_parts"."id"
FROM "dependency_list_export_parts"
WHERE "dependency_list_export_parts"."dependency_list_export_id" IN (1, 2, 3, 4, 5).where(group_id: descendant_group_ids).or(::Vulnerabilities::Export.where(project_id: descendant_project_ids))
SELECT vulnerability_exports pluck (cold: 2.9 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE ("vulnerability_exports"."group_id" IN (9970)
OR "vulnerability_exports"."project_id" = 278964).where(vulnerability_export_id: vuln_export_ids).pluck(:id)
SELECT vulnerability_export_parts pluck (cold: 0.9 ms / hot: 0.7 ms)
SELECT "vulnerability_export_parts"."id"
FROM "vulnerability_export_parts"
WHERE "vulnerability_export_parts"."vulnerability_export_id" IN (1, 2, 3, 4, 5)Phase 1 — Upload transfers (gitlab_main_org)
upload_class.where(model_id: model_id_batch, organization_id: old_organization.id)
upload SELECT: dependency_list_export_part_uploads (cold: 1.0 ms / hot: 1.0 ms)
SELECT "dependency_list_export_part_uploads"."id"
FROM "dependency_list_export_part_uploads"
WHERE "dependency_list_export_part_uploads"."model_id" = 278964
AND "dependency_list_export_part_uploads"."organization_id" = 1
LIMIT 1000upload_class.where(id: upload_ids).update_all(organization_id: new_organization.id)
upload UPDATE: dependency_list_export_part_uploads (cold: 2.3 ms)
UPDATE "dependency_list_export_part_uploads"
SET "organization_id" = 2000258
WHERE "dependency_list_export_part_uploads"."id" IN (
SELECT "dependency_list_export_part_uploads"."id"
FROM "dependency_list_export_part_uploads"
WHERE "dependency_list_export_part_uploads"."model_id" = 278964
AND "dependency_list_export_part_uploads"."organization_id" = 1
LIMIT 1000)upload_class.where(model_id: model_id_batch, organization_id: old_organization.id)
upload SELECT: vulnerability_export_uploads (cold: 1.0 ms / hot: 0.8 ms)
SELECT "vulnerability_export_uploads"."id"
FROM "vulnerability_export_uploads"
WHERE "vulnerability_export_uploads"."model_id" IN (278964)
AND "vulnerability_export_uploads"."organization_id" = 1
LIMIT 1000upload_class.where(id: upload_ids).update_all(organization_id: new_organization.id)
upload UPDATE: vulnerability_export_uploads (cold: 1.4 ms)
UPDATE "vulnerability_export_uploads"
SET "organization_id" = 2000258
WHERE "vulnerability_export_uploads"."id" IN (
SELECT "vulnerability_export_uploads"."id"
FROM "vulnerability_export_uploads"
WHERE "vulnerability_export_uploads"."model_id" IN (278964)
AND "vulnerability_export_uploads"."organization_id" = 1
LIMIT 1000)upload_class.where(model_id: model_id_batch, organization_id: old_organization.id)
upload SELECT: vulnerability_export_part_uploads (cold: 1.0 ms / hot: 0.9 ms)
SELECT "vulnerability_export_part_uploads"."id"
FROM "vulnerability_export_part_uploads"
WHERE "vulnerability_export_part_uploads"."model_id" = 278964
AND "vulnerability_export_part_uploads"."organization_id" = 1
LIMIT 1000upload_class.where(id: upload_ids).update_all(organization_id: new_organization.id)
upload UPDATE: vulnerability_export_part_uploads (cold: 1.8 ms)
UPDATE "vulnerability_export_part_uploads"
SET "organization_id" = 2000258
WHERE "vulnerability_export_part_uploads"."id" IN (
SELECT "vulnerability_export_part_uploads"."id"
FROM "vulnerability_export_part_uploads"
WHERE "vulnerability_export_part_uploads"."model_id" = 278964
AND "vulnerability_export_part_uploads"."organization_id" = 1
LIMIT 1000)Phase 1 — Trigger-fire state tables (gitlab_main_org)
state_class.where(parent_fk => upload_ids).where(organization_id: old_organization.id).update_all(organization_id: nil)
trigger fire UPDATE: dependency_list_export_part_upload_states (cold: 2.1 ms)
UPDATE "dependency_list_export_part_upload_states"
SET "organization_id" = NULL
WHERE "dependency_list_export_part_upload_states"."dependency_list_export_part_upload_id" IN (
SELECT "dependency_list_export_part_uploads"."id"
FROM "dependency_list_export_part_uploads"
WHERE "dependency_list_export_part_uploads"."model_id" = 278964)
AND "dependency_list_export_part_upload_states"."organization_id" = 1trigger fire UPDATE: vulnerability_export_upload_states (cold: 2.3 ms)
UPDATE "vulnerability_export_upload_states"
SET "organization_id" = NULL
WHERE "vulnerability_export_upload_states"."vulnerability_export_upload_id" IN (
SELECT "vulnerability_export_uploads"."id"
FROM "vulnerability_export_uploads"
WHERE "vulnerability_export_uploads"."model_id" = 278964)
AND "vulnerability_export_upload_states"."organization_id" = 1trigger fire UPDATE: vulnerability_export_part_upload_states (cold: 1.9 ms)
UPDATE "vulnerability_export_part_upload_states"
SET "organization_id" = NULL
WHERE "vulnerability_export_part_upload_states"."vulnerability_export_part_upload_id" IN (
SELECT "vulnerability_export_part_uploads"."id"
FROM "vulnerability_export_part_uploads"
WHERE "vulnerability_export_part_uploads"."model_id" = 278964)
AND "vulnerability_export_part_upload_states"."organization_id" = 1ee/app/services/organizations/transfer/security_exports_service.rb
Phase 2 — each_export_batch iteration queries (gitlab_sec)
export_class.where(scope).each_batch — iterates parent exports by group_id
each_export_batch cursor: dependency_list_exports by group_id (cold: 3.3 ms / hot: 3.3 ms)
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."group_id" IN (9970)
ORDER BY "dependency_list_exports"."id" ASC
LIMIT 1each_export_batch boundary: dependency_list_exports by group_id (cold: 3.3 ms / hot: 3.3 ms)
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."group_id" IN (9970)
AND "dependency_list_exports"."id" >= 1
ORDER BY "dependency_list_exports"."id" ASC
LIMIT 1 OFFSET 1000export_class.where(scope).each_batch — iterates parent exports by project_id
each_export_batch cursor: dependency_list_exports by project_id (cold: 3.3 ms / hot: 3.3 ms)
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."project_id" = 278964
ORDER BY "dependency_list_exports"."id" ASC
LIMIT 1each_export_batch boundary: dependency_list_exports by project_id (cold: 3.3 ms / hot: 3.3 ms)
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."project_id" = 278964
AND "dependency_list_exports"."id" >= 1
ORDER BY "dependency_list_exports"."id" ASC
LIMIT 1 OFFSET 1000each_export_batch(::Vulnerabilities::Export, scope) — iterates parent exports by group_id
each_export_batch cursor: vulnerability_exports by group_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."group_id" IN (9970)
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1each_export_batch boundary: vulnerability_exports by group_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."group_id" IN (9970)
AND "vulnerability_exports"."id" >= 1
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1 OFFSET 1000each_export_batch(::Vulnerabilities::Export, scope) — iterates parent exports by project_id
each_export_batch cursor: vulnerability_exports by project_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."project_id" = 278964
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1each_export_batch boundary: vulnerability_exports by project_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."project_id" = 278964
AND "vulnerability_exports"."id" >= 1
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1 OFFSET 1000Phase 2 — update_organization_id_for each_batch queries (gitlab_sec)
update_organization_id_for(::Dependencies::DependencyListExport::Part) { |r| r.where(dependency_list_export_id: export_ids) }
each_batch cursor: dependency_list_export_parts (cold: 12.7 ms / hot: 3.4 ms)
SELECT "dependency_list_export_parts"."id"
FROM "dependency_list_export_parts"
WHERE "dependency_list_export_parts"."organization_id" = 1
AND "dependency_list_export_parts"."dependency_list_export_id" IN (
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."group_id" IN (9970))
ORDER BY "dependency_list_export_parts"."id" ASC
LIMIT 1each_batch boundary: dependency_list_export_parts (cold: 7.3 ms / hot: 3.7 ms)
SELECT "dependency_list_export_parts"."id"
FROM "dependency_list_export_parts"
WHERE "dependency_list_export_parts"."organization_id" = 1
AND "dependency_list_export_parts"."dependency_list_export_id" IN (
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."group_id" IN (9970))
AND "dependency_list_export_parts"."id" >= 1
ORDER BY "dependency_list_export_parts"."id" ASC
LIMIT 1 OFFSET 1000each_batch UPDATE: dependency_list_export_parts (cold: 1.4 ms)
UPDATE "dependency_list_export_parts"
SET "organization_id" = 2000258
WHERE "dependency_list_export_parts"."organization_id" = 1
AND "dependency_list_export_parts"."dependency_list_export_id" IN (
SELECT "dependency_list_exports"."id"
FROM "dependency_list_exports"
WHERE "dependency_list_exports"."group_id" IN (9970))
AND "dependency_list_export_parts"."id" >= 1
AND "dependency_list_export_parts"."id" < 1001update_organization_id_for(::Vulnerabilities::Export) { |r| r.where(group_id: batch) }
each_batch cursor: vulnerability_exports by group_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."organization_id" = 1
AND "vulnerability_exports"."group_id" IN (9970)
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1each_batch boundary: vulnerability_exports by group_id (cold: 0.8 ms / hot: 0.9 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."organization_id" = 1
AND "vulnerability_exports"."group_id" IN (9970)
AND "vulnerability_exports"."id" >= 1
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1 OFFSET 1000each_batch UPDATE: vulnerability_exports by group_id (cold: 0.8 ms)
UPDATE "vulnerability_exports"
SET "organization_id" = 2000258
WHERE "vulnerability_exports"."organization_id" = 1
AND "vulnerability_exports"."group_id" IN (9970)
AND "vulnerability_exports"."id" >= 1
AND "vulnerability_exports"."id" < 1001update_organization_id_for(::Vulnerabilities::Export) { |r| r.where(project_id: batch) }
each_batch cursor: vulnerability_exports by project_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."organization_id" = 1
AND "vulnerability_exports"."project_id" = 278964
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1each_batch boundary: vulnerability_exports by project_id (cold: 0.8 ms / hot: 0.8 ms)
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."organization_id" = 1
AND "vulnerability_exports"."project_id" = 278964
AND "vulnerability_exports"."id" >= 1
ORDER BY "vulnerability_exports"."id" ASC
LIMIT 1 OFFSET 1000each_batch UPDATE: vulnerability_exports by project_id (cold: 0.8 ms)
UPDATE "vulnerability_exports"
SET "organization_id" = 2000258
WHERE "vulnerability_exports"."organization_id" = 1
AND "vulnerability_exports"."project_id" = 278964
AND "vulnerability_exports"."id" >= 1
AND "vulnerability_exports"."id" < 1001update_organization_id_for(::Vulnerabilities::Export::Part) { |r| r.where(vulnerability_export_id: export_ids) }
each_batch cursor: vulnerability_export_parts (cold: 2.7 ms / hot: 1.4 ms)
SELECT "vulnerability_export_parts"."id"
FROM "vulnerability_export_parts"
WHERE "vulnerability_export_parts"."organization_id" = 1
AND "vulnerability_export_parts"."vulnerability_export_id" IN (
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."group_id" IN (9970))
ORDER BY "vulnerability_export_parts"."id" ASC
LIMIT 1each_batch boundary: vulnerability_export_parts (cold: 1.8 ms / hot: 1.7 ms)
SELECT "vulnerability_export_parts"."id"
FROM "vulnerability_export_parts"
WHERE "vulnerability_export_parts"."organization_id" = 1
AND "vulnerability_export_parts"."vulnerability_export_id" IN (
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."group_id" IN (9970))
AND "vulnerability_export_parts"."id" >= 1
ORDER BY "vulnerability_export_parts"."id" ASC
LIMIT 1 OFFSET 1000each_batch UPDATE: vulnerability_export_parts (cold: 1.4 ms)
UPDATE "vulnerability_export_parts"
SET "organization_id" = 2000258
WHERE "vulnerability_export_parts"."organization_id" = 1
AND "vulnerability_export_parts"."vulnerability_export_id" IN (
SELECT "vulnerability_exports"."id"
FROM "vulnerability_exports"
WHERE "vulnerability_exports"."group_id" IN (9970))
AND "vulnerability_export_parts"."id" >= 1
AND "vulnerability_export_parts"."id" < 1001References
- Parent issue: #611974 (closed)
- Related epic: &21846