Project

General

Profile

Defect #39437 » 0001-use-a-global-lock-to-work-around-deadlock-errors-wit.patch

Jens Krämer, 2023-11-01 08:30

View differences:

Gemfile
69 69
      case adapter
70 70
      when 'mysql2'
71 71
        gem "mysql2", "~> 0.5.0", :platforms => [:mri, :mingw, :x64_mingw]
72
        gem "with_advisory_lock"
72 73
      when /postgresql/
73 74
        gem 'pg', '~> 1.5.3', :platforms => [:mri, :mingw, :x64_mingw]
74 75
      when /sqlite3/
lib/redmine/nested_set/issue_nested_set.rb
51 51
      end
52 52

  
53 53
      def add_to_nested_set(lock=true)
54
        lock_nested_set if lock
54
        if lock
55
          lock_nested_set { add_to_nested_set_without_lock }
56
        else
57
          add_to_nested_set_without_lock
58
        end
59
      end
60

  
61
      def add_to_nested_set_without_lock
55 62
        parent.send :reload_nested_set_values
56 63
        self.root_id = parent.root_id
57 64
        self.lft = target_lft
......
73 80
      end
74 81

  
75 82
      def handle_parent_change
76
        lock_nested_set
77
        reload_nested_set_values
78
        if parent_id_was
79
          remove_from_nested_set
80
        end
81
        if parent
82
          move_to_nested_set
83
        lock_nested_set do
84
          reload_nested_set_values
85
          if parent_id_was
86
            remove_from_nested_set
87
          end
88
          if parent
89
            move_to_nested_set
90
          end
91
          reload_nested_set_values
83 92
        end
84
        reload_nested_set_values
85 93
      end
86 94

  
87 95
      def move_to_nested_set
......
124 132
      end
125 133

  
126 134
      def destroy_children
127
        unless @without_nested_set_update
128
          lock_nested_set
129
          reload_nested_set_values
130
        end
131
        children.each {|c| c.send :destroy_without_nested_set_update}
132
        reload
133
        unless @without_nested_set_update
134
          self.class.where(:root_id => root_id).where("lft > ? OR rgt > ?", lft, lft).update_all(
135
            [
136
              "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " +
137
                "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END",
138
              {:lft => lft, :shift => rgt - lft + 1}
139
            ]
140
          )
135
        if @without_nested_set_update
136
          children.each {|c| c.send :destroy_without_nested_set_update}
137
          reload
138
        else
139
          lock_nested_set do
140
            reload_nested_set_values
141
            children.each {|c| c.send :destroy_without_nested_set_update}
142
            reload
143
            self.class.where(:root_id => root_id).where("lft > ? OR rgt > ?", lft, lft).update_all(
144
              [
145
                "lft = CASE WHEN lft > :lft THEN lft - :shift ELSE lft END, " +
146
                  "rgt = CASE WHEN rgt > :lft THEN rgt - :shift ELSE rgt END",
147
                {:lft => lft, :shift => rgt - lft + 1}
148
              ]
149
            )
150
          end
141 151
        end
142 152
      end
143 153

  
......
166 176
          # before locking
167 177
          sets_to_lock = [root_id, parent.try(:root_id)].compact.uniq
168 178
          self.class.reorder(:id).where(:root_id => sets_to_lock).lock(lock).ids
179
          yield
180
        elsif Redmine::Database.mysql?
181
          # Use a global lock to prevent concurrent modifications - MySQL row locks are broken, this will run into
182
          # deadlock errors all the time otherwise.
183
          # Trying to lock just the sets in question (by basing the lock name on root_id and parent&.root_id) will run
184
          # into the same issues as the sqlserver branch above
185
          Issue.with_advisory_lock!("lock_issues", timeout_seconds: 30) do
186
            # still lock the issues in question, for good measure
187
            sets_to_lock = [id, parent_id].compact
188
            inner_join_statement = self.class.select(:root_id).where(id: sets_to_lock).distinct(:root_id).to_sql
189
            self.class.reorder(:id).
190
              joins("INNER JOIN (#{inner_join_statement}) as i2 ON #{self.class.table_name}.root_id = i2.root_id").
191
              lock.ids
192

  
193
            yield
194
          end
169 195
        else
170 196
          sets_to_lock = [id, parent_id].compact
171 197
          self.class.reorder(:id).where("root_id IN (SELECT root_id FROM #{self.class.table_name} WHERE id IN (?))", sets_to_lock).lock.ids
198
          yield
172 199
        end
173 200
      end
174 201

  
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)
(3-3/4)