closure_tree 3.6.9 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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