Welcome to mirror list, hosted at ThFree Co, Russian Federation.

forbid_sidekiq_in_transactions.rb « initializers « config - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: c96524b824d60ed47fa43bc4c86cffe9e69b7214 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 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