Add loose FK between package tables and ci pipelines
⚔ Context
In the context of database decomposition, we have to remove all the database links to ci_*
tables.
In package, we have two tables that have foreign keys to table ci_pipelines
:
packages_build_infos
packages_package_file_build_infos
Now, build info tables have this requirement of not be destroyed when a pipeline is destroyed. To achieve this when the foreign key existed, we used a CASCADE SET NULL
restriction which worked well.
In !70614 (merged), we removed the database restriction and added has_many X, dependent: :nullify
statements to simulate the same behavior purely on the rails side.
The problem is that we can't do that due to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions:
When at least two different databases are modified during a transaction initiated on any database server, the application triggers a cross-database modification error (only in test environment).
and that's exactly what will happen with the dependent: :nullify
option.
As suggested in #345664 (closed), the way to fix this is by using loose foreign keys.
We can use them here despite the drawbacks because:
- build_info objects are never accessed directly (such as /packages/23/build_infos/42)
- the are mainly used for object navigation between a package and a pipeline.
🔬 What does this MR do and why?
- Add loose foreign keys between the package build info tables and ci pipelines table
- Remove the not so useful associations from the ci pipeline model
- objects are navigated from package -> pipeline and never the other way around = we don't need the association on the pipeline object to go to a package related object.
🎥 Screenshots or screen recordings
We created a package and uploaded it the same version using 3 pipelines:
Packages::Package.find(789).build_infos.map(&:pipeline_id)
=> [367, 368, 369]
After deleting a pipeline and waiting for the background worker to cleanup the foreign keys, we get the intended result:
Packages::Package.find(789).build_infos.map(&:pipeline_id)
=> [368, 369, nil]
The pipeline_id
in the last build info object has been nullified
🍵 How to set up and validate locally
In a rails console:
# creates a package
def fixture_file_upload(*args, **kwargs)
Rack::Test::UploadedFile.new(*args, **kwargs)
end
# note the package id here
pkg = FactoryBot.create(:npm_package, project: Project.first) # execute until it succeeds
# creates 5 pipelines
Packages::BuildInfo.create!(package_id: pkg.id, pipeline_id: FactoryBot.create(:ci_pipeline).id) # execute until it succeeds
Packages::BuildInfo.create!(package_id: pkg.id, pipeline_id: FactoryBot.create(:ci_pipeline).id) # execute until it succeeds
Packages::BuildInfo.create!(package_id: pkg.id, pipeline_id: FactoryBot.create(:ci_pipeline).id) # execute until it succeeds
# check the build infos
pkg.reload.build_infos.map(&:pipeline_id)
=> [370, 371, 372]
# destroy a pipeline
Ci::Pipeline.find(372).destroy!
# Wait for the cleanup job to run or run it manually
LooseForeignKeys::CleanupWorker.new.perform
# check the build infos again
pkg.build_infos.reload.map(&:pipeline_id)
=> [370, 371, nil]
🚀 MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.