aws-flow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +8 -0
- data/LICENSE.TXT +15 -0
- data/NOTICE.TXT +14 -0
- data/Rakefile +39 -0
- data/aws-flow-core/Gemfile +9 -0
- data/aws-flow-core/LICENSE.TXT +15 -0
- data/aws-flow-core/NOTICE.TXT +14 -0
- data/aws-flow-core/Rakefile +27 -0
- data/aws-flow-core/aws-flow-core.gemspec +12 -0
- data/aws-flow-core/lib/aws/flow.rb +26 -0
- data/aws-flow-core/lib/aws/flow/async_backtrace.rb +134 -0
- data/aws-flow-core/lib/aws/flow/async_scope.rb +195 -0
- data/aws-flow-core/lib/aws/flow/begin_rescue_ensure.rb +386 -0
- data/aws-flow-core/lib/aws/flow/fiber.rb +77 -0
- data/aws-flow-core/lib/aws/flow/flow_utils.rb +50 -0
- data/aws-flow-core/lib/aws/flow/future.rb +109 -0
- data/aws-flow-core/lib/aws/flow/implementation.rb +151 -0
- data/aws-flow-core/lib/aws/flow/simple_dfa.rb +85 -0
- data/aws-flow-core/lib/aws/flow/tasks.rb +405 -0
- data/aws-flow-core/test/aws/async_backtrace_spec.rb +41 -0
- data/aws-flow-core/test/aws/async_scope_spec.rb +118 -0
- data/aws-flow-core/test/aws/begin_rescue_ensure_spec.rb +665 -0
- data/aws-flow-core/test/aws/external_task_spec.rb +197 -0
- data/aws-flow-core/test/aws/factories.rb +52 -0
- data/aws-flow-core/test/aws/fiber_condition_variable_spec.rb +163 -0
- data/aws-flow-core/test/aws/fiber_spec.rb +78 -0
- data/aws-flow-core/test/aws/flow_spec.rb +255 -0
- data/aws-flow-core/test/aws/future_spec.rb +210 -0
- data/aws-flow-core/test/aws/rubyflow.rb +22 -0
- data/aws-flow-core/test/aws/simple_dfa_spec.rb +63 -0
- data/aws-flow-core/test/aws/spec_helper.rb +36 -0
- data/aws-flow.gemspec +13 -0
- data/lib/aws/decider.rb +67 -0
- data/lib/aws/decider/activity.rb +408 -0
- data/lib/aws/decider/activity_definition.rb +111 -0
- data/lib/aws/decider/async_decider.rb +673 -0
- data/lib/aws/decider/async_retrying_executor.rb +153 -0
- data/lib/aws/decider/data_converter.rb +40 -0
- data/lib/aws/decider/decider.rb +511 -0
- data/lib/aws/decider/decision_context.rb +60 -0
- data/lib/aws/decider/exceptions.rb +178 -0
- data/lib/aws/decider/executor.rb +149 -0
- data/lib/aws/decider/flow_defaults.rb +70 -0
- data/lib/aws/decider/generic_client.rb +178 -0
- data/lib/aws/decider/history_helper.rb +173 -0
- data/lib/aws/decider/implementation.rb +82 -0
- data/lib/aws/decider/options.rb +607 -0
- data/lib/aws/decider/state_machines.rb +373 -0
- data/lib/aws/decider/task_handler.rb +76 -0
- data/lib/aws/decider/task_poller.rb +207 -0
- data/lib/aws/decider/utilities.rb +187 -0
- data/lib/aws/decider/worker.rb +324 -0
- data/lib/aws/decider/workflow_client.rb +374 -0
- data/lib/aws/decider/workflow_clock.rb +104 -0
- data/lib/aws/decider/workflow_definition.rb +101 -0
- data/lib/aws/decider/workflow_definition_factory.rb +53 -0
- data/lib/aws/decider/workflow_enabled.rb +26 -0
- data/test/aws/decider_spec.rb +1299 -0
- data/test/aws/factories.rb +45 -0
- data/test/aws/integration_spec.rb +3108 -0
- data/test/aws/spec_helper.rb +23 -0
- metadata +138 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
# This file simply contains definitions and functions useful to the overall running of flow
|
17
|
+
module AWS
|
18
|
+
module Flow
|
19
|
+
module Core
|
20
|
+
class IllegalStateException < Exception; end
|
21
|
+
class CancellationException < Exception
|
22
|
+
attr_accessor :reason, :details
|
23
|
+
def initialize(reason = nil, details = nil)
|
24
|
+
@reason = reason
|
25
|
+
@details = details
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_backtrace(parent_backtrace)
|
30
|
+
# 1 frame for the function that actually removes the stack traces
|
31
|
+
# 1 frame for the function that calls into the function that removes
|
32
|
+
# frames in AsyncBacktrace
|
33
|
+
# 1 frame for the call into this function
|
34
|
+
# 1 frame for the initialize call of the BRE or External Task
|
35
|
+
# 1 frame for the new call into the BRE or ET
|
36
|
+
# 1 frame for the AsyncScope initialize that the BRE/ET has to be in
|
37
|
+
|
38
|
+
# "./lib/aws/rubyflow/asyncBacktrace.rb:75:in `caller'"
|
39
|
+
# "./lib/aws/rubyflow/asyncBacktrace.rb:21:in `create'"
|
40
|
+
# "./lib/aws/rubyflow/flow.rb:16:in `make_backtrace'"
|
41
|
+
# "./lib/aws/rubyflow/flow.rb:103:in `initialize'"
|
42
|
+
# "./lib/aws/rubyflow/asyncScope.rb:17:in `new'"
|
43
|
+
# "./lib/aws/rubyflow/asyncScope.rb:17:in `initialize'"
|
44
|
+
|
45
|
+
frames_to_skip = 7
|
46
|
+
backtrace = AsyncBacktrace.create(parent_backtrace, frames_to_skip)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
# This file contains our Future implementation, which allows us to have asynchronous, blocking promises
|
17
|
+
|
18
|
+
module AWS
|
19
|
+
module Flow
|
20
|
+
module Core
|
21
|
+
class AlreadySetException < Exception; end
|
22
|
+
|
23
|
+
# A Future represents the result of an asynchronous computation. Methods are
|
24
|
+
# provided to check if the computation is complete(Future#set), to wait for
|
25
|
+
# its completion(Future#wait), and to retrieve the result of the
|
26
|
+
# computation(Future#get). The result can only be retrieved using method get
|
27
|
+
# when the computation has completed, blocking if necessary until it is
|
28
|
+
# ready. This is okay, however, because it will block that Fiber, and
|
29
|
+
# another Fiber will start executing
|
30
|
+
class Future
|
31
|
+
|
32
|
+
# Sets the value of the future, and notifies all of the Fibers that tried
|
33
|
+
# to get when this future wasn't ready.
|
34
|
+
def set(result=nil)
|
35
|
+
raise AlreadySetException if @set
|
36
|
+
@set = true
|
37
|
+
@result = result
|
38
|
+
@conditional.broadcast if @conditional
|
39
|
+
@listeners.each { |b| b.call(self) } if @listeners
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Blocks if Future is not set
|
44
|
+
# raises CancellationError when task is cancelled
|
45
|
+
def get
|
46
|
+
until @set
|
47
|
+
@conditional ||= FiberConditionVariable.new
|
48
|
+
@conditional.wait
|
49
|
+
end
|
50
|
+
@result
|
51
|
+
end
|
52
|
+
|
53
|
+
def set?
|
54
|
+
@set
|
55
|
+
end
|
56
|
+
|
57
|
+
def unset
|
58
|
+
@set = nil
|
59
|
+
@result = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add a callback, block, which will fire when the future is set
|
63
|
+
def on_set(&block)
|
64
|
+
@listeners ||= []
|
65
|
+
# TODO probably want to use lambda here
|
66
|
+
@listeners << block
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Based on the ruby core source:
|
71
|
+
# https://github.com/ruby/ruby/blob/trunk/lib/thread.rb
|
72
|
+
class FiberConditionVariable
|
73
|
+
#
|
74
|
+
# Creates a new ConditionVariable
|
75
|
+
#
|
76
|
+
def initialize
|
77
|
+
@waiters = []
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# Have the current fiber wait on this condition variable, and wake up when
|
82
|
+
# the FiberConditionVariable is signalled/broadcaster
|
83
|
+
def wait
|
84
|
+
fiber = ::Fiber.current
|
85
|
+
@waiters << fiber
|
86
|
+
Fiber.yield
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Wakes up the first fiber in line waiting for this lock.
|
92
|
+
#
|
93
|
+
def signal
|
94
|
+
t = @waiters.shift
|
95
|
+
t.schedule if t && t.alive?
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Wakes up all fibers waiting for this lock.
|
101
|
+
#
|
102
|
+
def broadcast
|
103
|
+
signal until @waiters.empty?
|
104
|
+
self
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
#++
|
15
|
+
|
16
|
+
# This file contains the externally visible parts of flow that are expected to be used by customers of flow
|
17
|
+
module AWS
|
18
|
+
module Flow
|
19
|
+
module Core
|
20
|
+
class NoContextException < Exception; end
|
21
|
+
|
22
|
+
# @param block
|
23
|
+
# The block of code to be executed when the task is run.
|
24
|
+
#
|
25
|
+
# @raise [NoContextException]
|
26
|
+
# If the current fiber does not respond to {#__context__}.
|
27
|
+
#
|
28
|
+
# @return [Future]
|
29
|
+
# The tasks result, which is a {Future}.
|
30
|
+
#
|
31
|
+
def task(future = nil, &block)
|
32
|
+
fiber = ::Fiber.current
|
33
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
34
|
+
context = fiber.__context__
|
35
|
+
t = Task.new(nil, &block)
|
36
|
+
task_context = TaskContext.new(:parent => context.get_closest_containing_scope, :task => t)
|
37
|
+
context << t
|
38
|
+
t.result
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# @param block
|
43
|
+
# The block of code to be executed when the daemon task is run.
|
44
|
+
#
|
45
|
+
# @return [Future]
|
46
|
+
# The tasks result, which is a {Future}.
|
47
|
+
#
|
48
|
+
# @raise [NoContextException]
|
49
|
+
# If the current fiber does not respond to {#__context__}.
|
50
|
+
#
|
51
|
+
def daemon_task(&block)
|
52
|
+
fiber = ::Fiber.current
|
53
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
54
|
+
context = fiber.__context__
|
55
|
+
t = DaemonTask.new(nil, &block)
|
56
|
+
task_context = TaskContext.new(:parent => context.get_closest_containing_scope, :task => t)
|
57
|
+
context << t
|
58
|
+
t.result
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# @param block
|
63
|
+
# The block of code to be executed when the external task is run.
|
64
|
+
#
|
65
|
+
# @return [nil]
|
66
|
+
#
|
67
|
+
# @raise [NoContextException]
|
68
|
+
# If the current fiber does not respond to {#__context__}.
|
69
|
+
#
|
70
|
+
def external_task(&block)
|
71
|
+
fiber = ::Fiber.current
|
72
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
73
|
+
context = fiber.__context__
|
74
|
+
t = ExternalTask.new(:parent => context.get_closest_containing_scope, &block)
|
75
|
+
task_context = TaskContext.new(:parent => context.get_closest_containing_scope, :task => t)
|
76
|
+
context << t
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
#
|
82
|
+
#
|
83
|
+
# * *Args* :
|
84
|
+
# - block -> a block, which is passed in a BeginRescueEnsureWrapper, and which will define the BeginRescueEnsure#begin, BeginRescueEnsure#rescue, and BeginRescueEnsure#ensure methods
|
85
|
+
# * *Returns* :
|
86
|
+
# - The result of the begin statement, if there is no error, otherwise the value of the return statement
|
87
|
+
# * *Raises* :
|
88
|
+
# - +NoContextException+ -> If the current fiber does not respond to #__context__
|
89
|
+
#
|
90
|
+
def error_handler(&block)
|
91
|
+
fiber = ::Fiber.current
|
92
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
93
|
+
context = fiber.__context__
|
94
|
+
begin_rescue_ensure = BeginRescueEnsure.new(:parent => context.get_closest_containing_scope)
|
95
|
+
bge = BeginRescueEnsureWrapper.new(block, begin_rescue_ensure)
|
96
|
+
context << bge
|
97
|
+
context << begin_rescue_ensure
|
98
|
+
begin_rescue_ensure
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param block
|
102
|
+
# A code block, which is passed within a {BeginRescueEnsureWrapper}, and which must define the
|
103
|
+
# {BeginRescueEnsure#begin}, {BeginRescueEnsure#rescue}, and {BeginRescueEnsure#ensure} methods.
|
104
|
+
# @!visibility private
|
105
|
+
def _error_handler(&block)
|
106
|
+
error_handler(&block).result
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def wait_for_function(function, *futures)
|
111
|
+
conditional = FiberConditionVariable.new
|
112
|
+
futures.flatten!
|
113
|
+
return nil if futures.empty?
|
114
|
+
result = futures.select(&:set?)
|
115
|
+
return futures.find(&:set?)if function.call(result, futures)
|
116
|
+
futures.each do |f|
|
117
|
+
f.on_set do |set_one|
|
118
|
+
result << set_one
|
119
|
+
conditional.broadcast if function.call(result, futures)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
conditional.wait
|
123
|
+
result
|
124
|
+
end
|
125
|
+
|
126
|
+
# Blocks until *any* of the arguments are set.
|
127
|
+
#
|
128
|
+
# @param [Array] futures
|
129
|
+
# A list of futures to wait for. The function will return when at least one of these is set.
|
130
|
+
#
|
131
|
+
# @return [Array]
|
132
|
+
# A list of the set futures, in the order of being set.
|
133
|
+
#
|
134
|
+
def wait_for_any(*futures)
|
135
|
+
wait_for_function(lambda {|result, future_list| result.length >= 1 }, futures)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Blocks until *all* of the arguments are set.
|
139
|
+
#
|
140
|
+
# @param [Array<Future>] futures
|
141
|
+
# A list of futures to wait for. The function will return only when all of them are set.
|
142
|
+
#
|
143
|
+
# @return [Array]
|
144
|
+
# A list of the set futures, in the order of being set.
|
145
|
+
#
|
146
|
+
def wait_for_all(*futures)
|
147
|
+
wait_for_function(lambda {|result, future_list| result.size == future_list.size}, futures)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
module AWS
|
17
|
+
module Flow
|
18
|
+
module Core
|
19
|
+
|
20
|
+
# Contains a Data Flow Analysis (DFA)-like framework, where transition functions can perform arbitrary computation
|
21
|
+
# before moving to the next state
|
22
|
+
module SimpleDFA
|
23
|
+
attr_accessor :transitions, :symbols, :states, :start_state
|
24
|
+
|
25
|
+
# Creates a new SimpleDFA instance.
|
26
|
+
#
|
27
|
+
# @param start_state
|
28
|
+
# The state with which to start the framework.
|
29
|
+
#
|
30
|
+
def init(start_state)
|
31
|
+
include InstanceMethods
|
32
|
+
@start_state = start_state
|
33
|
+
@symbols = []
|
34
|
+
@states = []
|
35
|
+
@transitions = {}
|
36
|
+
@states << start_state
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return the start state
|
40
|
+
# The start state that was provided when this instance was created.
|
41
|
+
#
|
42
|
+
def get_start_state
|
43
|
+
@start_state
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Array]
|
47
|
+
# The list of all transitions that were added with {#add_transition}.
|
48
|
+
#
|
49
|
+
def get_transitions
|
50
|
+
@transitions
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_general(state, &block)
|
54
|
+
@symbols.each do |symbol|
|
55
|
+
if @transitions[[state, symbol]].nil?
|
56
|
+
@transitions[[state, symbol]] = block
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_transition(state, symbol, &block)
|
62
|
+
@symbols << symbol unless @symbols.include? symbol
|
63
|
+
@states << state unless @states.include? state
|
64
|
+
@transitions[[state, symbol]] = block
|
65
|
+
end
|
66
|
+
|
67
|
+
def uncovered_transitions
|
68
|
+
@states.product(@symbols) - @transitions.keys
|
69
|
+
end
|
70
|
+
|
71
|
+
module InstanceMethods
|
72
|
+
attr_accessor :current_state
|
73
|
+
|
74
|
+
def consume(symbol)
|
75
|
+
@current_state ||= self.class.get_start_state
|
76
|
+
func_to_call = self.class.get_transitions[[@current_state, symbol]]
|
77
|
+
raise "This is not a legal transition" unless func_to_call
|
78
|
+
func_to_call.call(self)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,405 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
# Contains all the task methods, which allow arbitrary blocks of code to be run asynchronously
|
17
|
+
|
18
|
+
module AWS
|
19
|
+
module Flow
|
20
|
+
module Core
|
21
|
+
class Task < FlowFiber
|
22
|
+
attr_reader :result, :block
|
23
|
+
attr_accessor :backtrace, :__context__, :parent
|
24
|
+
|
25
|
+
# Creates a new Task.
|
26
|
+
#
|
27
|
+
# @param __context__
|
28
|
+
# A Task needs a reference to the __context__ that created it so that when the "task" macro is called it can
|
29
|
+
# find the __context__ which the new Task should be added to.
|
30
|
+
#
|
31
|
+
# @param block
|
32
|
+
# A block of code that will be run by the task.
|
33
|
+
#
|
34
|
+
def initialize(__context__, &block)
|
35
|
+
@__context__ = __context__
|
36
|
+
@result = Future.new
|
37
|
+
@block = block
|
38
|
+
|
39
|
+
# Is the task alive?
|
40
|
+
def alive?
|
41
|
+
super && !@cancelled
|
42
|
+
#!!@alive# && !@cancelled
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return
|
46
|
+
# The executor for this task.
|
47
|
+
def executor
|
48
|
+
@__context__.executor
|
49
|
+
end
|
50
|
+
|
51
|
+
super() do
|
52
|
+
begin
|
53
|
+
# Not return because 1.9 will freak about local jump problems if you
|
54
|
+
# try to return, as this is inside a block
|
55
|
+
next if @cancelled
|
56
|
+
@result.set(lambda(&block).call)
|
57
|
+
next if @cancelled
|
58
|
+
@__context__.remove(self)
|
59
|
+
rescue Exception => e
|
60
|
+
if @backtrace != e
|
61
|
+
backtrace = AsyncBacktrace.create_from_exception(@backtrace, e)
|
62
|
+
e.set_backtrace(backtrace.backtrace) if backtrace
|
63
|
+
end
|
64
|
+
@__context__.fail(self, e)
|
65
|
+
ensure
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Passes the get_heirs calls to the context, to ensure uniform handling of get_heirs
|
72
|
+
#
|
73
|
+
def get_heirs
|
74
|
+
@__context__.get_heirs
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Returns true/false, depending on if we are in a daemon task or not
|
79
|
+
#
|
80
|
+
def is_daemon?
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
|
84
|
+
# Used by Future#signal to schedule the task for re-evaluation.
|
85
|
+
#
|
86
|
+
# This will simply add the task back to the list of things to be run in the parent's event loop.
|
87
|
+
#
|
88
|
+
def schedule
|
89
|
+
@__context__ << self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Cancel will prevent the execution of this particular task, if possible
|
93
|
+
#
|
94
|
+
# @param error
|
95
|
+
# The error that is the cause of the cancellation
|
96
|
+
#
|
97
|
+
def cancel(error)
|
98
|
+
@cancelled = true
|
99
|
+
@__context__.remove(self)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Fails the given task with the specified error.
|
103
|
+
#
|
104
|
+
# @param error
|
105
|
+
# The error that is the cause of the cancellation
|
106
|
+
#
|
107
|
+
# @!visibility private
|
108
|
+
def fail(this_task, error)
|
109
|
+
@__context__.fail(this_task, error)
|
110
|
+
end
|
111
|
+
# def fail(this_task, error)
|
112
|
+
# raise error
|
113
|
+
# end
|
114
|
+
|
115
|
+
# Adds a task to this task's context
|
116
|
+
#
|
117
|
+
# @param this_task
|
118
|
+
# The task to add.
|
119
|
+
#
|
120
|
+
def <<(this_task)
|
121
|
+
@__context__.parent << this_task
|
122
|
+
end
|
123
|
+
|
124
|
+
# Removes a task from this task's context
|
125
|
+
#
|
126
|
+
# @param this_task
|
127
|
+
# The task to remove.
|
128
|
+
#
|
129
|
+
def remove(this_task)
|
130
|
+
@__context__.remove(this_task)
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Similar to a regular Task in all functioning except that it is not assured a chance to execute. Whereas a
|
138
|
+
# begin/run/execute block cannot be closed out while there are still nonDaemonHeirs, it can happily entered the
|
139
|
+
# closed state with daemon heirs, making them essentially unrunnable.
|
140
|
+
class DaemonTask < Task
|
141
|
+
|
142
|
+
def is_daemon?
|
143
|
+
return true
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
# External Tasks are used to bridge asynchronous execution to external asynchronous APIs or events. It is passed a block, like so
|
149
|
+
#
|
150
|
+
# external_task do |t|
|
151
|
+
# t.cancellation_handler { |h, cause| h.fail(cause) }
|
152
|
+
# t.initiate_task { |h| trace << :task_started; h.complete; }
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# The {ExternalTask#initiate_task} method is expected to call an external API and return without blocking.
|
156
|
+
# Completion or failure of the external task is reported through ExternalTaskCompletionHandle, which is passed into
|
157
|
+
# the initiate_task and cancellation_handler blocks. The cancellation handler, defined in the same block as the
|
158
|
+
# initiate_task, is used to report the cancellation of the external task.
|
159
|
+
#
|
160
|
+
class ExternalTask < FlowFiber
|
161
|
+
attr_reader :block
|
162
|
+
attr_accessor :cancelled, :inCancellationHandler, :parent, :backtrace, :__context__
|
163
|
+
|
164
|
+
|
165
|
+
# Will always be false, provides a common api for BRE's to ensure they are maintaining their nonDaemonHeirsCount
|
166
|
+
# correctly
|
167
|
+
# @!visibility private
|
168
|
+
def is_daemon?
|
169
|
+
false
|
170
|
+
end
|
171
|
+
|
172
|
+
#
|
173
|
+
# Passes the get_heirs calls to the context, to ensure uniform handling of
|
174
|
+
# get_heirs
|
175
|
+
#
|
176
|
+
# @!visibility private
|
177
|
+
def get_heirs
|
178
|
+
@__context__.get_heirs
|
179
|
+
end
|
180
|
+
|
181
|
+
# @!visibility private
|
182
|
+
def initialize(options = {}, &block)
|
183
|
+
@inCancellationHandler = false
|
184
|
+
@block = block
|
185
|
+
# TODO: What should the default value be?
|
186
|
+
@parent = options[:parent]
|
187
|
+
@handle = ExternalTaskCompletionHandle.new(self)
|
188
|
+
block.call(self)
|
189
|
+
end
|
190
|
+
|
191
|
+
# This method is here because the way we create ExternalTasks is a little
|
192
|
+
# tricky - if the parent isn't passed in on construction(as is the case with
|
193
|
+
# the external_task function), then the parent will only be set after
|
194
|
+
# ExternalTask#initialize is called. We'd prefer to set it in the initiailze,
|
195
|
+
# however, the backtrace relies on the parent's backtrace, and so we cannot do
|
196
|
+
# that. Instead, we use this method to lazily create it, when it is
|
197
|
+
# called. The method itself simply sets the backtrace to the the
|
198
|
+
# make_backtrace of the parent's backtrace, if the backtrace is not already
|
199
|
+
# set, and will otherwise simply return the backtrace
|
200
|
+
# @!visibility private
|
201
|
+
def backtrace
|
202
|
+
@backtrace ||= make_backtrace(@parent.backtrace)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Add a task which removes yourself, and pass it through the parents executor
|
206
|
+
# @!visibility private
|
207
|
+
def remove_from_parent
|
208
|
+
@__context__.executor << FlowFiber.new { @parent.remove(self) }
|
209
|
+
end
|
210
|
+
|
211
|
+
# Add a task which fails yourself with the suppiled error, and pass it through
|
212
|
+
# the parents executor
|
213
|
+
# @!visibility private
|
214
|
+
def fail_to_parent(error)
|
215
|
+
@__context__.executor << FlowFiber.new { @parent.fail(self, error) }
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
# Part of the interface provided by Fiber, has to overridden to properly
|
220
|
+
# reflect that an ExternalTasks alive-ness relies on it's
|
221
|
+
# ExternalTaskCompletionHandle
|
222
|
+
# @!visibility private
|
223
|
+
def alive?
|
224
|
+
! @handle.completed
|
225
|
+
end
|
226
|
+
|
227
|
+
# @!visibility private
|
228
|
+
def cancel(cause)
|
229
|
+
return if @cancelled
|
230
|
+
return if @handle.failure != nil || @handle.completed
|
231
|
+
@cancelled = true
|
232
|
+
if @cancellation_task != nil
|
233
|
+
begin
|
234
|
+
@inCancellationHandler = true
|
235
|
+
@cancellation_task.call(cause)
|
236
|
+
rescue Exception => e
|
237
|
+
if ! self.backtrace.nil?
|
238
|
+
backtrace = AsyncBacktrace.create_from_exception(@backtrace, e)
|
239
|
+
e.set_backtrace(backtrace.backtrace) if backtrace
|
240
|
+
end
|
241
|
+
@handle.failure = e
|
242
|
+
ensure
|
243
|
+
@inCancellationHandler = false
|
244
|
+
if ! @handle.failure.nil?
|
245
|
+
fail_to_parent(@handle.failure)
|
246
|
+
elsif @handle.completed
|
247
|
+
remove_from_parent
|
248
|
+
end
|
249
|
+
end
|
250
|
+
else
|
251
|
+
remove_from_parent
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Store the cancellation handler block passed in for later reference
|
256
|
+
# @!visibility private
|
257
|
+
def cancellation_handler(&block)
|
258
|
+
@cancellation_task = lambda { |cause| block.call(@handle, cause) }
|
259
|
+
end
|
260
|
+
|
261
|
+
# Store the block passed in for later
|
262
|
+
# @!visibility private
|
263
|
+
def initiate_task(&block)
|
264
|
+
@initiation_task = lambda { block.call(@handle) }
|
265
|
+
end
|
266
|
+
|
267
|
+
# From the interface provided by Fiber, will execute the External Task
|
268
|
+
# @!visibility private
|
269
|
+
def resume
|
270
|
+
return if @cancelled
|
271
|
+
begin
|
272
|
+
@cancellation_handler = @initiation_task.call
|
273
|
+
rescue Exception => e
|
274
|
+
backtrace = AsyncBacktrace.create_from_exception(self.backtrace, e)
|
275
|
+
e.set_backtrace(backtrace.backtrace) if backtrace
|
276
|
+
@parent.fail(self, e)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Used to complete or fail an external task initiated through
|
282
|
+
# ExternalTask#initiate_task, and thus handles the logic of what to do when the
|
283
|
+
# external task is failed.
|
284
|
+
# @!visibility private
|
285
|
+
class ExternalTaskCompletionHandle
|
286
|
+
attr_accessor :completed, :failure, :external_task
|
287
|
+
|
288
|
+
# @!visibility private
|
289
|
+
def initialize(external_task)
|
290
|
+
@external_task = external_task
|
291
|
+
end
|
292
|
+
|
293
|
+
# Will merge the backtrace, set the @failure, and then fail the task from the parent
|
294
|
+
# @!visibility private
|
295
|
+
#
|
296
|
+
# @param error
|
297
|
+
# The exception to fail on
|
298
|
+
#
|
299
|
+
# @raise IllegalStateException
|
300
|
+
# Raises if failure hasn't been set, or if the task is already completed
|
301
|
+
#
|
302
|
+
# @!visibility private
|
303
|
+
def fail(error)
|
304
|
+
if ! @failure.nil?
|
305
|
+
raise IllegalStateException, "Invalid ExternalTaskCompletionHandle"
|
306
|
+
end
|
307
|
+
if @completed
|
308
|
+
raise IllegalStateException, "Already completed"
|
309
|
+
end
|
310
|
+
#TODO Might want to flip the logic to only alert if variable is set
|
311
|
+
if @stacktrace.nil?
|
312
|
+
if ! @external_task.backtrace.nil?
|
313
|
+
backtrace = AsyncBacktrace.create_from_exception(@external_task.backtrace, error)
|
314
|
+
error.set_backtrace(backtrace.backtrace) if backtrace
|
315
|
+
end
|
316
|
+
end
|
317
|
+
@failure = error
|
318
|
+
if ! @external_task.inCancellationHandler
|
319
|
+
@external_task.fail_to_parent(error)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Set's the task to complete, and removes it from it's parent
|
324
|
+
#
|
325
|
+
# @raise IllegalStateException
|
326
|
+
# If the failure hasn't been set, or if the task is already completed
|
327
|
+
#
|
328
|
+
# @!visibility private
|
329
|
+
def complete
|
330
|
+
if ! failure.nil?
|
331
|
+
raise IllegalStateException, ""
|
332
|
+
end
|
333
|
+
|
334
|
+
if @completed
|
335
|
+
raise IllegalStateException, "Already Completed"
|
336
|
+
end
|
337
|
+
@completed = true
|
338
|
+
@external_task.remove_from_parent if ! @external_task.inCancellationHandler
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# TaskContext is the class that holds some meta-information for tasks, and which stores the parent link for tasks.
|
343
|
+
# It seperates some of the concerns between tasks and what they have to know to follow back up the chain.
|
344
|
+
#
|
345
|
+
# All the methods here will simply delegate calls, either up to the parent, or down to the task
|
346
|
+
# @!visibility private
|
347
|
+
class TaskContext
|
348
|
+
|
349
|
+
attr_accessor :daemon, :parent, :backtrace, :cancelled
|
350
|
+
|
351
|
+
def initialize(options = {})
|
352
|
+
@parent = options[:parent]
|
353
|
+
@task = options[:task]
|
354
|
+
@task.__context__ = self
|
355
|
+
@non_cancelling = options[:non_cancelling]
|
356
|
+
@daemon = options[:daemon]
|
357
|
+
end
|
358
|
+
|
359
|
+
# @!visibility private
|
360
|
+
def get_closest_containing_scope
|
361
|
+
@task
|
362
|
+
# @ parent
|
363
|
+
end
|
364
|
+
|
365
|
+
# @!visibility private
|
366
|
+
def alive?
|
367
|
+
@task.alive?
|
368
|
+
end
|
369
|
+
|
370
|
+
# @!visibility private
|
371
|
+
def executor
|
372
|
+
@parent.executor
|
373
|
+
end
|
374
|
+
|
375
|
+
# @!visibility private
|
376
|
+
def get_heirs
|
377
|
+
str = "I am a #{@task.class}
|
378
|
+
and my block looks like #{@task.block}"
|
379
|
+
end
|
380
|
+
|
381
|
+
# @!visibility private
|
382
|
+
def fail(this_task, error)
|
383
|
+
@parent.fail(this_task, error)
|
384
|
+
end
|
385
|
+
|
386
|
+
# @!visibility private
|
387
|
+
def remove(thread)
|
388
|
+
@parent.remove(thread)
|
389
|
+
end
|
390
|
+
|
391
|
+
# @!visibility private
|
392
|
+
def cancel(error_type)
|
393
|
+
@task.cancelled = true
|
394
|
+
@parent.cancel(self)
|
395
|
+
end
|
396
|
+
|
397
|
+
# @!visibility private
|
398
|
+
def <<(task)
|
399
|
+
@parent << task
|
400
|
+
end
|
401
|
+
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|