Resolve cross join issues in ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb

Summary

Decomposition of some vulnerability tables has resulted in cross join issues in the listed service which have been allowed to progress anyways. Likely these will be resolvable simply once all the related tables have been decomposed to the correct schema.

Further details

Specs fail with CrossDatabaseModificationAcrossUnsupportedTablesError errors when temporary_ignore_tables_in_transaction is removed from Vulnerabilities::SecurityFinding::CreateMergeRequestService#execute.

NOTE: This references Resolve cross join issues in services/merge_req... (#480003 - closed) but it should reference #480359 (closed) (this very issue) instead.

module Vulnerabilities
  module SecurityFinding
    class CreateMergeRequestService < ::BaseProjectService
      def execute
        enforce_authorization!

        @error_message = nil

        merge_request = nil
        Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
          %w[
            internal_ids
            merge_requests
            merge_request_user_mentions
            vulnerability_merge_request_links
          ], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/480003'
        ) do
          merge_request = ApplicationRecord.transaction do
            vulnerability = find_or_create_vulnerability
            create_merge_request(vulnerability).tap do |merge_request|
              create_vulnerability_merge_request_link(merge_request, vulnerability)
            end
          end
        end

We have two sequences of SQL queries causing failures.

       0: INSERT INTO vulnerability_statistics AS target (project_id, total, unknown, letter_grade, created_at, updated_at)
         VALUES ($1, $2, $3, $4, now(), now())
       ON CONFLICT (project_id)
         DO UPDATE SET
           "total" = GREATEST(TARGET."total" + $5, $6), "unknown" = GREATEST(TARGET."unknown" + $7, $8), letter_grade = (CASE
       WHEN TARGET.critical + EXCLUDED.critical > $9 THEN
         $10
       WHEN TARGET.high + TARGET.unknown + EXCLUDED.high + EXCLUDED.unknown > $11 THEN
         $12
       WHEN TARGET.medium + EXCLUDED.medium > $13 THEN
         $14
       WHEN TARGET.low + EXCLUDED.low > $15 THEN
         $16
       ELSE
         $17
       END
       ), updated_at = now()
        /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test_sec,db_config_name:sec,line:/ee/app/services/vulnerabilities/statistics/update_service.rb:40:in `execute'*/
     
       1: UPDATE "internal_ids" SET "last_value" = ("internal_ids"."last_value" + $1) WHERE "internal_ids"."project_id" = $2 AND "internal_ids"."usage" = $3 RETURNING "last_value" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:164:in `update_record!'*/
       0: UPDATE "internal_ids" SET "last_value" = ("internal_ids"."last_value" + $1) WHERE "internal_ids"."project_id" = $2 AND "internal_ids"."usage" = $3 RETURNING "last_value" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:164:in `update_record!'*/
     
       1: INSERT INTO "internal_ids" ("project_id","namespace_id","usage","last_value") VALUES ($1, $2, $3, $4) ON CONFLICT  DO NOTHING RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:178:in `create_record!'*/
     
       2: INSERT INTO "merge_requests" ("target_branch", "source_branch", "source_project_id", "author_id", "title", "created_at", "updated_at", "target_project_id", "iid", "description", "merge_params", "title_html", "description_html", "cached_markdown_version", "lock_version") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/services/issuable_base_service.rb:370:in `transaction_create'*/
     
       3: DELETE FROM "merge_request_user_mentions" WHERE "merge_request_user_mentions"."merge_request_id" = $1 AND "merge_request_user_mentions"."note_id" IS NULL /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/concerns/cache_markdown_field.rb:168:in `store_mentions!'*/
     
       4: UPDATE "merge_requests" SET "merge_status" = $1 WHERE "merge_requests"."id" = $2 /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/lib/gitlab/timeless.rb:9:in `timeless'*/
     
       5: INSERT INTO "vulnerability_merge_request_links" ("vulnerability_id", "merge_request_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test_sec,db_config_name:sec,line:/ee/app/services/vulnerability_merge_request_links/create_service.rb:9:in `execute'*/

Spec failures

failure for first sequence of queries
  7) Vulnerabilities::SecurityFinding::CreateMergeRequestService#execute when there is an existing vulnerability for the security finding does not create a new Vulnerability, but creates a new MergeRequest, and a MergeRequestLink
     Failure/Error: raise CrossDatabaseModificationAcrossUnsupportedTablesError, messages.join("\n\n")
     
     Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError:
       Cross-database data modification of 'gitlab_main_cell, gitlab_sec, gitlab_main' were detected within a transaction modifying the 'internal_ids, merge_requests, merge_request_user_mentions, vulnerability_merge_request_links' tables. 
     
       Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception.
     
       0: UPDATE "internal_ids" SET "last_value" = ("internal_ids"."last_value" + $1) WHERE "internal_ids"."project_id" = $2 AND "internal_ids"."usage" = $3 RETURNING "last_value" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:164:in `update_record!'*/
     
       1: INSERT INTO "internal_ids" ("project_id","namespace_id","usage","last_value") VALUES ($1, $2, $3, $4) ON CONFLICT  DO NOTHING RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:178:in `create_record!'*/
     
       2: INSERT INTO "merge_requests" ("target_branch", "source_branch", "source_project_id", "author_id", "title", "created_at", "updated_at", "target_project_id", "iid", "description", "merge_params", "title_html", "description_html", "cached_markdown_version", "lock_version") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/services/issuable_base_service.rb:370:in `transaction_create'*/
     
       3: DELETE FROM "merge_request_user_mentions" WHERE "merge_request_user_mentions"."merge_request_id" = $1 AND "merge_request_user_mentions"."note_id" IS NULL /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/concerns/cache_markdown_field.rb:168:in `store_mentions!'*/
     
       4: UPDATE "merge_requests" SET "merge_status" = $1 WHERE "merge_requests"."id" = $2 /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/lib/gitlab/timeless.rb:9:in `timeless'*/
     
       5: INSERT INTO "vulnerability_merge_request_links" ("vulnerability_id", "merge_request_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test_sec,db_config_name:sec,line:/ee/app/services/vulnerability_merge_request_links/create_service.rb:9:in `execute'*/
     # ./lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb:127:in `analyze'
     # ./lib/gitlab/database/query_analyzer.rb:134:in `block in process_sql'
     # ./lib/gitlab/database/query_analyzer.rb:131:in `each'
     # ./lib/gitlab/database/query_analyzer.rb:131:in `process_sql'
     # ./lib/gitlab/database/query_analyzer.rb:74:in `block (2 levels) in hook!'
     # ./lib/gitlab/database/query_analyzer.rb:146:in `with_ignored_recursive_calls'
     # ./lib/gitlab/database/query_analyzer.rb:73:in `block in hook!'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `public_send'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `block in write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:228:in `retry_with_backoff'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:126:in `write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:61:in `block (2 levels) in <class:ConnectionProxy>'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `public_send'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `block in write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:228:in `retry_with_backoff'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:126:in `write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:78:in `transaction'
     # ./ee/app/services/vulnerability_merge_request_links/create_service.rb:9:in `execute'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:32:in `execute_service!'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:56:in `create_vulnerability_merge_request_link'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:16:in `block (2 levels) in execute'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:15:in `block in execute'
     # ./app/models/concerns/cross_database_modification.rb:91:in `block in transaction'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `public_send'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `block in write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:228:in `retry_with_backoff'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:126:in `write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:78:in `transaction'
     # ./lib/gitlab/database.rb:394:in `block in transaction'
     # ./lib/gitlab/database.rb:393:in `transaction'
     # ./app/models/concerns/cross_database_modification.rb:82:in `transaction'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:13:in `execute'
     # ./ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb:33:in `block (2 levels) in <top (required)>'
     # ./ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb:96:in `block (4 levels) in <top (required)>'
     # ./ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb:96:in `block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:474:in `block (3 levels) in <top (required)>'
     # ./lib/gitlab/sidekiq_sharding/validator.rb:42:in `enabled'
     # ./spec/spec_helper.rb:473:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:468:in `block (3 levels) in <top (required)>'
     # ./spec/support/sidekiq_middleware.rb:9:in `with_sidekiq_server_middleware'
     # ./spec/spec_helper.rb:459:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:455:in `block (3 levels) in <top (required)>'
     # ./lib/gitlab/application_context.rb:97:in `with_raw_context'
     # ./spec/spec_helper.rb:455:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:426:in `block (3 levels) in <top (required)>'
     # ./lib/gitlab/ci/config/feature_flags.rb:38:in `ensure_correct_usage'
     # ./spec/spec_helper.rb:425:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:271:in `block (2 levels) in <top (required)>'
     # ./spec/support/system_exit_detected.rb:7:in `block (2 levels) in <main>'
     # ./spec/support/database/prevent_cross_joins.rb:106:in `block (3 levels) in <main>'
     # ./spec/support/database/prevent_cross_joins.rb:60:in `with_cross_joins_prevented'
     # ./spec/support/database/prevent_cross_joins.rb:106:in `block (2 levels) in <main>'
failure for second sequence of SQL queries
  8) Vulnerabilities::SecurityFinding::CreateMergeRequestService#execute when there is an existing vulnerability for the security finding does not change the vulnerability present_on_default_branch value
     Failure/Error: raise CrossDatabaseModificationAcrossUnsupportedTablesError, messages.join("\n\n")
     
     Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError:
       Cross-database data modification of 'gitlab_main_cell, gitlab_sec, gitlab_main' were detected within a transaction modifying the 'internal_ids, merge_requests, merge_request_user_mentions, vulnerability_merge_request_links' tables. 
     
       Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception.
     
       0: UPDATE "internal_ids" SET "last_value" = ("internal_ids"."last_value" + $1) WHERE "internal_ids"."project_id" = $2 AND "internal_ids"."usage" = $3 RETURNING "last_value" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:164:in `update_record!'*/
     
       1: INSERT INTO "internal_ids" ("project_id","namespace_id","usage","last_value") VALUES ($1, $2, $3, $4) ON CONFLICT  DO NOTHING RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/internal_id.rb:178:in `create_record!'*/
     
       2: INSERT INTO "merge_requests" ("target_branch", "source_branch", "source_project_id", "author_id", "title", "created_at", "updated_at", "target_project_id", "iid", "description", "merge_params", "title_html", "description_html", "cached_markdown_version", "lock_version") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/services/issuable_base_service.rb:370:in `transaction_create'*/
     
       3: DELETE FROM "merge_request_user_mentions" WHERE "merge_request_user_mentions"."merge_request_id" = $1 AND "merge_request_user_mentions"."note_id" IS NULL /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/app/models/concerns/cache_markdown_field.rb:168:in `store_mentions!'*/
     
       4: UPDATE "merge_requests" SET "merge_status" = $1 WHERE "merge_requests"."id" = $2 /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test,db_config_name:main,line:/lib/gitlab/timeless.rb:9:in `timeless'*/
     
       5: INSERT INTO "vulnerability_merge_request_links" ("vulnerability_id", "merge_request_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" /*application:test,correlation_id:ad95f787b441057ad9f8addce47aa727,db_config_database:gitlabhq_test_sec,db_config_name:sec,line:/ee/app/services/vulnerability_merge_request_links/create_service.rb:9:in `execute'*/
     # ./lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb:127:in `analyze'
     # ./lib/gitlab/database/query_analyzer.rb:134:in `block in process_sql'
     # ./lib/gitlab/database/query_analyzer.rb:131:in `each'
     # ./lib/gitlab/database/query_analyzer.rb:131:in `process_sql'
     # ./lib/gitlab/database/query_analyzer.rb:74:in `block (2 levels) in hook!'
     # ./lib/gitlab/database/query_analyzer.rb:146:in `with_ignored_recursive_calls'
     # ./lib/gitlab/database/query_analyzer.rb:73:in `block in hook!'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `public_send'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `block in write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:228:in `retry_with_backoff'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:126:in `write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:61:in `block (2 levels) in <class:ConnectionProxy>'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `public_send'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `block in write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:228:in `retry_with_backoff'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:126:in `write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:78:in `transaction'
     # ./ee/app/services/vulnerability_merge_request_links/create_service.rb:9:in `execute'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:32:in `execute_service!'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:56:in `create_vulnerability_merge_request_link'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:16:in `block (2 levels) in execute'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:15:in `block in execute'
     # ./app/models/concerns/cross_database_modification.rb:91:in `block in transaction'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `public_send'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:127:in `block in write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:141:in `block in read_write'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:228:in `retry_with_backoff'
     # ./lib/gitlab/database/load_balancing/load_balancer.rb:130:in `read_write'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:126:in `write_using_load_balancer'
     # ./lib/gitlab/database/load_balancing/connection_proxy.rb:78:in `transaction'
     # ./lib/gitlab/database.rb:394:in `block in transaction'
     # ./lib/gitlab/database.rb:393:in `transaction'
     # ./app/models/concerns/cross_database_modification.rb:82:in `transaction'
     # ./ee/app/services/vulnerabilities/security_finding/create_merge_request_service.rb:13:in `execute'
     # ./ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb:33:in `block (2 levels) in <top (required)>'
     # ./ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb:104:in `block (4 levels) in <top (required)>'
     # ./ee/spec/services/vulnerabilities/security_finding/create_merge_request_service_spec.rb:104:in `block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:474:in `block (3 levels) in <top (required)>'
     # ./lib/gitlab/sidekiq_sharding/validator.rb:42:in `enabled'
     # ./spec/spec_helper.rb:473:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:468:in `block (3 levels) in <top (required)>'
     # ./spec/support/sidekiq_middleware.rb:9:in `with_sidekiq_server_middleware'
     # ./spec/spec_helper.rb:459:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:455:in `block (3 levels) in <top (required)>'
     # ./lib/gitlab/application_context.rb:97:in `with_raw_context'
     # ./spec/spec_helper.rb:455:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:426:in `block (3 levels) in <top (required)>'
     # ./lib/gitlab/ci/config/feature_flags.rb:38:in `ensure_correct_usage'
     # ./spec/spec_helper.rb:425:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:271:in `block (2 levels) in <top (required)>'
     # ./spec/support/system_exit_detected.rb:7:in `block (2 levels) in <main>'
     # ./spec/support/database/prevent_cross_joins.rb:106:in `block (3 levels) in <main>'
     # ./spec/support/database/prevent_cross_joins.rb:60:in `with_cross_joins_prevented'
     # ./spec/support/database/prevent_cross_joins.rb:106:in `block (2 levels) in <main>

Proposal

  • Remove temporary_ignore_tables_in_transaction from Vulnerabilities::SecurityFinding::CreateMergeRequestService#execute.
  • Implement the rollback mechanism in the service class itself, and destroy objects when the service fails. We can rely on the ActiveRecord::Rollback exception that's already raised when any of the services CreateMergeRequestService depends on fails.

Same as #503978 (closed), so doing on issue should close the other.

Edited by Fabien Catteau