Skip to content
Snippets Groups Projects
Commit 5ddaa4a3 authored by Patrick Bair's avatar Patrick Bair
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 116 additions and 19 deletions
Loading
Loading
@@ -712,3 +712,8 @@ QA/SelectorUsage:
- 'ee/spec/**/*.rb'
Exclude:
- 'spec/rubocop/**/*_spec.rb'
Performance/ActiveRecordSubtransactions:
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Loading
Loading
@@ -31,7 +31,7 @@ def self.pluck_primary_key
end
 
def self.safe_ensure_unique(retries: 0)
transaction(requires_new: true) do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield
end
rescue ActiveRecord::RecordNotUnique
Loading
Loading
@@ -55,7 +55,7 @@ def self.safe_find_or_create_by!(*args, &block)
# currently one third of the default 15-second timeout
def self.with_fast_read_statement_timeout(timeout_ms = 5000)
::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}")
 
yield
Loading
Loading
@@ -80,7 +80,7 @@ def self.optimized_safe_find_or_create_by(*args, &block)
#
# 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.
transaction(requires_new: true) { all.create(*args, &block) }
transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions
rescue ActiveRecord::RecordNotUnique
find_by(*args)
end
Loading
Loading
Loading
Loading
@@ -622,7 +622,7 @@ def instance_review_permitted?
def self.create_from_defaults
check_schema!
 
transaction(requires_new: true) do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
super
end
rescue ActiveRecord::RecordNotUnique
Loading
Loading
Loading
Loading
@@ -5,7 +5,7 @@ class MoveDeployKeysProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
 
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_deploy_keys_projects
remove_remaining_deploy_keys_projects if remove_remaining_elements
 
Loading
Loading
Loading
Loading
@@ -5,7 +5,7 @@ class MoveForksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
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
update_root_project
refresh_forks_count
Loading
Loading
Loading
Loading
@@ -5,7 +5,7 @@ class MoveLfsObjectsProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
 
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_lfs_objects_projects
remove_remaining_lfs_objects_project if remove_remaining_elements
 
Loading
Loading
Loading
Loading
@@ -5,7 +5,7 @@ class MoveNotificationSettingsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
 
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_notification_settings
remove_remaining_notification_settings if remove_remaining_elements
 
Loading
Loading
Loading
Loading
@@ -9,7 +9,7 @@ class MoveProjectAuthorizationsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
 
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_authorizations
 
remove_remaining_authorizations if remove_remaining_elements
Loading
Loading
Loading
Loading
@@ -9,7 +9,7 @@ class MoveProjectGroupLinksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
 
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_group_links
remove_remaining_project_group_links if remove_remaining_elements
 
Loading
Loading
Loading
Loading
@@ -9,7 +9,7 @@ class MoveProjectMembersService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
 
Project.transaction(requires_new: true) do
Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_members
remove_remaining_members if remove_remaining_elements
 
Loading
Loading
Loading
Loading
@@ -9,7 +9,7 @@ def execute(source_project, remove_remaining_elements: true)
 
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)
 
Project.reset_counters @project.id, :users_star_projects
Loading
Loading
Loading
Loading
@@ -39,7 +39,7 @@ def execute
private
 
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
 
migrate_records
Loading
Loading
Loading
Loading
@@ -85,7 +85,7 @@ def create_record
 
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!(
**scope,
usage: usage_value,
Loading
Loading
Loading
Loading
@@ -16,7 +16,7 @@ def execute
 
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)
Statistics::UpdateService.update_for(vulnerability)
HistoricalStatistics::UpdateService.update_for(@project)
Loading
Loading
Loading
Loading
@@ -65,7 +65,7 @@ def self.safe_find_or_create_by(*args)
end
 
def self.safe_ensure_unique(retries: 0)
transaction(requires_new: true) do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield
end
rescue ActiveRecord::RecordNotUnique
Loading
Loading
Loading
Loading
@@ -73,7 +73,7 @@ def usage_value
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
def create_record
subject.transaction(requires_new: true) do
subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!(
**scope,
usage: usage_value,
Loading
Loading
Loading
Loading
@@ -21,7 +21,7 @@ def find_shard_id(name)
shard_id = shards.fetch(name, nil)
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)
end
rescue ActiveRecord::RecordNotUnique
Loading
Loading
Loading
Loading
@@ -122,7 +122,7 @@ def run_block
end
 
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'")
 
log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
Loading
Loading
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment