Commit b8013dd4 authored by pbair's avatar pbair

Add rubocop to prevent use of subtransactions

Add a new rubocop rule that prevents direct use of subtransactions,
meaning any call to #transaction with the options `requires_new: true`.
parent 28beb7f8
...@@ -712,3 +712,8 @@ QA/SelectorUsage: ...@@ -712,3 +712,8 @@ QA/SelectorUsage:
- 'ee/spec/**/*.rb' - 'ee/spec/**/*.rb'
Exclude: Exclude:
- 'spec/rubocop/**/*_spec.rb' - 'spec/rubocop/**/*_spec.rb'
Performance/ActiveRecordSubtransactions:
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
...@@ -31,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -31,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base
end end
def self.safe_ensure_unique(retries: 0) def self.safe_ensure_unique(retries: 0)
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield yield
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
...@@ -55,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -55,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base
# currently one third of the default 15-second timeout # currently one third of the default 15-second timeout
def self.with_fast_read_statement_timeout(timeout_ms = 5000) def self.with_fast_read_statement_timeout(timeout_ms = 5000)
::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}") connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
yield yield
...@@ -80,7 +80,7 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -80,7 +80,7 @@ class ApplicationRecord < ActiveRecord::Base
# #
# When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create` # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
# and that skips some code that adds the newly created record to the association. # and that skips some code that adds the newly created record to the association.
transaction(requires_new: true) { all.create(*args, &block) } transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
find_by(*args) find_by(*args)
end end
......
...@@ -622,7 +622,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -622,7 +622,7 @@ class ApplicationSetting < ApplicationRecord
def self.create_from_defaults def self.create_from_defaults
check_schema! check_schema!
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
super super
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
......
...@@ -239,7 +239,7 @@ class InternalId < ApplicationRecord ...@@ -239,7 +239,7 @@ class InternalId < ApplicationRecord
lookup lookup
else else
begin begin
subject.transaction(requires_new: true) do subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
...@@ -362,7 +362,7 @@ class InternalId < ApplicationRecord ...@@ -362,7 +362,7 @@ class InternalId < ApplicationRecord
value value
else else
begin begin
subject.transaction(requires_new: true) do subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
internal_id = InternalId.create!(**scope, usage: usage, last_value: value) internal_id = InternalId.create!(**scope, usage: usage, last_value: value)
internal_id.last_value internal_id.last_value
end end
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_deploy_keys_projects move_deploy_keys_projects
remove_remaining_deploy_keys_projects if remove_remaining_elements remove_remaining_deploy_keys_projects if remove_remaining_elements
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super && source_project.fork_network return unless super && source_project.fork_network
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_fork_network_members move_fork_network_members
update_root_project update_root_project
refresh_forks_count refresh_forks_count
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_lfs_objects_projects move_lfs_objects_projects
remove_remaining_lfs_objects_project if remove_remaining_elements remove_remaining_lfs_objects_project if remove_remaining_elements
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_notification_settings move_notification_settings
remove_remaining_notification_settings if remove_remaining_elements remove_remaining_notification_settings if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_authorizations move_project_authorizations
remove_remaining_authorizations if remove_remaining_elements remove_remaining_authorizations if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_group_links move_group_links
remove_remaining_project_group_links if remove_remaining_elements remove_remaining_project_group_links if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_members move_project_members
remove_remaining_members if remove_remaining_elements remove_remaining_members if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
return unless user_stars.any? return unless user_stars.any?
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
user_stars.update_all(project_id: @project.id) user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects Project.reset_counters @project.id, :users_star_projects
......
...@@ -39,7 +39,7 @@ module Users ...@@ -39,7 +39,7 @@ module Users
private private
def migrate_records_in_transaction def migrate_records_in_transaction
user.transaction(requires_new: true) do user.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
@ghost_user = User.ghost @ghost_user = User.ghost
migrate_records migrate_records
......
...@@ -85,7 +85,7 @@ class DeduplicateEpicIids < ActiveRecord::Migration[6.0] ...@@ -85,7 +85,7 @@ class DeduplicateEpicIids < ActiveRecord::Migration[6.0]
instance = subject.is_a?(::Class) ? nil : subject instance = subject.is_a?(::Class) ? nil : subject
subject.transaction(requires_new: true) do subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
......
...@@ -16,7 +16,7 @@ module Vulnerabilities ...@@ -16,7 +16,7 @@ module Vulnerabilities
vulnerability = Vulnerability.new vulnerability = Vulnerability.new
Vulnerabilities::Finding.transaction(requires_new: true) do Vulnerabilities::Finding.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
save_vulnerability(vulnerability, finding) save_vulnerability(vulnerability, finding)
Statistics::UpdateService.update_for(vulnerability) Statistics::UpdateService.update_for(vulnerability)
HistoricalStatistics::UpdateService.update_for(@project) HistoricalStatistics::UpdateService.update_for(@project)
......
...@@ -65,7 +65,7 @@ module EE ...@@ -65,7 +65,7 @@ module EE
end end
def self.safe_ensure_unique(retries: 0) def self.safe_ensure_unique(retries: 0)
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield yield
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
......
...@@ -73,7 +73,7 @@ module Gitlab ...@@ -73,7 +73,7 @@ module Gitlab
# violation. We can safely roll-back the nested transaction and perform # violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record. # a lookup instead to retrieve the record.
def create_record def create_record
subject.transaction(requires_new: true) do subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
shard_id = shards.fetch(name, nil) shard_id = shards.fetch(name, nil)
return shard_id if shard_id.present? return shard_id if shard_id.present?
Shard.transaction(requires_new: true) do Shard.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
create!(name) create!(name)
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
......
...@@ -122,7 +122,7 @@ module Gitlab ...@@ -122,7 +122,7 @@ module Gitlab
end end
def run_block_with_lock_timeout def run_block_with_lock_timeout
ActiveRecord::Base.transaction(requires_new: true) do ActiveRecord::Base.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'") execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'")
log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms) log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
......
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
class ActiveRecordSubtransactions < RuboCop::Cop::Cop
MSG = 'Subtransactions should not be used. ' \
'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346'
def_node_matcher :match_transaction_with_options, <<~PATTERN
(send _ :transaction (hash $...))
PATTERN
def_node_matcher :subtransaction_option?, <<~PATTERN
(pair (:sym :requires_new) (true))
PATTERN
def on_send(node)
match_transaction_with_options(node) do |option_nodes|
option_nodes.each do |option_node|
next unless subtransaction_option?(option_node)
add_offense(option_node)
end
end
end
end
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/performance/active_record_subtransactions'
RSpec.describe RuboCop::Cop::Performance::ActiveRecordSubtransactions do
subject(:cop) { described_class.new }
let(:message) { described_class::MSG }
context 'when calling #transaction with only requires_new: true' do
it 'registers an offense' do
expect_offense(<<~RUBY)
ApplicationRecord.transaction(requires_new: true) do
^^^^^^^^^^^^^^^^^^ #{message}
Project.create!(name: 'MyProject')
end
RUBY
end
end
context 'when passing multiple arguments to #transaction, including requires_new: true' do
it 'registers an offense' do
expect_offense(<<~RUBY)
ApplicationRecord.transaction(isolation: :read_committed, requires_new: true) do
^^^^^^^^^^^^^^^^^^ #{message}
Project.create!(name: 'MyProject')
end
RUBY
end
end
context 'when calling #transaction with requires_new: false' do
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
ApplicationRecord.transaction(requires_new: false) do
Project.create!(name: 'MyProject')
end
RUBY
end
end
context 'when calling #transaction with other options' do
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
ApplicationRecord.transaction(isolation: :read_committed) do
Project.create!(name: 'MyProject')
end
RUBY
end
end
context 'when calling #transaction with no arguments' do
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
ApplicationRecord.transaction do
Project.create!(name: 'MyProject')
end
RUBY
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment