mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add thread_m/cattr_accessor/reader/writer suite of methods for declaring class and module variables that live per-thread
This commit is contained in:
parent
7a53a9c13c
commit
748b21db8f
5 changed files with 302 additions and 0 deletions
|
@ -1,3 +1,52 @@
|
|||
* Add thread_m/cattr_accessor/reader/writer suite of methods for declaring class and module variables that live per-thread.
|
||||
This makes it easy to declare per-thread globals that are encapsulated. Note: This is a sharp edge. A wild proliferation
|
||||
of globals is A Bad Thing. But like other sharp tools, when it's right, it's right.
|
||||
|
||||
Here's an example of a simple event tracking system where the object being tracked needs not pass a creator that it
|
||||
doesn't need itself along:
|
||||
|
||||
module Current
|
||||
thread_mattr_accessor :account
|
||||
thread_mattr_accessor :user
|
||||
|
||||
def self.reset() self.account = self.user = nil end
|
||||
end
|
||||
|
||||
class ApplicationController < ActiveController::Base
|
||||
before_action :set_current
|
||||
after_action { Current.reset }
|
||||
|
||||
private
|
||||
def set_current
|
||||
Current.account = Account.find(params[:account_id])
|
||||
Current.person = Current.account.people.find(params[:person_id])
|
||||
end
|
||||
end
|
||||
|
||||
class MessagesController < ApplicationController
|
||||
def create
|
||||
@message = Message.create!(message_params)
|
||||
end
|
||||
end
|
||||
|
||||
class Message < ApplicationRecord
|
||||
has_many :events
|
||||
after_create :track_created
|
||||
|
||||
private
|
||||
def track_created
|
||||
events.create! origin: self, action: :create
|
||||
end
|
||||
end
|
||||
|
||||
class Event < ApplicationRecord
|
||||
belongs_to :creator, class_name: 'Person'
|
||||
before_validation { self.creator ||= Current.person }
|
||||
end
|
||||
|
||||
*DHH*
|
||||
|
||||
|
||||
* Deprecated `Module#qualified_const_` in favour of the builtin Module#const_
|
||||
methods.
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ require 'active_support/core_ext/module/introspection'
|
|||
require 'active_support/core_ext/module/anonymous'
|
||||
require 'active_support/core_ext/module/reachable'
|
||||
require 'active_support/core_ext/module/attribute_accessors'
|
||||
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
||||
require 'active_support/core_ext/module/attr_internal'
|
||||
require 'active_support/core_ext/module/concerning'
|
||||
require 'active_support/core_ext/module/delegation'
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
require 'active_support/core_ext/array/extract_options'
|
||||
|
||||
# Extends the module object with class/module and instance accessors for
|
||||
# class/module attributes, just like the native attr* accessors for instance
|
||||
# attributes, but does so on a per-thread basis.
|
||||
#
|
||||
# So the values are scoped within the Thread.current space under the class name
|
||||
# of the module.
|
||||
class Module
|
||||
# Defines a per-thread class attribute and creates class and instance reader methods.
|
||||
# The underlying per-thread class variable is set to +nil+, if it is not previously defined.
|
||||
#
|
||||
# module Current
|
||||
# thread_mattr_reader :user
|
||||
# end
|
||||
#
|
||||
# Current.user # => nil
|
||||
# Thread.current[:attr_Current_user] = "DHH"
|
||||
# Current.user # => "DHH"
|
||||
#
|
||||
# The attribute name must be a valid method name in Ruby.
|
||||
#
|
||||
# module Foo
|
||||
# thread_mattr_reader :"1_Badname"
|
||||
# end
|
||||
# # => NameError: invalid attribute name: 1_Badname
|
||||
#
|
||||
# If you want to opt out the creation on the instance reader method, pass
|
||||
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_reader :user, instance_reader: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user # => NoMethodError
|
||||
def thread_mattr_reader(*syms)
|
||||
options = syms.extract_options!
|
||||
|
||||
syms.each do |sym|
|
||||
raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def self.#{sym}
|
||||
Thread.current[:"attr_#{name}_#{sym}"]
|
||||
end
|
||||
EOS
|
||||
|
||||
unless options[:instance_reader] == false || options[:instance_accessor] == false
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}
|
||||
Thread.current[:"attr_#{self.class.name}_#{sym}"]
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :thread_cattr_reader :thread_mattr_reader
|
||||
|
||||
# Defines a per-thread class attribute and creates a class and instance writer methods to
|
||||
# allow assignment to the attribute.
|
||||
#
|
||||
# module Current
|
||||
# thread_mattr_writer :user
|
||||
# end
|
||||
#
|
||||
# Current.user = "DHH"
|
||||
# Thread.current[:attr_Current_user] # => "DHH"
|
||||
#
|
||||
# If you want to opt out the instance writer method, pass
|
||||
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_writer :user, instance_writer: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user = "DHH" # => NoMethodError
|
||||
def thread_mattr_writer(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def self.#{sym}=(obj)
|
||||
Thread.current[:"attr_#{name}_#{sym}"] = obj
|
||||
end
|
||||
EOS
|
||||
|
||||
unless options[:instance_writer] == false || options[:instance_accessor] == false
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}=(obj)
|
||||
Thread.current[:"attr_#{self.class.name}_#{sym}"] = obj
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :thread_cattr_writer :thread_mattr_writer
|
||||
|
||||
# Defines both class and instance accessors for class attributes.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_accessor :user
|
||||
# end
|
||||
#
|
||||
# Current.user = "DHH"
|
||||
# Current.user # => "DHH"
|
||||
# Current.new.user # => "DHH"
|
||||
#
|
||||
# If a subclass changes the value then that will not change the value for
|
||||
# parent class. Similarly if parent class changes the value then that will not
|
||||
# change the value of subclasses either.
|
||||
#
|
||||
# class Customer < Account
|
||||
# end
|
||||
#
|
||||
# Customer.user = "CHH"
|
||||
# Account.user # => "DHH"
|
||||
#
|
||||
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
||||
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
||||
#
|
||||
# class Current
|
||||
# thread_mattr_accessor :user, instance_writer: false, instance_reader: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user = "DHH" # => NoMethodError
|
||||
# Current.new.user # => NoMethodError
|
||||
#
|
||||
# Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
|
||||
#
|
||||
# class Current
|
||||
# mattr_accessor :user, instance_accessor: false
|
||||
# end
|
||||
#
|
||||
# Current.new.user = "DHH" # => NoMethodError
|
||||
# Current.new.user # => NoMethodError
|
||||
def thread_mattr_accessor(*syms, &blk)
|
||||
thread_mattr_reader(*syms, &blk)
|
||||
thread_mattr_writer(*syms, &blk)
|
||||
end
|
||||
alias :thread_cattr_accessor :thread_mattr_accessor
|
||||
end
|
|
@ -1,6 +1,9 @@
|
|||
require 'active_support/core_ext/module/delegation'
|
||||
|
||||
module ActiveSupport
|
||||
# NOTE: This approach has been deprecated for end-user code in favor of thread_mattr_accessor and friends.
|
||||
# Please use that approach instead.
|
||||
#
|
||||
# This module is used to encapsulate access to thread local variables.
|
||||
#
|
||||
# Instead of polluting the thread locals namespace:
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
require 'abstract_unit'
|
||||
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
||||
|
||||
class ModuleAttributeAccessorTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@class = Class.new do
|
||||
thread_mattr_accessor :foo
|
||||
thread_mattr_accessor :bar, instance_writer: false
|
||||
thread_mattr_reader :shaq, instance_reader: false
|
||||
thread_mattr_accessor :camp, instance_accessor: false
|
||||
end
|
||||
|
||||
@object = @class.new
|
||||
end
|
||||
|
||||
def test_should_use_mattr_default
|
||||
Thread.new do
|
||||
assert_nil @class.foo
|
||||
assert_nil @object.foo
|
||||
end.join
|
||||
end
|
||||
|
||||
def test_should_set_mattr_value
|
||||
Thread.new do
|
||||
@class.foo = :test
|
||||
assert_equal :test, @class.foo
|
||||
|
||||
@class.foo = :test2
|
||||
assert_equal :test2, @class.foo
|
||||
end.join
|
||||
end
|
||||
|
||||
def test_should_not_create_instance_writer
|
||||
Thread.new do
|
||||
assert_respond_to @class, :foo
|
||||
assert_respond_to @class, :foo=
|
||||
assert_respond_to @object, :bar
|
||||
assert !@object.respond_to?(:bar=)
|
||||
end.join
|
||||
end
|
||||
|
||||
def test_should_not_create_instance_reader
|
||||
Thread.new do
|
||||
assert_respond_to @class, :shaq
|
||||
assert !@object.respond_to?(:shaq)
|
||||
end.join
|
||||
end
|
||||
|
||||
def test_should_not_create_instance_accessors
|
||||
Thread.new do
|
||||
assert_respond_to @class, :camp
|
||||
assert !@object.respond_to?(:camp)
|
||||
assert !@object.respond_to?(:camp=)
|
||||
end.join
|
||||
end
|
||||
|
||||
def test_values_should_not_bleed_between_threads
|
||||
threads = []
|
||||
threads << Thread.new do
|
||||
@class.foo = 'things'
|
||||
sleep 1
|
||||
assert_equal 'things', @class.foo
|
||||
end
|
||||
|
||||
threads << Thread.new do
|
||||
@class.foo = 'other things'
|
||||
sleep 1
|
||||
assert_equal 'other things', @class.foo
|
||||
end
|
||||
|
||||
threads << Thread.new do
|
||||
@class.foo = 'really other things'
|
||||
sleep 1
|
||||
assert_equal 'really other things', @class.foo
|
||||
end
|
||||
|
||||
threads.each { |t| t.join }
|
||||
end
|
||||
|
||||
def test_should_raise_name_error_if_attribute_name_is_invalid
|
||||
exception = assert_raises NameError do
|
||||
Class.new do
|
||||
thread_cattr_reader "1nvalid"
|
||||
end
|
||||
end
|
||||
assert_equal "invalid attribute name: 1nvalid", exception.message
|
||||
|
||||
exception = assert_raises NameError do
|
||||
Class.new do
|
||||
thread_cattr_writer "1nvalid"
|
||||
end
|
||||
end
|
||||
assert_equal "invalid attribute name: 1nvalid", exception.message
|
||||
|
||||
exception = assert_raises NameError do
|
||||
Class.new do
|
||||
thread_mattr_reader "1valid_part"
|
||||
end
|
||||
end
|
||||
assert_equal "invalid attribute name: 1valid_part", exception.message
|
||||
|
||||
exception = assert_raises NameError do
|
||||
Class.new do
|
||||
thread_mattr_writer "2valid_part"
|
||||
end
|
||||
end
|
||||
assert_equal "invalid attribute name: 2valid_part", exception.message
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue