From b5610969508c51f2044f937417f11b35772e6995 Mon Sep 17 00:00:00 2001 From: Rahoul Baruah Date: Fri, 13 Oct 2023 09:43:52 +0100 Subject: [PATCH] Version 0.2.0 Added #then for promise-style resolution of values --- Gemfile.lock | 2 +- README.md | 32 ++++++++ .../standard-procedure-async-0.2.0.gem.sha512 | 1 + lib/standard_procedure/async/actor.rb | 3 - lib/standard_procedure/async/await.rb | 12 --- lib/standard_procedure/async/version.rb | 2 +- spec/standard_procedure/async/actor_spec.rb | 80 +++++++++---------- 7 files changed, 72 insertions(+), 60 deletions(-) create mode 100644 checksums/standard-procedure-async-0.2.0.gem.sha512 diff --git a/Gemfile.lock b/Gemfile.lock index c22340e..935e545 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - standard-procedure-async (0.1.3) + standard-procedure-async (0.2.0) concurrent-ruby (>= 1.0) GEM diff --git a/README.md b/README.md index 4d42bb2..6604cda 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,22 @@ This queueing has an important implication. If you are within an actor class, you can happily call async methods on other objects and `await` the results - because each actor has its own independent queue. But if you are calling your own methods, you must **never** call `await` (or call `value` on the returned message object). Picture this: + +```ruby +class ClassThatWillDeadlock + include StandardProcedure::Async::Actor + + async :start_processing do + do_some_complicated_calculation.value + end + + async :do_some_complicated_calculation do + sleep 1 + 2 + 2 + end +end +``` + - Your actor has two `async` methods - `start_processing` and `do_complicated_calculation` - The main thread calls `start_processing` - so a message gets added to the head of the queue. - The actor's internal thread starts working on the message at the head of the queue, and `_start_processing` (the actual implementation) is called. @@ -175,6 +191,22 @@ Picture this: To avoid this, if you ever need the return value from an internal `async` method, always call the implementation method: `_do_complicated_calculation` instead of `do_complicated_calculation`. This does not put anything on the queue and proceeds as a normal method call. +```ruby +class ClassThatWillNotDeadlock + include StandardProcedure::Async::Actor + + async :start_processing do + _do_some_complicated_calculation + end + + async :do_some_complicated_calculation do + sleep 1 + 2 + 2 + end +end +``` + + As a fail-safe, any calls to `value` will also time-out after 30 seconds, returning Concurrent::MVar::TIMEOUT. If you need to override the timeout value you can use `message.value(timeout: value_in_seconds)`. ### The rules of using actors diff --git a/checksums/standard-procedure-async-0.2.0.gem.sha512 b/checksums/standard-procedure-async-0.2.0.gem.sha512 new file mode 100644 index 0000000..06d4ea2 --- /dev/null +++ b/checksums/standard-procedure-async-0.2.0.gem.sha512 @@ -0,0 +1 @@ +e9ecad84981421f13a69b6fa77964a1331488ddeb9581dcb2381d7daa4f6ae712f4a33edd1291b50fb70a49896a14b9b84dc6df4001a5c834ed561173685ba30 diff --git a/lib/standard_procedure/async/actor.rb b/lib/standard_procedure/async/actor.rb index fd6ba16..71b29c5 100644 --- a/lib/standard_procedure/async/actor.rb +++ b/lib/standard_procedure/async/actor.rb @@ -60,10 +60,7 @@ def value(timeout: 30) alias_method :await, :value def then &block - puts "in then" block&.call value - puts "called handler" - return self end def call diff --git a/lib/standard_procedure/async/await.rb b/lib/standard_procedure/async/await.rb index db1aa14..50ea208 100644 --- a/lib/standard_procedure/async/await.rb +++ b/lib/standard_procedure/async/await.rb @@ -2,16 +2,4 @@ module Kernel def await &block block.call.value end - - def wait_for timeout = 30, &block - puts "WAITING" - timeout = timeout * 10 - counter = 0 - while !block.call do - puts "...waiting" - sleep 0.1 - counter += 1 - raise "Timeout" if counter >= timeout - end - end end diff --git a/lib/standard_procedure/async/version.rb b/lib/standard_procedure/async/version.rb index f66ca67..bad7ad4 100644 --- a/lib/standard_procedure/async/version.rb +++ b/lib/standard_procedure/async/version.rb @@ -2,6 +2,6 @@ module StandardProcedure module Async - VERSION = "0.1.3" + VERSION = "0.2.0" end end diff --git a/spec/standard_procedure/async/actor_spec.rb b/spec/standard_procedure/async/actor_spec.rb index f8da4f5..ccfb803 100644 --- a/spec/standard_procedure/async/actor_spec.rb +++ b/spec/standard_procedure/async/actor_spec.rb @@ -41,74 +41,68 @@ # test that do_something actually ran in a different thread expect(other_thread).not_to eq current_thread end - - it "accesses the result of an asynchronous method using #value" do - klass = Class.new do + + it "accesses the result of an asynchronous method using #value" do + klass = Class.new do include StandardProcedure::Async::Actor - + async :say_hello do sleep 0.1 "Hello" end end - + expect(klass.new.say_hello.value).to eq "Hello" end - it "accesses the result of an asynchronous method using #await" do - klass = Class.new do + it "accesses the result of an asynchronous method using #await" do + klass = Class.new do include StandardProcedure::Async::Actor - + async :say_hello do sleep 0.1 "Hello" end end - + expect(klass.new.say_hello.await).to eq "Hello" end - it "accesses the result of an asynchronous method using #get" do - klass = Class.new do + it "accesses the result of an asynchronous method using #get" do + klass = Class.new do include StandardProcedure::Async::Actor - + async :say_hello do sleep 0.1 "Hello" end end - + expect(klass.new.say_hello.get).to eq "Hello" end - # it "accesses the result of an asynchronous method using #then, yielding the result in the caller's thread" do - # current_thread = Thread.current.to_s - # other_thread = nil - # handler_thread = nil - # - # klass = Class.new do - # include StandardProcedure::Async::Actor - # - # async :do_something do - # puts "doing something" - # sleep 2 - # puts "done it" - # Thread.current.to_s - # end - # end - # - # puts Thread.current.to_s - # klass.new.do_something.then do |result| - # puts "in handler" - # puts Thread.current.to_s - # other_thread = result - # handler_thread = Thread.current.to_s - # puts "handler is done #{other_thread}" - # end - # - # expect(other_thread).to_not eq current_thread - # expect(handler_thread).to eq current_thread - # end + it "accesses the result of an asynchronous method using #then, yielding the result in the caller's thread" do + current_thread = Thread.current.to_s + other_thread = nil + handler_thread = nil + + klass = Class.new do + include StandardProcedure::Async::Actor + + async :do_something do + sleep 1 + Thread.current.to_s + end + end + + klass.new.do_something.then do |result| + other_thread = result + handler_thread = Thread.current.to_s + end + + expect(other_thread).to_not eq current_thread + expect(handler_thread).to eq current_thread + end it "adds multiple messages to a queue and performs them in order" do things = Concurrent::Array.new @@ -117,7 +111,7 @@ include StandardProcedure::Async::Actor async :do_something do |number| - sleep(0.3) if number % 2 == 0 + sleep(rand) things << number :done end @@ -138,7 +132,7 @@ include StandardProcedure::Async::Actor async :wait_for_ages do - sleep 10 # this will never finish + sleep 10 # this will never finish because we set the timeout to 1 "You will never see this" end end