From bebcdcc6b0cc13a48706eec2c6b98b83705808d7 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 7 Jan 2020 21:11:00 +1100 Subject: [PATCH] feat: add supports for timeouts improved code gen by not initializing `Generic` --- shard.yml | 6 ++++- spec/timeout_spec.cr | 23 ++++++++++++++++++ src/promise.cr | 42 ++++++++++++++++++++++++++------- src/promise/deferred_promise.cr | 3 +-- src/promise/implicit_defer.cr | 3 +-- src/promise/resolved_promise.cr | 3 +-- 6 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 spec/timeout_spec.cr diff --git a/shard.yml b/shard.yml index 0d7eeb1..a0e5e94 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,9 @@ name: promise -version: 2.0.1 +version: 2.1.0 + +dependencies: + tasker: + github: spider-gazelle/tasker development_dependencies: ameba: diff --git a/spec/timeout_spec.cr b/spec/timeout_spec.cr new file mode 100644 index 0000000..637698e --- /dev/null +++ b/spec/timeout_spec.cr @@ -0,0 +1,23 @@ +require "spec" +require "../src/promise" + +describe "promise timeouts" do + it "should timeout a promise" do + p = Promise.new(Symbol, timeout: 2.milliseconds) + expect_raises(Promise::Timeout) { p.get } + end + + it "should timeout a defer" do + p = Promise.defer(timeout: 1.millisecond) { sleep 1; "p1 wins" } + expect_raises(Promise::Timeout) { p.get } + end + + it "should timeout a race" do + expect_raises(Promise::Timeout) do + Promise.race( + Promise.defer(timeout: 1.millisecond) { sleep 1; "p1 wins" }, + Promise.defer { sleep 1; "p2 wins" } + ).get + end + end +end diff --git a/src/promise.cr b/src/promise.cr index 3a8e58d..101b1cf 100644 --- a/src/promise.cr +++ b/src/promise.cr @@ -1,3 +1,5 @@ +require "tasker" + abstract class Promise class Generic(Output) macro get_type_var @@ -15,8 +17,38 @@ abstract class Promise end end - macro new(type) - ::Promise::DeferredPromise({{type.id}}).new + class Timeout < Exception + end + + macro new(type, timeout = nil) + {% if timeout %} + begin + %promise = ::Promise::DeferredPromise({{type.id}}).new + %task = Tasker.instance.in({{timeout}}) { %promise.reject(::Promise::Timeout.new("operation timeout")) } + %promise.finally { %task.cancel } + %promise + end + {% else %} + ::Promise::DeferredPromise({{type.id}}).new + {% end %} + end + + # Execute code in the next tick of the event loop + # and return a promise for obtaining the value + macro defer(same_thread = false, timeout = nil, &block) + begin + %promise = ::Promise::ImplicitDefer.new({{same_thread}}) { + {{block.body}} + }.execute! + + {% if timeout %} + %task = Tasker.instance.in({{timeout}}) { %promise.reject(::Promise::Timeout.new("operation timeout")) } + %promise.finally { |err| %task.cancel unless err.is_a?(::Promise::Timeout) } + %promise + {% end %} + + %promise + end end macro reject(type, reason) @@ -42,12 +74,6 @@ abstract class Promise ::Promise::ResolvedPromise.new(value) end - # Execute code in the next tick of the event loop - # and return a promise for obtaining the value - def self.defer(same_thread = false, &block : -> _) - ImplicitDefer.new(same_thread, &block).execute! - end - macro map(collection, same_thread = false, &block) %promise_collection = {{collection}}.map do |{{*block.args}}| ::Promise.defer(same_thread: {{same_thread}}) do diff --git a/src/promise/deferred_promise.cr b/src/promise/deferred_promise.cr index a971c46..b45ae38 100644 --- a/src/promise/deferred_promise.cr +++ b/src/promise/deferred_promise.cr @@ -10,8 +10,7 @@ class Promise::DeferredPromise(Input) < Promise end def promise_execute - generic_type = Generic(Output).new.type_var - promise = DeferredPromise(typeof(generic_type)).new + promise = DeferredPromise(typeof(Generic(Output).new.type_var)).new execute = Proc(Input, Nil).new do |value| begin promise.resolve(@callback.call(value)) diff --git a/src/promise/implicit_defer.cr b/src/promise/implicit_defer.cr index bc1dab1..7bd55c5 100644 --- a/src/promise/implicit_defer.cr +++ b/src/promise/implicit_defer.cr @@ -6,8 +6,7 @@ class Promise::ImplicitDefer(Output) def execute! # Replace NoReturn with Nil if the block will always `raise` an error - generic_type = Generic(Output).new - promise = DeferredPromise(typeof(generic_type.type_var)).new + promise = DeferredPromise(typeof(Generic(Output).new.type_var)).new spawn(same_thread: @same_thread) do begin diff --git a/src/promise/resolved_promise.cr b/src/promise/resolved_promise.cr index 1017d59..154e7ca 100644 --- a/src/promise/resolved_promise.cr +++ b/src/promise/resolved_promise.cr @@ -9,8 +9,7 @@ class Promise::ResolvedPromise(Input) < Promise::DeferredPromise(Input) def execute! # Replace NoReturn with Nil if the block will always `raise` an error - generic_type = Generic(Output).new.type_var - promise = DeferredPromise(typeof(generic_type)).new + promise = DeferredPromise(typeof(Generic(Output).new.type_var)).new spawn(same_thread: true) do begin