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,111 @@
|
|
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
|
+
|
19
|
+
# Defines an executable activity.
|
20
|
+
#
|
21
|
+
# @!attribute [ActivityOptions] execution_options
|
22
|
+
# The {ActivityOptions} for this activity.
|
23
|
+
#
|
24
|
+
class ActivityDefinition
|
25
|
+
attr_accessor :execution_options
|
26
|
+
|
27
|
+
# Creates a new ActivityDefinition instance
|
28
|
+
#
|
29
|
+
# @param [Object] instance
|
30
|
+
#
|
31
|
+
# @param [Symbol] activity_method
|
32
|
+
# The method to run when {#execute} is called.
|
33
|
+
#
|
34
|
+
# @param [Hash] registration_options
|
35
|
+
#
|
36
|
+
# @param [Hash] execution_options
|
37
|
+
# The {ActivityOptions} for this activity.
|
38
|
+
#
|
39
|
+
# @param [Object] converter
|
40
|
+
#
|
41
|
+
def initialize(instance, activity_method, registration_options, execution_options, converter)
|
42
|
+
@instance = instance
|
43
|
+
@activity_method = activity_method
|
44
|
+
@registration_options = registration_options
|
45
|
+
@execution_options = execution_options
|
46
|
+
@converter = converter
|
47
|
+
end
|
48
|
+
|
49
|
+
# Executes the activity
|
50
|
+
#
|
51
|
+
# === Parameters
|
52
|
+
#
|
53
|
+
# @param [Object] input
|
54
|
+
# Optional input for the activity execution.
|
55
|
+
#
|
56
|
+
# @param [ActivityExecutionContext] context
|
57
|
+
# The context for the activity execution.
|
58
|
+
#
|
59
|
+
def execute(input, context)
|
60
|
+
begin
|
61
|
+
@instance._activity_execution_context = context
|
62
|
+
# Since we encode all the inputs in some converter, and these inputs
|
63
|
+
# are not "true" ruby objects yet, there is no way for that input to
|
64
|
+
# be an instance of the NilClass(the only thing that responds true to
|
65
|
+
# .nil?) and thus we can be assured that if input.nil?, then the
|
66
|
+
# method had no input
|
67
|
+
if input.nil?
|
68
|
+
result = @instance.send(@activity_method)
|
69
|
+
else
|
70
|
+
ruby_input = @converter.load input
|
71
|
+
result = @instance.send(@activity_method, *ruby_input)
|
72
|
+
end
|
73
|
+
rescue Exception => e
|
74
|
+
#TODO we need the proper error handling here
|
75
|
+
raise e if e.is_a? CancellationException
|
76
|
+
raise ActivityFailureException.new(e.message, @converter.dump(e))
|
77
|
+
ensure
|
78
|
+
@instance._activity_execution_context = nil
|
79
|
+
end
|
80
|
+
return @converter.dump result
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class ActivityExecutionContext
|
86
|
+
attr_accessor :service, :domain, :task
|
87
|
+
def initialize(service, domain, task)
|
88
|
+
@service = service
|
89
|
+
@domain = domain
|
90
|
+
@task = task
|
91
|
+
end
|
92
|
+
def task_token
|
93
|
+
@task.task_token
|
94
|
+
end
|
95
|
+
|
96
|
+
def workflow_execution
|
97
|
+
@task.workflow_execution
|
98
|
+
end
|
99
|
+
|
100
|
+
def record_activity_heartbeat(details)
|
101
|
+
to_send = {:task_token => task_token.to_s, :details => details.to_s }
|
102
|
+
response = @service.record_activity_task_heartbeat(to_send)
|
103
|
+
# TODO See if cancel requested, throw exception if so
|
104
|
+
raise CancellationException if response["cancelRequested"]
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,673 @@
|
|
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
|
+
|
19
|
+
# Represents a decision ID
|
20
|
+
class DecisionID
|
21
|
+
|
22
|
+
# Creates a new DecisionID
|
23
|
+
#
|
24
|
+
# @param decision_target
|
25
|
+
# The decision target
|
26
|
+
#
|
27
|
+
# @param string_id
|
28
|
+
# The string that identifies this decisison
|
29
|
+
#
|
30
|
+
def initialize(decision_target, string_id)
|
31
|
+
@decision_target = decision_target
|
32
|
+
@string_id = string_id
|
33
|
+
end
|
34
|
+
|
35
|
+
# Hash function to return an unique value for the DecisionID
|
36
|
+
#
|
37
|
+
# @return
|
38
|
+
# The calculated hash value for the DecisionID.
|
39
|
+
#
|
40
|
+
def hash
|
41
|
+
prime = 31
|
42
|
+
result = 1
|
43
|
+
result = result * prime + (@decision_target == nil ? 0 : @decision_target.hash)
|
44
|
+
result = prime * result + (@string_id == nil ? 0 : @string_id.hash)
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
# Is this Decision ID the same as another?
|
49
|
+
#
|
50
|
+
# @param [Object] other
|
51
|
+
# The object to compare with.
|
52
|
+
#
|
53
|
+
# @return [true, false]
|
54
|
+
# Returns `true` if the object is the same as this DecisionID; `false` otherwise.
|
55
|
+
#
|
56
|
+
def eql?(other)
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @!visibility private
|
62
|
+
class DecisionWrapper
|
63
|
+
#TODO Consider taking out the id, it's unclear if it is needed
|
64
|
+
def initialize(id, decision, options = [])
|
65
|
+
@decision = decision
|
66
|
+
@id = id
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!visibility private
|
70
|
+
def get_decision
|
71
|
+
@decision
|
72
|
+
end
|
73
|
+
# @!visibility private
|
74
|
+
def consume(symbol)
|
75
|
+
# quack like a state machine
|
76
|
+
end
|
77
|
+
# quack like a decision, too
|
78
|
+
def keys
|
79
|
+
return []
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
# A decision helper for a workflow
|
85
|
+
#
|
86
|
+
# @!attribute [Hash] activity_options
|
87
|
+
#
|
88
|
+
# @!attribute [Hash] activity_scheduling_event_id_to_activity_id
|
89
|
+
#
|
90
|
+
# @!attribute [Hash] decision_map
|
91
|
+
#
|
92
|
+
# @!attribute [Hash] scheduled_activities
|
93
|
+
#
|
94
|
+
# @!attribute [Hash] scheduled_external_workflows
|
95
|
+
#
|
96
|
+
# @!attribute [Hash] scheduled_signals
|
97
|
+
#
|
98
|
+
# @!attribute [Hash] scheduled_timers
|
99
|
+
#
|
100
|
+
# @!attribute [Hash] signal_initiated_event_to_signal_id
|
101
|
+
#
|
102
|
+
# @!visibility private
|
103
|
+
class DecisionHelper
|
104
|
+
attr_accessor :decision_map, :activity_scheduling_event_id_to_activity_id, :scheduled_activities, :scheduled_timers, :activity_options, :scheduled_external_workflows, :scheduled_signals, :signal_initiated_event_to_signal_id, :child_initiated_event_id_to_workflow_id, :workflow_context_data
|
105
|
+
class << self
|
106
|
+
attr_reader :maximum_decisions_per_completion, :completion_events, :force_immediate_decision_timer
|
107
|
+
end
|
108
|
+
@force_immediate_decision_timer = "FORCE_IMMEDIATE_DECISION_TIMER"
|
109
|
+
@maximum_decisions_per_completion = 100
|
110
|
+
@completion_events = [:CancelWorkflowExecution, :CompleteWorkflowExecution, :FailWorkflowExecution, :ContinueAsNewWorkflowExecution]
|
111
|
+
|
112
|
+
# Creates a new, empty {DecisionHelper} instance.
|
113
|
+
def initialize
|
114
|
+
@decision_map = {}
|
115
|
+
@id = Hash.new {|hash, key| hash[key] = 0 }
|
116
|
+
@scheduled_signals = {}
|
117
|
+
@activity_scheduling_event_id_to_activity_id = {}
|
118
|
+
@scheduled_activities = {}
|
119
|
+
@scheduled_external_workflows = {}
|
120
|
+
@scheduled_timers = {}
|
121
|
+
@activity_options = {}
|
122
|
+
@signal_initiated_event_to_signal_id = {}
|
123
|
+
@child_initiated_event_id_to_workflow_id = {}
|
124
|
+
end
|
125
|
+
|
126
|
+
# @!visibility private
|
127
|
+
def get_next_id(decision_target)
|
128
|
+
id = (@id[decision_target] += 1)
|
129
|
+
"#{decision_target}#{id}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# @!visibility private
|
133
|
+
def get_next_state_machine_which_will_schedule(list)
|
134
|
+
return if list.empty?
|
135
|
+
ele = list.first
|
136
|
+
ele = list.shift until (list.empty? || ele.get_decision != nil)
|
137
|
+
ele
|
138
|
+
end
|
139
|
+
|
140
|
+
def is_completion_event(decision)
|
141
|
+
DecisionHelper.completion_events.include? decision.get_decision[:decision_type].to_sym
|
142
|
+
end
|
143
|
+
|
144
|
+
# @!visibility private
|
145
|
+
def handle_decision_task_started_event
|
146
|
+
# In order to ensure that the events we have already scheduled do not
|
147
|
+
# make a decision, we will process only maximum_decisions_per_completion
|
148
|
+
# here
|
149
|
+
count = 0
|
150
|
+
decision_list = @decision_map.values
|
151
|
+
decision_state_machine = get_next_state_machine_which_will_schedule(decision_list)
|
152
|
+
until decision_state_machine.nil?
|
153
|
+
next_decision_state_machine = get_next_state_machine_which_will_schedule(decision_list)
|
154
|
+
count += 1
|
155
|
+
if (count == DecisionHelper.maximum_decisions_per_completion &&
|
156
|
+
next_decision_state_machine != nil &&
|
157
|
+
! is_completion_event(next_decision_state_machine))
|
158
|
+
break
|
159
|
+
end
|
160
|
+
decision_state_machine.consume(:handle_decision_task_started_event)
|
161
|
+
decision_state_machine = next_decision_state_machine
|
162
|
+
if (next_decision_state_machine != nil &&
|
163
|
+
count < DecisionHelper.maximum_decisions_per_completion)
|
164
|
+
next_decision_state_machine.consume(:handle_decision_task_started_event)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @!visibility private
|
170
|
+
def method_missing(method_name, *args)
|
171
|
+
if [:[]=, :[]].include? method_name
|
172
|
+
@decision_map.send(method_name, *args)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns the activity ID for a scheduled activity
|
177
|
+
#
|
178
|
+
# @param [String] scheduled_id
|
179
|
+
# The scheduled activity ID.
|
180
|
+
#
|
181
|
+
# @!visibility private
|
182
|
+
def get_activity_id(scheduled_id)
|
183
|
+
activity_scheduling_event_id_to_activity_id[scheduled_id]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# The asynchronous decider class
|
188
|
+
class AsyncDecider
|
189
|
+
include Utilities::SelfMethods
|
190
|
+
attr_accessor :task_token, :decision_helper
|
191
|
+
|
192
|
+
# Creates a new asynchronous decider
|
193
|
+
def initialize(workflow_definition_factory, history_helper, decision_helper)
|
194
|
+
@workflow_definition_factory = workflow_definition_factory
|
195
|
+
@history_helper = history_helper
|
196
|
+
@decision_helper = decision_helper
|
197
|
+
@decision_task = history_helper.get_decision_task
|
198
|
+
@workflow_clock = WorkflowClock.new(@decision_helper)
|
199
|
+
|
200
|
+
@workflow_context = WorkflowContext.new(@decision_task, @workflow_clock)
|
201
|
+
@activity_client = GenericActivityClient.new(@decision_helper, nil)
|
202
|
+
@workflow_client = GenericWorkflowClient.new(@decision_helper, @workflow_context)
|
203
|
+
@decision_context = DecisionContext.new(@activity_client, @workflow_client, @workflow_clock, @workflow_context, @decision_helper)
|
204
|
+
end
|
205
|
+
|
206
|
+
# @note *Beware, this getter will modify things*, as it creates decisions for the objects in the {AsyncDecider}
|
207
|
+
# that need decisions sent out.
|
208
|
+
#
|
209
|
+
# @!visibility private
|
210
|
+
def get_decisions
|
211
|
+
result = @decision_helper.decision_map.values.map {|decision_object|
|
212
|
+
decision_object.get_decision}.compact
|
213
|
+
if result.length > DecisionHelper.maximum_decisions_per_completion
|
214
|
+
result = result.slice(0, DecisionHelper.maximum_decisions_per_completion - 1)
|
215
|
+
result << ({:decision_type => "StartTimer", :start_timer_decision_attributes => {
|
216
|
+
:timer_id => DecisionHelper.force_immediate_decision_timer,
|
217
|
+
:start_to_fire_timeout => "0"
|
218
|
+
}})
|
219
|
+
end
|
220
|
+
return result
|
221
|
+
end
|
222
|
+
|
223
|
+
# @!visibility private
|
224
|
+
def decide
|
225
|
+
begin
|
226
|
+
decide_impl
|
227
|
+
rescue Exception => error
|
228
|
+
raise error
|
229
|
+
ensure
|
230
|
+
begin
|
231
|
+
@decision_helper.workflow_context_data = @definition.get_workflow_state
|
232
|
+
rescue WorkflowException => error
|
233
|
+
@decision_helper.workflow_context_data = error.details
|
234
|
+
rescue Exception => error
|
235
|
+
@decision_helper.workflow_context_data = error.message
|
236
|
+
# Catch and do stuff
|
237
|
+
ensure
|
238
|
+
@workflow_definition_factory.delete_workflow_definition(@definition)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# @!visibility private
|
244
|
+
def decide_impl
|
245
|
+
single_decision_event = @history_helper.get_single_decision_events
|
246
|
+
while single_decision_event.length > 0
|
247
|
+
@decision_helper.handle_decision_task_started_event
|
248
|
+
[*single_decision_event].each do |event|
|
249
|
+
last_non_replay_event_id = @history_helper.get_last_non_replay_event_id
|
250
|
+
@workflow_clock.replaying = false if event.event_id >= last_non_replay_event_id
|
251
|
+
@workflow_clock.replay_current_time_millis = @history_helper.get_replay_current_time_millis
|
252
|
+
process_event(event)
|
253
|
+
event_loop(event)
|
254
|
+
end
|
255
|
+
@task_token = @history_helper.get_decision_task.task_token
|
256
|
+
complete_workflow if completed?
|
257
|
+
single_decision_event = @history_helper.get_single_decision_events
|
258
|
+
end
|
259
|
+
if @unhandled_decision
|
260
|
+
@unhandled_decision = false
|
261
|
+
complete_workflow
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Registers a {FailWorkflowExecution} decision.
|
266
|
+
#
|
267
|
+
# @param [DecisionID] decision_id
|
268
|
+
# The ID of the {DecisionTaskCompleted} event corresponding to the decision task that resulted in the decision
|
269
|
+
# failing in this execution. This information can be useful for tracing the sequence of events back from the
|
270
|
+
# failure.
|
271
|
+
#
|
272
|
+
# @param [Exception] failure
|
273
|
+
# The exception that is associated with the failed workflow.
|
274
|
+
#
|
275
|
+
# @see http://docs.aws.amazon.com/amazonswf/latest/apireference/API_FailWorkflowExecutionDecisionAttributes.html
|
276
|
+
# FailWorkflowExecutionDecisionAttributes
|
277
|
+
#
|
278
|
+
def make_fail_decision(decision_id, failure)
|
279
|
+
decision_type = "FailWorkflowExecution"
|
280
|
+
|
281
|
+
# Sizes taken from
|
282
|
+
# http://docs.aws.amazon.com/amazonswf/latest/apireference/API_FailWorkflowExecutionDecisionAttributes.html
|
283
|
+
#reason = failure.reason if (failure.respond_to? :reason)
|
284
|
+
max_response_size = 32768
|
285
|
+
truncation_overhead = 8000
|
286
|
+
reason ||= failure.message.slice(0, 255)
|
287
|
+
detail_size = max_response_size - truncation_overhead
|
288
|
+
|
289
|
+
# If you don't have details, you must be some other type of
|
290
|
+
# exception. We can't do anything exceedingly clever, so lets just get
|
291
|
+
# the stack trace and pop that out
|
292
|
+
details = failure.details if (failure.respond_to? :details)
|
293
|
+
details ||= failure.backtrace.join("")
|
294
|
+
new_details = details[0..(max_response_size - truncation_overhead)]
|
295
|
+
if details.length > (max_response_size - truncation_overhead)
|
296
|
+
new_details += "->->->->->THIS BACKTRACE WAS TRUNCATED"
|
297
|
+
end
|
298
|
+
# details.unshift(reason)
|
299
|
+
# details = details.join("\n")
|
300
|
+
|
301
|
+
fail_workflow_execution_decision_attributes = {:reason => reason, :details => new_details}
|
302
|
+
decision = {:decision_type => decision_type, :fail_workflow_execution_decision_attributes => fail_workflow_execution_decision_attributes}
|
303
|
+
CompleteWorkflowStateMachine.new(decision_id, decision)
|
304
|
+
|
305
|
+
end
|
306
|
+
|
307
|
+
def make_completion_decision(decision_id, decision)
|
308
|
+
CompleteWorkflowStateMachine.new(decision_id, decision)
|
309
|
+
end
|
310
|
+
def make_cancel_decision(decision_id)
|
311
|
+
CompleteWorkflowStateMachine.new(decision_id, {:decision_type => "CancelWorkflowExecution"})
|
312
|
+
end
|
313
|
+
# Continues this as a new workflow, using the provided decision and options.
|
314
|
+
#
|
315
|
+
# @param [DecisionID] decision_id
|
316
|
+
# The decision id to use.
|
317
|
+
#
|
318
|
+
# @param [WorkflowOptions] continue_as_new_options
|
319
|
+
# The options to use for the new workflow.
|
320
|
+
#
|
321
|
+
def continue_as_new_workflow(decision_id, continue_as_new_options)
|
322
|
+
result = {
|
323
|
+
:decision_type => "ContinueAsNewWorkflowExecution",
|
324
|
+
}
|
325
|
+
|
326
|
+
task_list = continue_as_new_options.task_list ? {:task_list => {:name => continue_as_new_options.task_list}} : {}
|
327
|
+
to_add = continue_as_new_options.get_options([:execution_start_to_close_timeout, :task_start_to_close_timeout, :child_policy, :tag_list, :workflow_type_version, :input], task_list)
|
328
|
+
result[:continue_as_new_workflow_execution_decision_attributes] = to_add
|
329
|
+
CompleteWorkflowStateMachine.new(decision_id, result)
|
330
|
+
end
|
331
|
+
|
332
|
+
# Registers a `CompleteWorkflowExecution` decision.
|
333
|
+
#
|
334
|
+
# @see http://docs.aws.amazon.com/amazonswf/latest/apireference/API_CompleteWorkflowExecutionDecisionAttributes.html
|
335
|
+
# CompleteWorkflowExecutionDecisionAttributes
|
336
|
+
#
|
337
|
+
def complete_workflow
|
338
|
+
return unless @completed && ! @unhandled_decision
|
339
|
+
decision_id = DecisionID.new(:Self, nil)
|
340
|
+
if @failure
|
341
|
+
@decision_helper[decision_id] = make_fail_decision(decision_id, @failure)
|
342
|
+
elsif @cancel_requested
|
343
|
+
@decision_helper[decision_id] = make_cancel_decision(decision_id)
|
344
|
+
else
|
345
|
+
|
346
|
+
if ! @workflow_context.continue_as_new_options.nil?
|
347
|
+
@decision_helper[decision_id] = continue_as_new_workflow(decision_id, @workflow_context.continue_as_new_options)
|
348
|
+
else
|
349
|
+
if @result.nil?
|
350
|
+
@decision_helper[decision_id] = make_completion_decision(decision_id, {
|
351
|
+
:decision_type => "CompleteWorkflowExecution"})
|
352
|
+
else
|
353
|
+
@decision_helper[decision_id] = make_completion_decision(decision_id, {
|
354
|
+
:decision_type => "CompleteWorkflowExecution",
|
355
|
+
:complete_workflow_execution_decision_attributes => {:result => @result.get}})
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# Is the task completed?
|
362
|
+
#
|
363
|
+
# @return [true, false]
|
364
|
+
# Returns `true` if the task is completed; `false` otherwise.
|
365
|
+
#
|
366
|
+
def completed?
|
367
|
+
@completed
|
368
|
+
end
|
369
|
+
|
370
|
+
# Handler for the `:ActivityTaskScheduled` event.
|
371
|
+
#
|
372
|
+
# @param [Object] event
|
373
|
+
# The event to process
|
374
|
+
#
|
375
|
+
def handle_activity_task_scheduled(event)
|
376
|
+
activity_id = event.attributes[:activity_id]
|
377
|
+
@decision_helper.activity_scheduling_event_id_to_activity_id[event.id] = activity_id
|
378
|
+
@decision_helper[activity_id].consume(:handle_initiated_event)
|
379
|
+
return @decision_helper[activity_id].done?
|
380
|
+
end
|
381
|
+
|
382
|
+
# Handler for the `:WorkflowExecutionStarted` event.
|
383
|
+
#
|
384
|
+
# @param [Object] event
|
385
|
+
# The event to process
|
386
|
+
#
|
387
|
+
def handle_workflow_execution_started(event)
|
388
|
+
@workflow_async_scope = AsyncScope.new do
|
389
|
+
FlowFiber.current[:decision_context] = @decision_context
|
390
|
+
input = (event.attributes.keys.include? :input) ? event.attributes[:input] : nil
|
391
|
+
@definition = @workflow_definition_factory.get_workflow_definition(@decision_context)
|
392
|
+
@result = @definition.execute(input)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Handler for the `:TimerFired` event.
|
397
|
+
#
|
398
|
+
# @param [Object] event
|
399
|
+
# The event to process
|
400
|
+
#
|
401
|
+
def handle_timer_fired(event)
|
402
|
+
timer_id = event.attributes[:timer_id]
|
403
|
+
return if timer_id == DecisionHelper.force_immediate_decision_timer
|
404
|
+
@decision_helper[timer_id].consume(:handle_completion_event)
|
405
|
+
if @decision_helper[timer_id].done?
|
406
|
+
open_request = @decision_helper.scheduled_timers.delete(timer_id)
|
407
|
+
open_request.blocking_promise.set(nil)
|
408
|
+
open_request.completion_handle.complete
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Handler for the `:StartTimerFailed` event.
|
413
|
+
#
|
414
|
+
# @param [Object] event
|
415
|
+
# The event to process
|
416
|
+
#
|
417
|
+
def handle_start_timer_failed(event)
|
418
|
+
timer_id = event.attributes.timer_id
|
419
|
+
return if timer_id == DecisionHelper.force_immediate_decision_timer
|
420
|
+
handle_event(event, {
|
421
|
+
:id_methods => [:timer_id],
|
422
|
+
:consume_symbol => :handle_completion_event,
|
423
|
+
:decision_helper_scheduled => :scheduled_timers,
|
424
|
+
:handle_open_request => lambda do |event, open_request|
|
425
|
+
exception = StartTimerFailedException(event.id, timer_id, nil, event.attributes.cause)
|
426
|
+
open_request.completion_handle.fail(exception)
|
427
|
+
end
|
428
|
+
})
|
429
|
+
state_machine = @decision_helper[timer_id]
|
430
|
+
|
431
|
+
|
432
|
+
end
|
433
|
+
|
434
|
+
# Handler for the `:WorkflowExecutionCancelRequested` event.
|
435
|
+
# @param [Object] event
|
436
|
+
# The event to process
|
437
|
+
def handle_workflow_execution_cancel_requested(event)
|
438
|
+
@workflow_async_scope.cancel(CancellationException.new("Cancelled from a WorkflowExecutionCancelRequested"))
|
439
|
+
@cancel_requested = true
|
440
|
+
end
|
441
|
+
|
442
|
+
# Handler for the `:ActivityTaskCancelRequested` event.
|
443
|
+
# @param [Object] event
|
444
|
+
# The event to process
|
445
|
+
def handle_activity_task_cancel_requested(event)
|
446
|
+
activity_id = event.attributes[:activity_id]
|
447
|
+
@decision_helper[activity_id].consume(:handle_cancellation_initiated_event)
|
448
|
+
end
|
449
|
+
|
450
|
+
# Handler for the `:RequestCancelActivityTaskFailed` event.
|
451
|
+
# @param [Object] event
|
452
|
+
# The event to process
|
453
|
+
def handle_request_cancel_activity_task_failed(event)
|
454
|
+
handle_event(event, {
|
455
|
+
:id_methods => [:activity_id],
|
456
|
+
:consume_symbol => :handle_cancellation_failure_event
|
457
|
+
})
|
458
|
+
end
|
459
|
+
|
460
|
+
def handle_closing_failure
|
461
|
+
@unhandled_decision = true
|
462
|
+
@decision_helper[:SELF, nil].consume(:handle_initiation_failed_event)
|
463
|
+
end
|
464
|
+
|
465
|
+
# Handler for the `:CompleteWorkflowExecutionFailed` event.
|
466
|
+
# @param [Object] event
|
467
|
+
# The event to process
|
468
|
+
def handle_complete_workflow_execution_failed(event)
|
469
|
+
handle_closing_failure
|
470
|
+
end
|
471
|
+
|
472
|
+
# Handler for the `:FailWorkflowExecutionFailed` event.
|
473
|
+
#
|
474
|
+
# @param [Object] event
|
475
|
+
# The event to process
|
476
|
+
#
|
477
|
+
def handle_fail_workflow_execution_failed(event)
|
478
|
+
handle_closing_failure
|
479
|
+
end
|
480
|
+
|
481
|
+
# Handler for the `:CancelWorkflowExecutionFailed` event.
|
482
|
+
#
|
483
|
+
# @param [Object] event
|
484
|
+
# The event to process
|
485
|
+
#
|
486
|
+
def handle_cancel_workflow_execution_failed(event)
|
487
|
+
handle_closing_failure
|
488
|
+
end
|
489
|
+
|
490
|
+
# Handler for the `:ContinueAsNewWorkflowExecutionFailed' event.
|
491
|
+
#
|
492
|
+
# @param [Object] event
|
493
|
+
# The event to process
|
494
|
+
#
|
495
|
+
def handle_continue_as_new_workflow_execution_failed(event)
|
496
|
+
handle_closing_failure
|
497
|
+
end
|
498
|
+
|
499
|
+
# Handler for the `:TimerStarted` event.
|
500
|
+
#
|
501
|
+
# @param [Object] event
|
502
|
+
# The event to process
|
503
|
+
#
|
504
|
+
def handle_timer_started(event)
|
505
|
+
timer_id = event.attributes[:timer_id]
|
506
|
+
return if timer_id == DecisionHelper.force_immediate_decision_timer
|
507
|
+
@decision_helper[timer_id].consume(:handle_initiated_event)
|
508
|
+
@decision_helper[timer_id].done?
|
509
|
+
end
|
510
|
+
|
511
|
+
# Handler for the `:TimerCanceled` event.
|
512
|
+
#
|
513
|
+
# @param [Object] event
|
514
|
+
# The event to process
|
515
|
+
#
|
516
|
+
def handle_timer_canceled(event)
|
517
|
+
handle_event(event, {
|
518
|
+
:id_methods => [:timer_id],
|
519
|
+
:consume_symbol => :handle_cancellation_event,
|
520
|
+
:decision_helper_scheduled => :scheduled_timers,
|
521
|
+
:handle_open_request => lambda do |event, open_request|
|
522
|
+
if ! open_request.nil?
|
523
|
+
cancellation_exception = CancellationException.new("Cancelled from a Timer Cancelled event")
|
524
|
+
open_request.completion_handle.fail(cancellation_exception)
|
525
|
+
end
|
526
|
+
end
|
527
|
+
})
|
528
|
+
end
|
529
|
+
|
530
|
+
# Handler for the `:SignalExternalWorkflowExecutionInitiated` event.
|
531
|
+
#
|
532
|
+
# @param [Object] event
|
533
|
+
# The event to process
|
534
|
+
#
|
535
|
+
def handle_signal_external_workflow_execution_initiated(event)
|
536
|
+
signal_id = event.attributes[:control]
|
537
|
+
@decision_helper.signal_initiated_event_to_signal_id[event.id] = signal_id
|
538
|
+
@decision_helper[signal_id].consume(:handle_initiated_event)
|
539
|
+
@decision_helper[signal_id].done?
|
540
|
+
end
|
541
|
+
|
542
|
+
# Handler for the `:RequestCancelExternalWorkflowExecutionInitiated` event.
|
543
|
+
#
|
544
|
+
# @param [Object] event
|
545
|
+
# The event to process
|
546
|
+
#
|
547
|
+
def handle_request_cancel_external_workflow_execution_initiated(event)
|
548
|
+
handle_event(event, {
|
549
|
+
:id_methods => [:workflow_id],
|
550
|
+
:consume_symbol => :handle_cancellation_initiated_event
|
551
|
+
})
|
552
|
+
end
|
553
|
+
|
554
|
+
# Handler for the `:RequestCancelExternalWorkflowExecutionFailed` event.
|
555
|
+
#
|
556
|
+
# @param [Object] event
|
557
|
+
# The event to process
|
558
|
+
#
|
559
|
+
def handle_request_cancel_external_workflow_execution_failed(event)
|
560
|
+
handle_event(event, {
|
561
|
+
:id_methods => [:workflow_id],
|
562
|
+
:consume_symbol => :handle_cancellation_failure_event
|
563
|
+
})
|
564
|
+
end
|
565
|
+
|
566
|
+
# Handler for the `:StartChildWorkflowExecutionInitiated` event.
|
567
|
+
#
|
568
|
+
# @param [Object] event
|
569
|
+
# The event to process
|
570
|
+
#
|
571
|
+
def handle_start_child_workflow_execution_initiated(event)
|
572
|
+
workflow_id = event.attributes[:workflow_id]
|
573
|
+
@decision_helper.child_initiated_event_id_to_workflow_id[event.id] = workflow_id
|
574
|
+
@decision_helper[workflow_id].consume(:handle_initiated_event)
|
575
|
+
@decision_helper[workflow_id].done?
|
576
|
+
end
|
577
|
+
|
578
|
+
# Handler for the `:CancelTimerFailed` event.
|
579
|
+
#
|
580
|
+
# @param [Object] event
|
581
|
+
# The event to process
|
582
|
+
#
|
583
|
+
def handle_cancel_timer_failed(event)
|
584
|
+
handle_event(event, {
|
585
|
+
:id_methods => [:timer_id],
|
586
|
+
:consume_symbol => :handle_cancellation_failure_event
|
587
|
+
})
|
588
|
+
end
|
589
|
+
|
590
|
+
# Handler for the `:WorkflowExecutionSignaled` event.
|
591
|
+
#
|
592
|
+
# @param [Object] event
|
593
|
+
# The event to process
|
594
|
+
#
|
595
|
+
def handle_workflow_execution_signaled(event)
|
596
|
+
signal_name = event.attributes[:signal_name]
|
597
|
+
input = event.attributes[:input] if event.attributes.keys.include? :input
|
598
|
+
input ||= NoInput.new
|
599
|
+
# TODO do stuff if we are @completed
|
600
|
+
t = Task.new(nil) do
|
601
|
+
@definition.signal_received(signal_name, input)
|
602
|
+
end
|
603
|
+
task_context = TaskContext.new(:parent => @workflow_async_scope.get_closest_containing_scope, :task => t)
|
604
|
+
@workflow_async_scope.get_closest_containing_scope << t
|
605
|
+
end
|
606
|
+
|
607
|
+
# Processes decider events
|
608
|
+
#
|
609
|
+
# @param [Object] event
|
610
|
+
# The event to process
|
611
|
+
#
|
612
|
+
def process_event(event)
|
613
|
+
event_type_symbol = event.event_type.to_sym
|
614
|
+
# Mangle the name so that it is handle_ + the name of the event type in snakecase
|
615
|
+
handle_event = "handle_" + event.event_type.gsub(/(.)([A-Z])/,'\1_\2').downcase
|
616
|
+
noop_set = Set.new([:DecisionTaskScheduled, :DecisionTaskCompleted,
|
617
|
+
:DecisionTaskStarted, :DecisionTaskTimedOut, :WorkflowExecutionTimedOut,
|
618
|
+
:WorkflowExecutionTerminated, :MarkerRecorded,
|
619
|
+
:WorkflowExecutionCompleted, :WorkflowExecutionFailed,
|
620
|
+
:WorkflowExecutionCanceled, :WorkflowExecutionContinuedAsNew, :ActivityTaskStarted])
|
621
|
+
|
622
|
+
return if noop_set.member? event_type_symbol
|
623
|
+
|
624
|
+
self_set = Set.new([:TimerFired, :StartTimerFailed,
|
625
|
+
:WorkflowExecutionCancel, :ActivityTaskScheduled,
|
626
|
+
:WorkflowExecutionCancelRequested,
|
627
|
+
:ActivityTaskCancelRequested, :RequestCancelActivityTaskFailed,
|
628
|
+
:CompleteWorkflowExecutionFailed, :FailWorkflowExecutionFailed,
|
629
|
+
:CancelWorkflowExecutionFailed, :ContinueAsNewWorkflowExecutionFailed,
|
630
|
+
:TimerStarted, :TimerCanceled,
|
631
|
+
:SignalExternalWorkflowExecutionInitiated,
|
632
|
+
:RequestCancelExternalWorkflowExecutionInitiated,
|
633
|
+
:RequestCancelExternalWorkflowExecutionFailed,
|
634
|
+
:StartChildWorkflowExecutionInitiated, :CancelTimerFailed, :WorkflowExecutionStarted, :WorkflowExecutionSignaled])
|
635
|
+
|
636
|
+
activity_client_set = Set.new([:ActivityTaskCompleted,
|
637
|
+
:ActivityTaskCanceled, :ActivityTaskTimedOut,
|
638
|
+
:ScheduleActivityTaskFailed, :ActivityTaskFailed])
|
639
|
+
|
640
|
+
workflow_client_set =
|
641
|
+
Set.new([:ExternalWorkflowExecutionCancelRequested,
|
642
|
+
:ChildWorkflowExecutionCanceled, :ChildWorkflowExecutionCompleted,
|
643
|
+
:ChildWorkflowExecutionFailed,
|
644
|
+
:ChildWorkflowExecutionStarted, :ChildWorkflowExecutionTerminated,
|
645
|
+
:ChildWorkflowExecutionTimedOut, :ExternalWorkflowExecutionSignaled,
|
646
|
+
:SignalExternalWorkflowExecutionFailed,
|
647
|
+
:StartChildWorkflowExecutionFailed])
|
648
|
+
|
649
|
+
event_set_to_object_mapping = { self_set => self,
|
650
|
+
activity_client_set => @activity_client,
|
651
|
+
workflow_client_set => @workflow_client }
|
652
|
+
thing_to_operate_on = event_set_to_object_mapping.map {|key, value|
|
653
|
+
value if key.member? event_type_symbol }.compact.first
|
654
|
+
thing_to_operate_on.send(handle_event, event)
|
655
|
+
# DecisionTaskStarted is taken care of at TODO
|
656
|
+
end
|
657
|
+
|
658
|
+
# @!visibility private
|
659
|
+
def event_loop(event)
|
660
|
+
return if @completed
|
661
|
+
begin
|
662
|
+
@completed = @workflow_async_scope.eventLoop
|
663
|
+
#TODO Make this a cancellationException, set it up correctly?
|
664
|
+
rescue Exception => e
|
665
|
+
@failure = e unless @cancel_requested
|
666
|
+
@completed = true
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
end
|
671
|
+
|
672
|
+
end
|
673
|
+
end
|