diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index 34fcbd124bb5..392ba99f4e4b 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -7,6 +7,7 @@ require 'active_support/core_ext/module/loading' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/model_naming' +require 'active_support/core_ext/module/synchronization' class Module include ActiveSupport::CoreExt::Module::ModelNaming diff --git a/activesupport/lib/active_support/core_ext/module/synchronization.rb b/activesupport/lib/active_support/core_ext/module/synchronization.rb new file mode 100644 index 000000000000..bf7740f851fd --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/synchronization.rb @@ -0,0 +1,35 @@ +class Module + # Synchronize access around a method, delegating synchronization to a + # particular mutex. A mutex (either a Mutex, or any object that responds to + # #synchronize and yields to a block) must be provided as a final :with option. + # The :with option should be a symbol or string, and can represent a method, + # constant, or instance or class variable. + # Example: + # class SharedCache + # @@lock = Mutex.new + # def expire + # ... + # end + # synchronize :expire, :with => :@@lock + # end + def synchronize(*methods) + options = methods.extract_options! + unless options.is_a?(Hash) && with = options[:with] + raise ArgumentError, "Synchronization needs a mutex. Supply an options hash with a :with key as the last argument (e.g. synchronize :hello, :with => :@mutex)." + end + + methods.each do |method| + if instance_methods.include?("#{method}_without_synchronization") + raise ArgumentError, "#{method} is already synchronized. Double synchronization is not currently supported." + end + module_eval(<<-EOS, __FILE__, __LINE__) + def #{method}_with_synchronization(*args, &block) + #{with}.synchronize do + #{method}_without_synchronization(*args,&block) + end + end + EOS + alias_method_chain method, :synchronization + end + end +end \ No newline at end of file diff --git a/activesupport/test/core_ext/module/synchronization_test.rb b/activesupport/test/core_ext/module/synchronization_test.rb new file mode 100644 index 000000000000..78be6b725fff --- /dev/null +++ b/activesupport/test/core_ext/module/synchronization_test.rb @@ -0,0 +1,57 @@ +require 'abstract_unit' + +class SynchronizationTest < Test::Unit::TestCase + def setup + @target = Class.new + @target.cattr_accessor :mutex, :instance_writer => false + @target.mutex = Mutex.new + @instance = @target.new + end + + def test_synchronize_aliases_method_chain_with_synchronize + @target.module_eval do + attr_accessor :value + synchronize :value, :with => :mutex + end + assert @instance.respond_to?(:value_with_synchronization) + assert @instance.respond_to?(:value_without_synchronization) + end + + def test_synchronize_does_not_change_behavior + @target.module_eval do + attr_accessor :value + synchronize :value, :with => :mutex + end + expected = "some state" + @instance.value = expected + assert_equal expected, @instance.value + end + + def test_synchronize_with_no_mutex_raises_an_argument_error + assert_raises(ArgumentError) do + @target.synchronize :to_s + end + end + + def test_double_synchronize_raises_an_argument_error + @target.synchronize :to_s, :with => :mutex + assert_raises(ArgumentError) do + @target.synchronize :to_s, :with => :mutex + end + end + + def test_mutex_is_entered_during_method_call + dummy = Object.new + def dummy.synchronize + @sync_count ||= 0 + @sync_count += 1 + yield + end + def dummy.sync_count; @sync_count; end + @target.mutex = dummy + @target.synchronize :to_s, :with => :mutex + @instance.to_s + @instance.to_s + assert_equal 2, dummy.sync_count + end +end \ No newline at end of file