112 lines
3.8 KiB
Ruby
112 lines
3.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Sidekiq
|
|
module Worker
|
|
EnqueueFromTransactionError = Class.new(StandardError)
|
|
|
|
def self.skipping_transaction_check(&block)
|
|
previous_skip_transaction_check = self.skip_transaction_check
|
|
Thread.current[:sidekiq_worker_skip_transaction_check] = true
|
|
yield
|
|
ensure
|
|
Thread.current[:sidekiq_worker_skip_transaction_check] = previous_skip_transaction_check
|
|
end
|
|
|
|
def self.skip_transaction_check
|
|
Thread.current[:sidekiq_worker_skip_transaction_check]
|
|
end
|
|
|
|
def self.inside_transaction?
|
|
::ApplicationRecord.inside_transaction? || ::Ci::ApplicationRecord.inside_transaction?
|
|
end
|
|
|
|
def self.raise_exception_for_being_inside_a_transaction?
|
|
!skip_transaction_check && inside_transaction?
|
|
end
|
|
|
|
def self.raise_inside_transaction_exception(cause:)
|
|
raise Sidekiq::Worker::EnqueueFromTransactionError, <<~MSG
|
|
#{cause} cannot be enqueued inside a transaction as this can lead to
|
|
race conditions when the worker runs before the transaction is committed and
|
|
tries to access a model that has not been saved yet.
|
|
|
|
Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead.
|
|
MSG
|
|
end
|
|
|
|
module ClassMethods
|
|
module NoEnqueueingFromTransactions
|
|
%i(perform_async perform_at perform_in).each do |name|
|
|
define_method(name) do |*args|
|
|
if Sidekiq::Worker.raise_exception_for_being_inside_a_transaction?
|
|
begin
|
|
Sidekiq::Worker.raise_inside_transaction_exception(cause: "#{self}.#{name}")
|
|
rescue Sidekiq::Worker::EnqueueFromTransactionError => e
|
|
Gitlab::AppLogger.error(e.message) if ::Rails.env.production?
|
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
|
end
|
|
end
|
|
|
|
super(*args)
|
|
end
|
|
end
|
|
end
|
|
|
|
prepend NoEnqueueingFromTransactions
|
|
end
|
|
end
|
|
end
|
|
|
|
# We deliver emails using the `deliver_later` method and it uses ActiveJob
|
|
# under the hood, which later processes the email via the defined ActiveJob adapter's `enqueue` method.
|
|
# For GitLab, the ActiveJob adapter is Sidekiq (in development and production environments).
|
|
# We need to set the following up to override the ActiveJob adapater
|
|
# so as to ensure that no mailer jobs are enqueued from within a transaction.
|
|
module ActiveJob
|
|
module QueueAdapters
|
|
module NoEnqueueingFromTransactions
|
|
%i(enqueue enqueue_at).each do |name|
|
|
define_method(name) do |*args|
|
|
if Sidekiq::Worker.raise_exception_for_being_inside_a_transaction?
|
|
begin
|
|
job = args.first
|
|
Sidekiq::Worker.raise_inside_transaction_exception(
|
|
cause: "The #{job.class} job, enqueued into the queue: #{job.queue_name}"
|
|
)
|
|
rescue Sidekiq::Worker::EnqueueFromTransactionError => e
|
|
Gitlab::AppLogger.error(e.message) if ::Rails.env.production?
|
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
|
end
|
|
end
|
|
|
|
super(*args)
|
|
end
|
|
end
|
|
end
|
|
|
|
# This adapter is used in development & production environments.
|
|
class SidekiqAdapter
|
|
prepend NoEnqueueingFromTransactions
|
|
end
|
|
|
|
# This adapter is used in test environment.
|
|
# If we don't override the test environment adapter,
|
|
# we won't be seeing any failing jobs during the CI run,
|
|
# even if we enqueue mailers from within a transaction.
|
|
class TestAdapter
|
|
prepend NoEnqueueingFromTransactions
|
|
end
|
|
end
|
|
end
|
|
|
|
module ActiveRecord
|
|
class Base
|
|
module SkipTransactionCheckAfterCommit
|
|
def committed!(*args, **kwargs)
|
|
Sidekiq::Worker.skipping_transaction_check { super }
|
|
end
|
|
end
|
|
|
|
prepend SkipTransactionCheckAfterCommit
|
|
end
|
|
end
|