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 +32 -1
- data/lib/closure_tree.rb +4 -1
- data/lib/closure_tree/acts_as_tree.rb +38 -30
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/database.yml +8 -5
- data/spec/parallel_spec.rb +59 -0
- data/spec/spec_helper.rb +11 -2
- metadata +22 -4
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
|
|
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
|
data/lib/closure_tree.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
attrs
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
228
|
+
self.children << child
|
|
229
|
+
child
|
|
229
230
|
end
|
|
230
|
-
node = child
|
|
231
231
|
end
|
|
232
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
358
|
-
root.
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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(
|
|
378
|
+
root.find_or_create_by_path(subpath, attributes)
|
|
371
379
|
end
|
|
372
380
|
|
|
373
381
|
def hash_tree_scope(limit_depth = nil)
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/db/database.yml
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
sqlite3:
|
|
2
2
|
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
|
|
3
|
-
database:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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/))
|
data/spec/spec_helper.rb
CHANGED
|
@@ -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"] ||= "
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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
|