aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/lib/active_record/base.rb41
-rw-r--r--activerecord/test/cases/relation_scoping_test.rb18
-rw-r--r--activerecord/test/models/developer.rb9
3 files changed, 53 insertions, 15 deletions
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 102d8f4175..5a57e1bc70 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1206,6 +1206,14 @@ MSG
Thread.current["#{self}_current_scope"] = scope
end
+ def ignore_default_scope? #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"]
+ end
+
+ def ignore_default_scope=(ignore) #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"] = ignore
+ end
+
# Use this macro in your model to set a default scope for all operations on
# the model.
#
@@ -1259,24 +1267,27 @@ MSG
# The @ignore_default_scope flag is used to prevent an infinite recursion situation where
# a default scope references a scope which has a default scope which references a scope...
def build_default_scope #:nodoc:
- return if defined?(@ignore_default_scope) && @ignore_default_scope
- @ignore_default_scope = true
-
- if method(:default_scope).owner != Base.singleton_class
- default_scope
- elsif default_scopes.any?
- default_scopes.inject(relation) do |default_scope, scope|
- if scope.is_a?(Hash)
- default_scope.apply_finder_options(scope)
- elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(scope.call)
- else
- default_scope.merge(scope)
+ return if ignore_default_scope?
+
+ begin
+ self.ignore_default_scope = true
+
+ if method(:default_scope).owner != Base.singleton_class
+ default_scope
+ elsif default_scopes.any?
+ default_scopes.inject(relation) do |default_scope, scope|
+ if scope.is_a?(Hash)
+ default_scope.apply_finder_options(scope)
+ elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
+ default_scope.merge(scope.call)
+ else
+ default_scope.merge(scope)
+ end
end
end
+ ensure
+ self.ignore_default_scope = false
end
- ensure
- @ignore_default_scope = false
end
# Returns the class type of the record using the current module as a prefix. So descendants of
diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb
index 673aff403f..1e2093273e 100644
--- a/activerecord/test/cases/relation_scoping_test.rb
+++ b/activerecord/test/cases/relation_scoping_test.rb
@@ -524,4 +524,22 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
end
+
+ def test_default_scope_is_threadsafe
+ if in_memory_db?
+ skip "in memory db can't share a db between threads"
+ end
+
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ threads << Thread.new do
+ Thread.current[:long_default_scope] = true
+ assert_equal 1, ThreadsafeDeveloper.all.count
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.count
+ end
+ threads.each(&:join)
+ end
end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index f182a7fa97..4dc9fff9fd 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -227,3 +227,12 @@ class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base
default_scope OpenStruct.new(:call => includes(:projects))
end
+
+class ThreadsafeDeveloper < ActiveRecord::Base
+ self.table_name = 'developers'
+
+ def self.default_scope
+ sleep 0.05 if Thread.current[:long_default_scope]
+ limit(1)
+ end
+end