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_
|
* Deprecated `Module#qualified_const_` in favour of the builtin Module#const_
|
||||||
methods.
|
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/anonymous'
|
||||||
require 'active_support/core_ext/module/reachable'
|
require 'active_support/core_ext/module/reachable'
|
||||||
require 'active_support/core_ext/module/attribute_accessors'
|
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/attr_internal'
|
||||||
require 'active_support/core_ext/module/concerning'
|
require 'active_support/core_ext/module/concerning'
|
||||||
require 'active_support/core_ext/module/delegation'
|
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'
|
require 'active_support/core_ext/module/delegation'
|
||||||
|
|
||||||
module ActiveSupport
|
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.
|
# This module is used to encapsulate access to thread local variables.
|
||||||
#
|
#
|
||||||
# Instead of polluting the thread locals namespace:
|
# 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