Fix orphaned uploads from vulnerability export FK cleanup
What does this MR do and why?
This MR fixes a gap where LFK cleanup may miss deleting Upload records for Vulnerabilities::Export jobs, specifically when deleting an org, namespace, project or user. The current behaviour of LooseForeignKeys::CleanerService just issues a raw bulk DELETE, which doesn't cascade down to the associated Upload record. The uploads partitions have no sharding-key foreign key back to organizations, so deleting an organization does not cascade those upload rows away either.
This follows the precedent already set for file-backed tables: p_ci_job_artifacts opts out of the generic bulk DELETE with a cleaner_class key in config/gitlab_loose_foreign_keys.yml, handled by Ci::JobArtifacts::LooseForeignKeyCleanerService.
This MR:
- Adds
Vulnerabilities::Exports::LooseForeignKeyCleanerService, which routes the delete throughVulnerabilities::Exports::BatchDestroyServiceso the exports' uploads are removed rather than orphaned. - Adds
Vulnerabilities::Exports::PartsLooseForeignKeyCleanerServicefor thevulnerability_export_partsentry. It is needed separately because the YAML is ordered alphabetically, sovulnerability_export_partsis processed beforevulnerability_exportswhen an organization is deleted. - Wires both into the five affected loose foreign key definitions via
cleaner_class. - Changes
Gitlab::Database::LooseForeignKeys.build_definitionto resolvecleaner_classthrough a new privateresolve_cleaner_classmethod. On FOSS, whereee/is removed, it usessafe_constantize, and an unresolvable EE-only cleaner falls back to the generic cleaner, which is the current FOSS behaviour anyway. On EE it still usesconstantize, so a genuine load failure raises rather than silently reverting to the bulk delete.ALLOWED_CLEANER_CLASSESstill rejects unknown names.
Design notes
- Neither cleaner opens a transaction around its delete, because the cleanup also writes to
uploads, which lives in a different database. That differs from the job artifacts cleaner, which can hold its rows withFOR UPDATE SKIP LOCKEDinside a transaction. SinceSKIP LOCKEDoutside a transaction releases the locks as soon as theSELECTends, these cleaners do not use it at all; concurrent runs are kept apart upstream, whereLooseForeignKeys::ProcessDeletedRecordsServiceclaims the parent records. - Like the job artifacts cleaner, these subclasses always delete and do not read
loose_foreign_key_definition.options[:conditions]or branch onon_delete. No conditions are configured for these tables, but adding one, or switching an entry toasync_nullifywhile leavingcleaner_classset, would not behave as expected.
Depends on
Vulnerabilities::Exports::BatchDestroyService only deletes the exports' parts and their uploads once !254286 (merged) merges. Until then this MR fixes the orphaned uploads owned by the exports themselves, and the parts' uploads are still orphaned by the cascade. No further code change is needed here once that merges.
Not covered by this MR
- The same loose foreign key gap exists for
dependency_list_exports(pipelines, users, projects, namespaces) anddependency_list_export_parts(organizations). Deliberately deferred; the expiry-path equivalent for those tables is fixed in !254397 (merged) - Orphans that already exist are not removed. Those need the documented
delete_orphaned_uploadsconsole step: https://docs.gitlab.com/administration/geo/replication/troubleshooting/synchronization_verification/#failed-verification-of-uploads-on-the-primary-geo-site
References
- Related issue: #602878 (closed)
- Expiry-path fix for the same tables: !254286 (merged)
How to set up and validate locally
- Open a rails console.
- Run the following. On
masterthe last line returns1; with this MR it returns0.
project = FactoryBot.create(:project)
export = FactoryBot.create(:vulnerability_export, :with_csv_file, project: project)
Upload.for_model_type_and_id(Vulnerabilities::Export, export.id).count # => 1
project.destroy!
LooseForeignKeys::CleanupWorker.new.perform
Vulnerabilities::Export.exists?(export.id) # => false
Upload.for_model_type_and_id(Vulnerabilities::Export, export.id).count # => 0MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.