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 +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
|