From 28f89b954a9845304b54e2de2f7c4ab27333d432 Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Mon, 8 Jun 2015 15:44:58 -0600 Subject: [PATCH 1/2] AtomicReference: update try_update API (resolves #330) --- CHANGELOG.md | 11 +++++--- .../atomic_reference/direct_update.rb | 25 ++++++++++++++++--- .../atomic/atomic_reference_spec.rb | 22 ++++++++++++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 868a1ba24..17566a645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ### Next Release v0.9.0 (Target Date: 7 June 2015) + +* Updated `AtomicReference` + - `AtomicReference#try_update` now simply returns instead of raising exception + - `AtomicReference#try_update!` was added to raise exceptions if an update + fails. Note: this is the same behavior as the old `try_update` * Pure Java implementations of - `AtomicBoolean` - `AtomicFixnum` @@ -58,7 +63,7 @@ - `Channel` - `Exchanger` - `LazyRegister` - - **new Future Framework** - unified + - **new Future Framework** - unified implementation of Futures and Promises which combines Features of previous `Future`, `Promise`, `IVar`, `Event`, `Probe`, `dataflow`, `Delay`, `TimerTask` into single framework. It uses extensively new synchronization layer to make all the paths **lock-free** with exception of blocking threads on `#wait`. @@ -80,7 +85,7 @@ - Add AbstractContext#default_executor to be able to override executor class wide - Add basic IO example - Documentation somewhat improved - - All messages should have same priority. It's now possible to send `actor << job1 << job2 << :terminate!` and + - All messages should have same priority. It's now possible to send `actor << job1 << job2 << :terminate!` and be sure that both jobs are processed first. * Refactored `Channel` to use newer synchronization objects * Added `#reset` and `#cancel` methods to `TimerSet` @@ -162,7 +167,7 @@ Please see the [roadmap](https://github.com/ruby-concurrency/concurrent-ruby/iss - `SerializedExecutionDelegator` for serializing *any* executor * Updated `Async` with serialized execution * Updated `ImmediateExecutor` and `PerThreadExecutor` with full executor service lifecycle -* Added a `Delay` to root `Actress` initialization +* Added a `Delay` to root `Actress` initialization * Minor bug fixes to thread pools * Refactored many intermittently failing specs * Removed Java interop warning `executor.rb:148 warning: ambiguous Java methods found, using submit(java.lang.Runnable)` diff --git a/lib/concurrent/atomic_reference/direct_update.rb b/lib/concurrent/atomic_reference/direct_update.rb index 8ac5e8871..fc9aac4bc 100644 --- a/lib/concurrent/atomic_reference/direct_update.rb +++ b/lib/concurrent/atomic_reference/direct_update.rb @@ -13,7 +13,7 @@ module AtomicDirectUpdate # Pass the current value to the given block, replacing it # with the block's result. May retry if the value changes # during the block's execution. - # + # # @yield [Object] Calculate a new value for the atomic reference using # given (old) value # @yieldparam [Object] old_value the starting value of the atomic reference @@ -27,9 +27,28 @@ def update # @!macro [attach] atomic_reference_method_try_update # # Pass the current value to the given block, replacing it + # with the block's result. Return nil if the update fails. + # + # @yield [Object] Calculate a new value for the atomic reference using + # given (old) value + # @yieldparam [Object] old_value the starting value of the atomic reference + # + # @return [Object] the new value, or nil if update failed + def try_update + old_value = get + new_value = yield old_value + + return unless compare_and_set old_value, new_value + + new_value + end + + # @!macro [attach] atomic_reference_method_try_update! + # + # Pass the current value to the given block, replacing it # with the block's result. Raise an exception if the update # fails. - # + # # @yield [Object] Calculate a new value for the atomic reference using # given (old) value # @yieldparam [Object] old_value the starting value of the atomic reference @@ -37,7 +56,7 @@ def update # @return [Object] the new value # # @raise [Concurrent::ConcurrentUpdateError] if the update fails - def try_update + def try_update! old_value = get new_value = yield old_value unless compare_and_set(old_value, new_value) diff --git a/spec/concurrent/atomic/atomic_reference_spec.rb b/spec/concurrent/atomic/atomic_reference_spec.rb index b50295124..dbecc66a7 100644 --- a/spec/concurrent/atomic/atomic_reference_spec.rb +++ b/spec/concurrent/atomic/atomic_reference_spec.rb @@ -33,6 +33,15 @@ expect(res).to eq 1001 end + specify :test_try_update_bang do + # use a number outside JRuby's fixnum cache range, to ensure identity is preserved + atomic = described_class.new(1000) + res = atomic.try_update! {|v| v + 1} + + expect(atomic.value).to eq 1001 + expect(res).to eq 1001 + end + specify :test_swap do atomic = described_class.new(1000) res = atomic.swap(1001) @@ -42,12 +51,21 @@ end specify :test_try_update_fails do + # use a number outside JRuby's fixnum cache range, to ensure identity is preserved + atomic = described_class.new(1000) + expect( + # assigning within block exploits implementation detail for test + atomic.try_update {|v| atomic.value = 1001 ; v + 1} + ).to be_falsey + end + + specify :test_try_update_bang_fails do # use a number outside JRuby's fixnum cache range, to ensure identity is preserved atomic = described_class.new(1000) expect { # assigning within block exploits implementation detail for test - atomic.try_update{|v| atomic.value = 1001 ; v + 1} - }.to raise_error(Concurrent::ConcurrentUpdateError) + atomic.try_update! {|v| atomic.value = 1001 ; v + 1} + }.to raise_error Concurrent::ConcurrentUpdateError end specify :test_update_retries do From a7dc6b1e3ceee1e6ae89bd32f162f808694484c3 Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Wed, 17 Jun 2015 13:25:25 -0600 Subject: [PATCH 2/2] AtomicReference: add note about try_update API --- lib/concurrent/atomic_reference/direct_update.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/concurrent/atomic_reference/direct_update.rb b/lib/concurrent/atomic_reference/direct_update.rb index fc9aac4bc..e25abe3cd 100644 --- a/lib/concurrent/atomic_reference/direct_update.rb +++ b/lib/concurrent/atomic_reference/direct_update.rb @@ -33,6 +33,10 @@ def update # given (old) value # @yieldparam [Object] old_value the starting value of the atomic reference # + # @note This method was altered to avoid raising an exception by default. + # Instead, this method now returns `nil` in case of failure. For more info, + # please see: https://github.com/ruby-concurrency/concurrent-ruby/pull/336 + # # @return [Object] the new value, or nil if update failed def try_update old_value = get @@ -53,6 +57,11 @@ def try_update # given (old) value # @yieldparam [Object] old_value the starting value of the atomic reference # + # @note This behavior mimics the behavior of the original + # `AtomicReference#try_update` API. The reason this was changed was to + # avoid raising exceptions (which are inherently slow) by default. For more + # info: https://github.com/ruby-concurrency/concurrent-ruby/pull/336 + # # @return [Object] the new value # # @raise [Concurrent::ConcurrentUpdateError] if the update fails