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.
Files changed (62) hide show
  1. data/Gemfile +8 -0
  2. data/LICENSE.TXT +15 -0
  3. data/NOTICE.TXT +14 -0
  4. data/Rakefile +39 -0
  5. data/aws-flow-core/Gemfile +9 -0
  6. data/aws-flow-core/LICENSE.TXT +15 -0
  7. data/aws-flow-core/NOTICE.TXT +14 -0
  8. data/aws-flow-core/Rakefile +27 -0
  9. data/aws-flow-core/aws-flow-core.gemspec +12 -0
  10. data/aws-flow-core/lib/aws/flow.rb +26 -0
  11. data/aws-flow-core/lib/aws/flow/async_backtrace.rb +134 -0
  12. data/aws-flow-core/lib/aws/flow/async_scope.rb +195 -0
  13. data/aws-flow-core/lib/aws/flow/begin_rescue_ensure.rb +386 -0
  14. data/aws-flow-core/lib/aws/flow/fiber.rb +77 -0
  15. data/aws-flow-core/lib/aws/flow/flow_utils.rb +50 -0
  16. data/aws-flow-core/lib/aws/flow/future.rb +109 -0
  17. data/aws-flow-core/lib/aws/flow/implementation.rb +151 -0
  18. data/aws-flow-core/lib/aws/flow/simple_dfa.rb +85 -0
  19. data/aws-flow-core/lib/aws/flow/tasks.rb +405 -0
  20. data/aws-flow-core/test/aws/async_backtrace_spec.rb +41 -0
  21. data/aws-flow-core/test/aws/async_scope_spec.rb +118 -0
  22. data/aws-flow-core/test/aws/begin_rescue_ensure_spec.rb +665 -0
  23. data/aws-flow-core/test/aws/external_task_spec.rb +197 -0
  24. data/aws-flow-core/test/aws/factories.rb +52 -0
  25. data/aws-flow-core/test/aws/fiber_condition_variable_spec.rb +163 -0
  26. data/aws-flow-core/test/aws/fiber_spec.rb +78 -0
  27. data/aws-flow-core/test/aws/flow_spec.rb +255 -0
  28. data/aws-flow-core/test/aws/future_spec.rb +210 -0
  29. data/aws-flow-core/test/aws/rubyflow.rb +22 -0
  30. data/aws-flow-core/test/aws/simple_dfa_spec.rb +63 -0
  31. data/aws-flow-core/test/aws/spec_helper.rb +36 -0
  32. data/aws-flow.gemspec +13 -0
  33. data/lib/aws/decider.rb +67 -0
  34. data/lib/aws/decider/activity.rb +408 -0
  35. data/lib/aws/decider/activity_definition.rb +111 -0
  36. data/lib/aws/decider/async_decider.rb +673 -0
  37. data/lib/aws/decider/async_retrying_executor.rb +153 -0
  38. data/lib/aws/decider/data_converter.rb +40 -0
  39. data/lib/aws/decider/decider.rb +511 -0
  40. data/lib/aws/decider/decision_context.rb +60 -0
  41. data/lib/aws/decider/exceptions.rb +178 -0
  42. data/lib/aws/decider/executor.rb +149 -0
  43. data/lib/aws/decider/flow_defaults.rb +70 -0
  44. data/lib/aws/decider/generic_client.rb +178 -0
  45. data/lib/aws/decider/history_helper.rb +173 -0
  46. data/lib/aws/decider/implementation.rb +82 -0
  47. data/lib/aws/decider/options.rb +607 -0
  48. data/lib/aws/decider/state_machines.rb +373 -0
  49. data/lib/aws/decider/task_handler.rb +76 -0
  50. data/lib/aws/decider/task_poller.rb +207 -0
  51. data/lib/aws/decider/utilities.rb +187 -0
  52. data/lib/aws/decider/worker.rb +324 -0
  53. data/lib/aws/decider/workflow_client.rb +374 -0
  54. data/lib/aws/decider/workflow_clock.rb +104 -0
  55. data/lib/aws/decider/workflow_definition.rb +101 -0
  56. data/lib/aws/decider/workflow_definition_factory.rb +53 -0
  57. data/lib/aws/decider/workflow_enabled.rb +26 -0
  58. data/test/aws/decider_spec.rb +1299 -0
  59. data/test/aws/factories.rb +45 -0
  60. data/test/aws/integration_spec.rb +3108 -0
  61. data/test/aws/spec_helper.rb +23 -0
  62. 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