closure_tree 3.6.9 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -362,16 +362,47 @@ No. This gem's API is based on the assumption that each node has either 0 or 1 p
362
362
  The underlying closure tree structure will support multiple parents, but there would be many
363
363
  breaking-API changes to support it. I'm open to suggestions and pull requests.
364
364
 
365
+ ### How do I use this with test fixtures?
366
+
367
+ Test fixtures aren't going to be running your ```after_save``` hooks after inserting all your
368
+ fixture data, so you need to call ```.rebuild!``` before your test runs. There's an example in
369
+ the spec ```tag_spec.rb```:
370
+
371
+ ```ruby
372
+ describe "Tag with fixtures" do
373
+ fixtures :tags
374
+ before :each do
375
+ Tag.rebuild! # <- required if you use fixtures
376
+ end
377
+ ```
378
+
379
+ **However, if you're just starting with Rails, may I humbly suggest you adopt a factory library**,
380
+ rather than using fixtures? [Lots of people have written about this already](https://www.google.com/search?q=fixtures+versus+factories).
381
+
382
+
365
383
  ## Testing
366
384
 
367
385
  Closure tree is [tested under every combination](http://travis-ci.org/#!/mceachen/closure_tree) of
368
386
 
369
387
  * Ruby 1.8.7 and Ruby 1.9.3
370
388
  * The latest Rails 3.0, 3.1, and 3.2 branches, and
371
- * MySQL, PostgreSQL, & SQLite.
389
+ * MySQL and PostgreSQL. SQLite works in a single-threaded environment.
390
+
391
+ Assuming you're using [rbenv](https://github.com/sstephenson/rbenv), you can use ```tests.sh``` to
392
+ run the test matrix locally.
393
+
394
+ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
395
+ [known issue](https://github.com/rails/rails/issues/7538).
372
396
 
373
397
  ## Change log
374
398
 
399
+ ### 3.7.0
400
+
401
+ **Thread safety!**
402
+ * [Advisory locks](https://github.com/mceachen/with_advisory_lock) were
403
+ integrated with the class-level ```find_or_create_by_path``` and ```rebuild!```.
404
+ * Pessimistic locking is used by the instance-level ```find_or_create_by_path```.
405
+
375
406
  ### 3.6.9
376
407
 
377
408
  * [Don Morrison](https://github.com/elskwid) massaged the [#hash_tree](#nested-hashes) query to
@@ -1,3 +1,6 @@
1
+ require 'active_support'
1
2
  require 'closure_tree/acts_as_tree'
2
3
 
3
- ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
4
+ ActiveSupport.on_load :active_record do
5
+ ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
6
+ end
@@ -209,38 +209,38 @@ module ClosureTree
209
209
  path = path.is_a?(Enumerable) ? path.dup : [path]
210
210
  node = self
211
211
  while !path.empty? && node
212
- node = node.children.send("find_by_#{name_column}", path.shift)
212
+ node = node.children.where(name_sym => path.shift).first
213
213
  end
214
214
  node
215
215
  end
216
216
 
217
217
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
218
218
  def find_or_create_by_path(path, attributes = {})
219
- path = path.is_a?(Enumerable) ? path.dup : [path]
220
- node = self
221
- attrs = {}
222
- attrs[:type] = self.type if ct_subclass? && ct_has_type?
223
- path.each do |name|
224
- attrs[name_sym] = name
225
- child = node.children.where(attrs).first
226
- unless child
219
+ subpath = path.is_a?(Enumerable) ? path.dup : [path]
220
+ child_name = subpath.shift
221
+ return self unless child_name
222
+ child = transaction do
223
+ lock!
224
+ attrs = {name_sym => child_name}
225
+ attrs[:type] = self.type if ct_subclass? && ct_has_type?
226
+ self.children.where(attrs).first || begin
227
227
  child = self.class.new(attributes.merge(attrs))
228
- node.children << child
228
+ self.children << child
229
+ child
229
230
  end
230
- node = child
231
231
  end
232
- node
232
+ child.find_or_create_by_path(subpath, attributes)
233
233
  end
234
234
 
235
235
  def find_all_by_generation(generation_level)
236
236
  s = ct_base_class.joins(<<-SQL)
237
- INNER JOIN (
238
- SELECT descendant_id
239
- FROM #{quoted_hierarchy_table_name}
240
- WHERE ancestor_id = #{self.id}
241
- GROUP BY 1
242
- HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
243
- ) AS descendants ON (#{quoted_table_name}.#{ct_base_class.primary_key} = descendants.descendant_id)
237
+ INNER JOIN (
238
+ SELECT descendant_id
239
+ FROM #{quoted_hierarchy_table_name}
240
+ WHERE ancestor_id = #{self.id}
241
+ GROUP BY 1
242
+ HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
243
+ ) AS descendants ON (#{quoted_table_name}.#{ct_base_class.primary_key} = descendants.descendant_id)
244
244
  SQL
245
245
  order_option ? s.order(order_option) : s
246
246
  end
@@ -347,27 +347,35 @@ module ClosureTree
347
347
  # Rebuilds the hierarchy table based on the parent_id column in the database.
348
348
  # Note that the hierarchy table will be truncated.
349
349
  def rebuild!
350
- hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
351
- roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
350
+ with_advisory_lock("closure_tree.#{ct_class}.rebuild") do
351
+ transaction do
352
+ hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
353
+ roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
354
+ end
355
+ end
352
356
  nil
353
357
  end
354
358
 
355
359
  # Find the node whose +ancestry_path+ is +path+
356
360
  def find_by_path(path)
357
- root = roots.send("find_by_#{name_column}", path.shift)
358
- root.try(:find_by_path, path)
361
+ subpath = path.dup
362
+ root = roots.where(name_sym => subpath.shift).first
363
+ root.find_by_path(subpath) if root
359
364
  end
360
365
 
361
366
  # Find or create nodes such that the +ancestry_path+ is +path+
362
367
  def find_or_create_by_path(path, attributes = {})
363
- name = path.shift
364
- # shenanigans because find_or_create can't infer we want the same class as this:
365
- # Note that roots will already be constrained to this subclass (in the case of polymorphism):
366
- root = roots.send("find_by_#{name_column}", name)
367
- if root.nil?
368
- root = create!(attributes.merge(name_sym => name))
368
+ subpath = path.dup
369
+ root_name = subpath.shift
370
+ root = with_advisory_lock("closure_tree.#{ct_class}.find_or_create(#{root_name})") do
371
+ transaction do
372
+ # shenanigans because find_or_create can't infer we want the same class as this:
373
+ # Note that roots will already be constrained to this subclass (in the case of polymorphism):
374
+ roots.where(name_sym => root_name).first ||
375
+ create!(attributes.merge(name_sym => root_name))
376
+ end
369
377
  end
370
- root.find_or_create_by_path(path, attributes)
378
+ root.find_or_create_by_path(subpath, attributes)
371
379
  end
372
380
 
373
381
  def hash_tree_scope(limit_depth = nil)
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.6.9" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.7.0" unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -1,16 +1,19 @@
1
1
  sqlite3:
2
2
  adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
3
- database: closure_tree.sqlite3.db
4
- sqlite3mem:
5
- adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
6
- database: ":memory:"
7
- postgresql:
3
+ database: spec/sqlite3.db
4
+ pool: 50
5
+ timeout: 5000
6
+
7
+ pg:
8
8
  adapter: postgresql
9
9
  username: postgres
10
10
  database: closure_tree_test
11
11
  min_messages: ERROR
12
+ pool: 50
13
+
12
14
  mysql:
13
15
  adapter: mysql2
14
16
  host: localhost
15
17
  username: root
16
18
  database: closure_tree_test
19
+ pool: 50
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe "threadhot" do
4
+
5
+ before :each do
6
+ TagHierarchy.delete_all
7
+ Tag.delete_all
8
+ @iterations = 5
9
+ @workers = 8
10
+ @parent = nil
11
+ end
12
+
13
+ def find_or_create_at_even_second(run_at)
14
+ sleep(run_at - Time.now.to_f)
15
+ ActiveRecord::Base.connection.reconnect!
16
+ (@parent || Tag).find_or_create_by_path([run_at.to_s, :a, :b, :c].compact)
17
+ end
18
+
19
+ def run_workers
20
+ start_time = Time.now.to_i + 2
21
+ @times = @iterations.times.collect { |ea| start_time + (ea * 2) }
22
+ @names = @times.collect { |ea| ea.to_s }
23
+ @threads = @workers.times.collect do
24
+ Thread.new do
25
+ @times.each { |ea| find_or_create_at_even_second(ea) }
26
+ end
27
+ end
28
+ @threads.each { |ea| ea.join }
29
+ end
30
+
31
+
32
+ it "class method will not create dupes" do
33
+ run_workers
34
+ Tag.roots.collect { |ea| ea.name.to_i }.should =~ @times
35
+ # No dupe children:
36
+ %w(a b c).each do |ea|
37
+ Tag.find_all_by_name(ea).size.should == @iterations
38
+ end
39
+ end
40
+
41
+ it "instance method will not create dupes" do
42
+ @parent = Tag.create!(:name => "root")
43
+ run_workers
44
+ @parent.reload.children.collect { |ea| ea.name.to_i }.should =~ @times
45
+ Tag.find_all_by_name(@names).size.should == @iterations
46
+ %w(a b c).each do |ea|
47
+ Tag.find_all_by_name(ea).size.should == @iterations
48
+ end
49
+ end
50
+
51
+ it "creates dupe roots without advisory locks" do
52
+ # disable with_advisory_lock:
53
+ Tag.should_receive(:with_advisory_lock).any_number_of_times { |lock_name, &block| block.call }
54
+ run_workers
55
+ Tag.find_all_by_name(@names).size.should > @iterations
56
+ end
57
+
58
+ # SQLite doesn't like parallelism, and Rails 3.0 and 3.1 have known threading issues. SKIP.
59
+ end if ((ENV["DB"] != "sqlite3") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
@@ -11,8 +11,9 @@ require 'active_support'
11
11
  require 'active_model'
12
12
  require 'active_record'
13
13
  require 'action_controller' # rspec-rails needs this :(
14
-
14
+ require 'with_advisory_lock'
15
15
  require 'closure_tree'
16
+ require 'tmpdir'
16
17
 
17
18
  #log = Logger.new(STDOUT)
18
19
  #log.sev_threshold = Logger::DEBUG
@@ -20,7 +21,7 @@ require 'closure_tree'
20
21
 
21
22
  require 'yaml'
22
23
  require 'erb'
23
- ENV["DB"] ||= "sqlite3mem"
24
+ ENV["DB"] ||= "mysql"
24
25
  ActiveRecord::Base.table_name_prefix = ENV['DB_PREFIX'].to_s
25
26
  ActiveRecord::Base.table_name_suffix = ENV['DB_SUFFIX'].to_s
26
27
  ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
@@ -41,6 +42,8 @@ ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
41
42
  alias_method_chain :log, :query_append
42
43
  end
43
44
 
45
+ Thread.abort_on_exception = true
46
+
44
47
  RSpec.configure do |config|
45
48
  config.fixture_path = "#{plugin_test_dir}/fixtures"
46
49
  # true runs the tests 1 second faster, but then you can't
@@ -49,4 +52,10 @@ RSpec.configure do |config|
49
52
  config.after(:each) do
50
53
  DB_QUERIES.clear
51
54
  end
55
+ config.before(:all) do
56
+ ENV['FLOCK_DIR'] = Dir.mktmpdir
57
+ end
58
+ config.after(:all) do
59
+ FileUtils.remove_entry_secure ENV['FLOCK_DIR']
60
+ end
52
61
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.9
4
+ version: 3.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-31 00:00:00.000000000 Z
12
+ date: 2013-01-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: 3.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: with_advisory_lock
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  - !ruby/object:Gem::Dependency
31
47
  name: rake
32
48
  requirement: !ruby/object:Gem::Requirement
@@ -191,6 +207,7 @@ files:
191
207
  - spec/fixtures/tags.yml
192
208
  - spec/hash_tree_spec.rb
193
209
  - spec/label_spec.rb
210
+ - spec/parallel_spec.rb
194
211
  - spec/spec_helper.rb
195
212
  - spec/support/models.rb
196
213
  - spec/tag_spec.rb
@@ -209,7 +226,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
209
226
  version: '0'
210
227
  segments:
211
228
  - 0
212
- hash: -2239773870557793017
229
+ hash: 2666801847924685333
213
230
  required_rubygems_version: !ruby/object:Gem::Requirement
214
231
  none: false
215
232
  requirements:
@@ -218,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
235
  version: '0'
219
236
  segments:
220
237
  - 0
221
- hash: -2239773870557793017
238
+ hash: 2666801847924685333
222
239
  requirements: []
223
240
  rubyforge_project:
224
241
  rubygems_version: 1.8.23
@@ -233,6 +250,7 @@ test_files:
233
250
  - spec/fixtures/tags.yml
234
251
  - spec/hash_tree_spec.rb
235
252
  - spec/label_spec.rb
253
+ - spec/parallel_spec.rb
236
254
  - spec/spec_helper.rb
237
255
  - spec/support/models.rb
238
256
  - spec/tag_spec.rb