Resolve cross-join in ProjectsGrade.grades_for with an instance
What does this MR do and why?
Resolve cross-join in ProjectsGrades.grades_for(vulnerable) when the vulnerable is an InstanceSecurityDashboard.
- Add a new method that plucks the IDs of all user dashboard projects that are visible and not archived.
- Change
.grades_forto leverage that method, and pass the project IDs toVulnerabilities::Statistics.for_project.
Note: There are up to 1,000 projects in a dashboard. In production the vast majority of dashboards only have a few projects. The queries has been tested on postgres.ai with hundreds of projects.
References
Please include cross links to any resources that are relevant to this MR. This will give reviewers and future readers helpful context to give an efficient review of the changes introduced.
MR acceptance checklist
Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Screenshots or screen recordings
On an environment where sec is separate,
spec for .grades_for with an instance are now successful.
--- rspec_failures_master 2024-12-13 00:40:07.461651303 -0800
+++ rspec_failures_mr 2024-12-13 00:40:37.121367875 -0800
@@ -1,14 +1,11 @@
-29 examples, 8 failures
+27 examples, 5 failures
Failed examples:
rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:60 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is a Group when subgroups are not included when the filter is not given when remove_cross_join_from_vulnerabilities_projects_grade is disabled returns the letter grades for given vulnerable
rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:85 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is a Group when subgroups are not included when the filter is given when remove_cross_join_from_vulnerabilities_projects_grade is disabled returns the filtered letter grade for given vulnerable
rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:117 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is a Group when subgroups are included when the filter is not given when remove_cross_join_from_vulnerabilities_projects_grade is disabled returns the letter grades for given vulnerable
-rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:136 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is a Group when subgroups are included when the filter is not given when multiple batches are required when remove_cross_join_from_vulnerabilities_projects_grade is disabled returns the letter grades for given vulnerable
-rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:162 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is a Group when subgroups are included when the filter is given when remove_cross_join_from_vulnerabilities_projects_grade is disabled returns the filtered letter grade for given vulnerable
-rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:196 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerables are groups when remove_cross_join_from_vulnerabilities_projects_grade is disabled in one of the groups returns all letter grades for each vulnerable
-rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:227 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is an InstanceSecurityDashboard when the filter is not given returns the letter grades for given vulnerable
-rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:242 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is an InstanceSecurityDashboard when the filter is given returns the filtered letter grade for given vulnerable
+rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:142 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerable is a Group when subgroups are included when the filter is given when remove_cross_join_from_vulnerabilities_projects_grade is disabled returns the filtered letter grade for given vulnerable
+rspec ./ee/spec/models/vulnerabilities/projects_grade_spec.rb:176 # Vulnerabilities::ProjectsGrade.grades_for when the given vulnerables are groups when remove_cross_join_from_vulnerabilities_projects_grade is disabled in one of the groups returns all letter grades for each vulnerable
-[TEST PROF INFO] Time spent in factories: 00:02.268 (21.37% of total time)
+[TEST PROF INFO] Time spent in factories: 00:01.823 (17.27% of total time)
NOTE: Specs for .grades_for with groups and
when remove_cross_join_from_vulnerabilities_projects_grade is disabled still fail.
That's expected. See Resolve cross-join in ProjectsGrade.grades_for ... (!172720 - merged)
NOTE: The exact lines of the specs no longer match because an unnecessary context has been removed.
console log
$ git checkout master
$ bundle exec bin/rspec /home/debian/gitlab-development-kit/gitlab/ee/spec/models/vulnerabilities/projects_grade_spec.rb|grep -A 100 failures>rspec_failures_master
$ git checkout 503387-remove-cross-join-from-vulnerabilities-projects_grade-2
$ bundle exec bin/rspec /home/debian/gitlab-development-kit/gitlab/ee/spec/models/vulnerabilities/projects_grade_spec.rb|grep -A 100 failures>rspec_failures_mr
$ git diff --stat master.. ee/spec/
ee/spec/models/vulnerabilities/projects_grade_spec.rb | 20 --------------------
1 file changed, 20 deletions(-)
How to set up and validate locally
-
Run the following spec in an environment where
secis separate.bundle exec bin/rspec -E InstanceSecurityDashboard ee/spec/models/vulnerabilities/projects_grade_spec.rb -
Create a personal security dashboard, add projects, and check the
Project security statuson the right.
SQL queries
What previously was a single JOIN query is now divided in two consecutive queries.
See !175602 (comment 2269010409)
non_archived_project_ids
Query triggered by InstanceSecurityDashboard#non_archived_project_ids:
SELECT
"projects"."id"
FROM
"projects"
LEFT JOIN project_features ON projects.id = project_features.project_id
WHERE
"projects"."id" IN (
SELECT
"users_security_dashboard_projects"."project_id"
FROM
"users_security_dashboard_projects"
WHERE
"users_security_dashboard_projects"."user_id" = 188
AND (
EXISTS(
SELECT
1
FROM
"project_authorizations"
WHERE
"users_security_dashboard_projects"."user_id" = 188
AND "project_authorizations"."user_id" = 188
AND (
users_security_dashboard_projects.project_id = project_authorizations.project_id
)
AND "project_authorizations"."access_level" IN (30, 40, 50)
)
)
)
AND (
"project_features"."security_and_compliance_access_level" IS NULL
OR "project_features"."security_and_compliance_access_level" IN (20, 30)
OR (
"project_features"."security_and_compliance_access_level" = 10
AND EXISTS (
SELECT
1
FROM
"project_authorizations"
WHERE
"project_authorizations"."user_id" = 188
AND (
project_authorizations.project_id = project_features.project_id
)
AND (
project_authorizations.access_level >= 10
)
)
)
)
AND "projects"."archived" = FALSE
See https://console.postgres.ai/gitlab/gitlab-production-main/sessions/34658/commands/107220
Second query
Query triggered by .grades_for with an InstanceSecurityDashboard but no filter:
SELECT
"vulnerability_statistics"."letter_grade",
array_agg(project_id) project_ids
FROM
"vulnerability_statistics"
WHERE
"vulnerability_statistics"."project_id" IN (86, 87)
GROUP BY
"vulnerability_statistics"."letter_grade"
See https://console.postgres.ai/gitlab/gitlab-production-main/sessions/34607/commands/106970
Query triggered by .grades_for with an InstanceSecurityDashboard but and filter on the letter grade:
SELECT
"vulnerability_statistics"."letter_grade",
array_agg(project_id) project_ids
FROM
"vulnerability_statistics"
WHERE
"vulnerability_statistics"."project_id" IN (86, 87)
AND "vulnerability_statistics"."letter_grade" = 1
GROUP BY
"vulnerability_statistics"."letter_grade"
Related to #513472 (closed)