1
0
Fork 0
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:
David Heinemeier Hansson 2015-12-17 14:28:19 +01:00
parent 7a53a9c13c
commit 748b21db8f
5 changed files with 302 additions and 0 deletions

View file

@ -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.

View file

@ -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'

View file

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

View file

@ -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:

View file

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