Project

General

Profile

Defect #39437 » 0001-work-around-nested-set-related-deadlocks-in-MySQL-Ma.patch

Jens Krämer, 2023-10-30 10:43

View differences:

lib/redmine/nested_set/issue_nested_set.rb
166 166
          # before locking
167 167
          sets_to_lock = [root_id, parent.try(:root_id)].compact.uniq
168 168
          self.class.reorder(:id).where(:root_id => sets_to_lock).lock(lock).ids
169
        elsif Redmine::Database.mysql?
170
          # (ab)use settings as a global lock to prevent deadlocks
171
          Setting.lock.ids
172

  
173
          # still lock the issues in question, for good measure
174
          sets_to_lock = [id, parent_id].compact
175
          inner_join_statement = self.class.select(:root_id).where(id: sets_to_lock).distinct(:root_id).to_sql
176
          self.class.reorder(:id).
177
            joins("INNER JOIN (#{inner_join_statement}) as i2 ON #{self.class.table_name}.root_id = i2.root_id").
178
            lock.ids
169 179
        else
170 180
          sets_to_lock = [id, parent_id].compact
171 181
          self.class.reorder(:id).where("root_id IN (SELECT root_id FROM #{self.class.table_name} WHERE id IN (?))", sets_to_lock).lock.ids
test/unit/issue_nested_set_concurrency_test.rb
29 29
  self.use_transactional_tests = false
30 30

  
31 31
  def setup
32
    skip if sqlite? || mysql?
32
    skip if sqlite?
33 33
    User.current = nil
34 34
    CustomField.delete_all
35 35
  end
......
74 74
    assert_equal (2..61).to_a, children_bounds
75 75
  end
76 76

  
77
  def test_concurrent_subtask_removal
78
    with_settings :notified_events => [] do
79
      root = Issue.generate!
80

  
81
      60.times do
82
        Issue.generate! :parent_issue_id => root.id
83
      end
84

  
85
      # pick 40 random subtask ids
86
      child_ids = Issue.where(root_id: root.id, parent_id: root.id).pluck(:id)
87
      ids_to_remove = child_ids.sample(40).shuffle
88
      ids_to_keep = child_ids - ids_to_remove
89

  
90
      # remove these from the set, using four parallel threads
91
      threads = []
92
      ids_to_remove.each_slice(10) do |ids|
93
        threads << Thread.new do
94
          ActiveRecord::Base.connection_pool.with_connection do
95
            begin
96
              ids.each do |id|
97
                Issue.find(id).update(parent_id: nil)
98
              end
99
            rescue => e
100
              Thread.current[:exception] = e.message
101
            end
102
          end
103
        end
104
      end
105

  
106
      threads.each do |thread|
107
        thread.join
108
        assert_nil thread[:exception]
109
      end
110

  
111
      assert_equal 20, Issue.where(parent_id: root.id).count
112
      Issue.where(id: ids_to_remove).each do |issue|
113
        assert_nil issue.parent_id
114
        assert_equal issue.id, issue.root_id
115
        assert_equal 1, issue.lft
116
        assert_equal 2, issue.rgt
117
      end
118

  
119
      root.reload
120
      assert_equal [1, 42], [root.lft, root.rgt]
121
      children_bounds = root.children.sort_by(&:lft).map {|c| [c.lft, c.rgt]}.flatten
122
      assert_equal (2..41).to_a, children_bounds
123

  
124
    end
125
  end
126

  
77 127
  private
78 128

  
79 129
  def threaded(count, &block)
(1-1/4)