| 
									
										
										
										
											2019-03-21 03:27:53 +00:00
										 |  |  | # frozen_string_literal: true | 
					
						
							| 
									
										
										
										
											2019-03-15 01:32:57 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  | # Redmine - project management software | 
					
						
							| 
									
										
										
										
											2024-02-26 22:55:54 +00:00
										 |  |  | # Copyright (C) 2006-  Jean-Philippe Lang | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  | # | 
					
						
							|  |  |  | # This program is free software; you can redistribute it and/or | 
					
						
							|  |  |  | # modify it under the terms of the GNU General Public License | 
					
						
							|  |  |  | # as published by the Free Software Foundation; either version 2 | 
					
						
							|  |  |  | # of the License, or (at your option) any later version. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  | # GNU General Public License for more details. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # You should have received a copy of the GNU General Public License | 
					
						
							|  |  |  | # along with this program; if not, write to the Free Software | 
					
						
							|  |  |  | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-01 07:13:39 +00:00
										 |  |  | require_relative '../test_helper' | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | class IssueNestedSetConcurrencyTest < ActiveSupport::TestCase | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |   self.use_transactional_tests = false | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   def setup | 
					
						
							| 
									
										
										
										
											2023-11-18 14:39:28 +00:00
										 |  |  |     skip if sqlite? | 
					
						
							| 
									
										
										
										
											2023-11-18 22:30:52 +00:00
										 |  |  |     if mysql? | 
					
						
							|  |  |  |       connection = ActiveRecord::Base.connection_db_config.configuration_hash.deep_dup | 
					
						
							| 
									
										
										
										
											2023-11-19 07:10:53 +00:00
										 |  |  |       connection[:variables] = mysql8? ? { transaction_isolation: "READ-COMMITTED" } : { tx_isolation: "READ-COMMITTED" } | 
					
						
							| 
									
										
										
										
											2023-11-18 22:30:52 +00:00
										 |  |  |       ActiveRecord::Base.establish_connection connection | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2018-12-16 17:23:31 +00:00
										 |  |  |     User.current = nil | 
					
						
							| 
									
										
										
										
											2024-11-28 10:11:49 +00:00
										 |  |  |     CustomField.destroy_all | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def teardown | 
					
						
							|  |  |  |     Issue.delete_all | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def test_concurrency | 
					
						
							| 
									
										
										
										
											2015-02-18 14:20:47 +00:00
										 |  |  |     # Generates an issue and destroys it in order | 
					
						
							|  |  |  |     # to load all needed classes before starting threads | 
					
						
							|  |  |  |     i = Issue.generate! | 
					
						
							|  |  |  |     i.destroy | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-18 14:20:47 +00:00
										 |  |  |     root = Issue.generate! | 
					
						
							|  |  |  |     assert_difference 'Issue.count', 60 do | 
					
						
							|  |  |  |       threaded(3) do | 
					
						
							|  |  |  |         10.times do | 
					
						
							|  |  |  |           i = Issue.generate! :parent_issue_id => root.id | 
					
						
							|  |  |  |           c1 = Issue.generate! :parent_issue_id => i.id | 
					
						
							|  |  |  |           c2 = Issue.generate! :parent_issue_id => i.id | 
					
						
							|  |  |  |           c3 = Issue.generate! :parent_issue_id => i.id | 
					
						
							|  |  |  |           c2.reload.destroy | 
					
						
							|  |  |  |           c1.reload.destroy | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-18 16:36:23 +00:00
										 |  |  |   def test_concurrent_subtasks_creation | 
					
						
							|  |  |  |     root = Issue.generate! | 
					
						
							|  |  |  |     assert_difference 'Issue.count', 30 do | 
					
						
							|  |  |  |       threaded(3) do | 
					
						
							|  |  |  |         10.times do | 
					
						
							|  |  |  |           Issue.generate! :parent_issue_id => root.id | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     root.reload | 
					
						
							|  |  |  |     assert_equal [1, 62], [root.lft, root.rgt] | 
					
						
							|  |  |  |     children_bounds = root.children.sort_by(&:lft).map {|c| [c.lft, c.rgt]}.flatten | 
					
						
							|  |  |  |     assert_equal (2..61).to_a, children_bounds | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-18 22:27:35 +00:00
										 |  |  |   def test_concurrent_subtask_removal | 
					
						
							|  |  |  |     with_settings :notified_events => [] do | 
					
						
							|  |  |  |       root = Issue.generate! | 
					
						
							|  |  |  |       60.times do | 
					
						
							|  |  |  |         Issue.generate! :parent_issue_id => root.id | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       # pick 40 random subtask ids | 
					
						
							|  |  |  |       child_ids = Issue.where(root_id: root.id, parent_id: root.id).pluck(:id) | 
					
						
							|  |  |  |       ids_to_remove = child_ids.sample(40).shuffle | 
					
						
							|  |  |  |       ids_to_keep = child_ids - ids_to_remove | 
					
						
							|  |  |  |       # remove these from the set, using four parallel threads | 
					
						
							|  |  |  |       threads = [] | 
					
						
							|  |  |  |       ids_to_remove.each_slice(10) do |ids| | 
					
						
							|  |  |  |         threads << Thread.new do | 
					
						
							|  |  |  |           ActiveRecord::Base.connection_pool.with_connection do | 
					
						
							|  |  |  |             begin | 
					
						
							|  |  |  |               ids.each do |id| | 
					
						
							|  |  |  |                 Issue.find(id).update(parent_id: nil) | 
					
						
							|  |  |  |               end | 
					
						
							|  |  |  |             rescue => e | 
					
						
							|  |  |  |               Thread.current[:exception] = e.message | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       threads.each do |thread| | 
					
						
							|  |  |  |         thread.join | 
					
						
							|  |  |  |         assert_nil thread[:exception] | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       assert_equal 20, Issue.where(parent_id: root.id).count | 
					
						
							|  |  |  |       Issue.where(id: ids_to_remove).each do |issue| | 
					
						
							|  |  |  |         assert_nil issue.parent_id | 
					
						
							|  |  |  |         assert_equal issue.id, issue.root_id | 
					
						
							|  |  |  |         assert_equal 1, issue.lft | 
					
						
							|  |  |  |         assert_equal 2, issue.rgt | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       root.reload | 
					
						
							|  |  |  |       assert_equal [1, 42], [root.lft, root.rgt] | 
					
						
							|  |  |  |       children_bounds = root.children.sort_by(&:lft).map {|c| [c.lft, c.rgt]}.flatten | 
					
						
							|  |  |  |       assert_equal (2..41).to_a, children_bounds | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-18 14:20:47 +00:00
										 |  |  |   private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-21 00:46:15 +00:00
										 |  |  |   def threaded(count, &) | 
					
						
							| 
									
										
										
										
											2015-02-18 14:20:47 +00:00
										 |  |  |     with_settings :notified_events => [] do | 
					
						
							|  |  |  |       threads = [] | 
					
						
							|  |  |  |       count.times do |i| | 
					
						
							|  |  |  |         threads << Thread.new(i) do | 
					
						
							|  |  |  |           ActiveRecord::Base.connection_pool.with_connection do | 
					
						
							|  |  |  |             begin | 
					
						
							|  |  |  |               yield | 
					
						
							| 
									
										
										
										
											2019-05-25 06:50:25 +00:00
										 |  |  |             rescue => e | 
					
						
							| 
									
										
										
										
											2015-02-18 14:20:47 +00:00
										 |  |  |               Thread.current[:exception] = e.message | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |             end | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2015-02-18 14:20:47 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |       threads.each do |thread| | 
					
						
							|  |  |  |         thread.join | 
					
						
							|  |  |  |         assert_nil thread[:exception] | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |