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,374 @@
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
+
20
+ # A future provided by a [WorkflowExecution](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/WorkflowExecution.html).
21
+ #
22
+ # @!attribute _workflow_execution
23
+ # The [WorkflowExecution](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/WorkflowExecution.html)
24
+ # instance that this future belongs to.
25
+ #
26
+ # @!attribute return_value
27
+ # The return value of the future.
28
+ #
29
+ class WorkflowFuture
30
+ attr_accessor :_workflow_execution, :return_value
31
+
32
+
33
+ # Creates a new workflow future
34
+ #
35
+ # @param workflow_execution
36
+ #
37
+ def initialize(workflow_execution)
38
+ @_workflow_execution = workflow_execution
39
+ @return_value = Future.new
40
+ end
41
+
42
+ def method_missing(method_name, *args, &block)
43
+ @return_value.send(method_name, *args, &block)
44
+ end
45
+
46
+
47
+ # Gets the current value of the workflow execution
48
+ # @return
49
+ def workflow_execution
50
+ @_workflow_execution
51
+ end
52
+ end
53
+
54
+
55
+
56
+
57
+ # @!visibility private
58
+ class NoInput
59
+ def empty?; return true; end
60
+ end
61
+
62
+
63
+ # A client for a workflow execution.
64
+ #
65
+ # @!attribute domain
66
+ # The SWF domain used for this workflow.
67
+ #
68
+ # @!attribute [Hash, WorkflowOptions] options
69
+ # Workflow options for this client.
70
+ #
71
+ class WorkflowClient < GenericClient
72
+ attr_accessor :domain, :options
73
+
74
+ # Creates a new {WorkflowClient}
75
+ #
76
+ # @param service
77
+ # The SWF [Client](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/Client.html) to use for
78
+ # creating this {WorkflowClient}.
79
+ #
80
+ # @param domain
81
+ # The SWF [Domain](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/Domain.html) in which to
82
+ # start the workflow execution.
83
+ #
84
+ # @param workflow_class
85
+ #
86
+ # @param [Hash, WorkflowOptions] options
87
+ # Workflow options for this client.
88
+ #
89
+ def initialize(service, domain, workflow_class, options)
90
+ @service = service
91
+ @domain = domain
92
+ @workflow_class = workflow_class
93
+ @options = options
94
+ @failure_map = {}
95
+ super
96
+ end
97
+
98
+
99
+ # @!visibility private
100
+ def self.default_option_class; WorkflowOptions; end
101
+
102
+ # Gets the events for this workflow client
103
+ #
104
+ def events
105
+ @execution.events if @execution
106
+ end
107
+
108
+
109
+ # Gets the current {DecisionContext} for the workflow client.
110
+ #
111
+ def get_decision_context
112
+ @decision_context ||= FlowFiber.current[:decision_context]
113
+ @decision_helper ||= @decision_context.decision_helper
114
+ end
115
+
116
+
117
+ # Begins executing this workflow.
118
+ #
119
+ # @param input
120
+ # Input to provide to the
121
+ #
122
+ # @param [Hash, StartWorkflowOptions] block
123
+ # A hash of {StartWorkflowOptions} to use for this workflow execution.
124
+ #
125
+ def start_execution(*input, &block)
126
+ if Utilities::is_external
127
+ self.start_external_workflow(input, &block)
128
+ else
129
+ self.start_internal_workflow(input, &block)
130
+ end
131
+ end
132
+
133
+ # Records a `WorkflowExecutionSignaled` event in the workflow execution history and creates a decision task for
134
+ # the workflow execution. The event is recorded with the specified user-defined signal name and input (if
135
+ # provided).
136
+ #
137
+ # @param signal_name
138
+ # The user-defined name of the signal.
139
+ #
140
+ # @param workflow_execution
141
+ # The workflow execution to signal.
142
+ #
143
+ # @param [Hash, SignalWorkflowOptions] block
144
+ # A block of {SignalWorkflowOptions} for the `WorkflowExecutionSignaled` event.
145
+ #
146
+ def signal_workflow_execution(signal_name = nil, workflow_execution = nil, &block)
147
+ if Utilities::is_external
148
+ self.signal_external_workflow(signal_name, workflow_execution, &block)
149
+ else
150
+ self.signal_internal_workflow(signal_name, workflow_execution, &block)
151
+ end
152
+ end
153
+
154
+
155
+ # Called by {#signal_workflow_execution}
156
+ # @!visibility private
157
+ def signal_external_workflow(signal_name, workflow_execution, &block)
158
+ options = Utilities::interpret_block_for_options(SignalWorkflowOptions, block)
159
+ options.signal_name ||= signal_name
160
+ if workflow_execution
161
+ options.domain ||= workflow_execution.domain.name.to_s
162
+ options.workflow_id ||= workflow_execution.workflow_id.to_s
163
+ end
164
+ @service.signal_workflow_execution(options.get_full_options)
165
+ end
166
+
167
+ # Called by {#signal_workflow_execution}
168
+ # @!visibility private
169
+ def signal_internal_workflow(signal_name, workflow_execution, &block)
170
+ get_decision_context
171
+ options = Utilities::interpret_block_for_options(SignalWorkflowOptions, block)
172
+ # Unpack the workflow execution from the future
173
+ workflow_execution = workflow_execution.workflow_execution if workflow_execution.respond_to? :workflow_execution
174
+ options.signal_name ||= signal_name.to_s
175
+ options.workflow_id ||= workflow_execution.workflow_id.get.to_s
176
+ execution_method = options.execution_method || @options.execution_method
177
+ raise "You haven't specified an execution method!" if execution_method.nil?
178
+ Utilities::merge_all_options(options)
179
+ open_request = OpenRequestInfo.new
180
+ decision_id = @decision_helper.get_next_id(:Signal)
181
+ options.control ||= decision_id
182
+ external_task do |external|
183
+ external.initiate_task do |handle|
184
+ @decision_helper[decision_id] = SignalDecisionStateMachine.new(decision_id, options)
185
+ open_request.completion_handle = handle
186
+ @decision_helper.scheduled_signals[decision_id] = open_request
187
+ end
188
+ external.cancellation_handler do |handle, cause|
189
+ @decision_helper[decision_id].consume(:cancel)
190
+ open_request = @decision_helper.scheduled_signal.delete(decision_id)
191
+ raise "Signal #{decision_id} wasn't scheduled" unless open_request
192
+ handle.complete
193
+ end
194
+ end
195
+ return open_request.result
196
+ end
197
+
198
+
199
+ # Called by {#start_execution}
200
+ # @!visibility private
201
+ def start_internal_workflow(input = NoInput.new, &block)
202
+ get_decision_context
203
+ options = Utilities::interpret_block_for_options(StartWorkflowOptions, block)
204
+ workflow_id_future, run_id_future = Future.new, Future.new
205
+ output = WorkflowFuture.new(AWS::SimpleWorkflow::WorkflowExecution.new(@domain, workflow_id_future, run_id_future))
206
+ options = Utilities::merge_all_options(@options, options)
207
+ new_options = StartWorkflowOptions.new(options)
208
+ open_request = OpenRequestInfo.new
209
+ workflow_id = new_options.workflow_id
210
+ run_id = @decision_context.workflow_context.decision_task.workflow_execution.run_id
211
+ workflow_id ||= @decision_helper.get_next_id(run_id.to_s + ":")
212
+ workflow_id_future.set(workflow_id)
213
+ error_handler do |t|
214
+ t.begin do
215
+ @data_converter = new_options.data_converter
216
+ input = @data_converter.dump input unless input.empty?
217
+ attributes = {}
218
+ new_options.input ||= input unless input.empty?
219
+ if @workflow_class != nil && new_options.execution_method.nil?
220
+ new_options.execution_method = @workflow_class.entry_point
221
+ end
222
+ raise "Can't find an execution method for workflow #{@workflow_class}" if new_options.execution_method.nil?
223
+
224
+ attributes[:options] = new_options
225
+ attributes[:workflow_id] = workflow_id
226
+ # TODO Use ChildWorkflowOptions
227
+ attributes[:tag_list] = []
228
+
229
+ external_task do |external|
230
+ external.initiate_task do |handle|
231
+ open_request.completion_handle = handle
232
+ open_request.run_id = run_id_future
233
+ open_request.description = output.workflow_execution
234
+ @decision_helper.scheduled_external_workflows[workflow_id.to_s] = open_request
235
+ @decision_helper[workflow_id.to_s] = ChildWorkflowDecisionStateMachine.new(workflow_id, attributes)
236
+ end
237
+
238
+ external.cancellation_handler do |handle, cause|
239
+ state_machine = @decision_helper[workflow_id.to_s]
240
+ if state_machine.current_state == :created
241
+ open_request = @decision_helper.scheduled_external_workflows.delete(workflow_id)
242
+ open_request.complete
243
+ end
244
+ state_machine.consume(:cancel)
245
+ end
246
+ end
247
+
248
+ t.rescue(Exception) do |error|
249
+ if error.is_a? ChildWorkflowFailedException
250
+ details = @data_converter.load(error.details)
251
+ error.details = details
252
+ error.cause = details
253
+ end
254
+ @failure_map[workflow_id.to_s] = error
255
+ end
256
+ t.ensure do
257
+ result = @data_converter.load open_request.result
258
+ output.set(result)
259
+ raise @failure_map[workflow_id.to_s] if @failure_map[workflow_id.to_s] && new_options.return_on_start
260
+ end
261
+ end
262
+ end
263
+ return output if new_options.return_on_start
264
+ output.get
265
+ this_failure = @failure_map[workflow_id.to_s]
266
+ raise this_failure if this_failure
267
+ return output.get
268
+ end
269
+
270
+
271
+ # Called by {#start_execution}
272
+ # @!visibility private
273
+ def start_external_workflow(input = NoInput.new, &block)
274
+ options = Utilities::interpret_block_for_options(StartWorkflowOptions, block)
275
+ options = Utilities::merge_all_options(@options, options)
276
+ @converter ||= YAMLDataConverter.new
277
+ # Basically, we want to avoid the special "NoInput, but allow stuff like nil in"
278
+ if ! (input.class <= NoInput || input.empty?)
279
+ options[:input] = @converter.dump input
280
+ end
281
+ if @workflow_class.nil?
282
+ execution_method = @options.execution_method
283
+ version = @options.version
284
+ else
285
+ # TODO This is a nasty hack
286
+ workflow_type = @workflow_class.workflows.first
287
+ execution_method = workflow_type.options.execution_method
288
+ version = workflow_type.version
289
+ end
290
+ version = options[:version] ? options[:version] : version
291
+ execution_method = options[:execution_method] ? options[:execution_method] : execution_method
292
+ raise "Can't find an execution method for workflow #{workflow_class}" if execution_method.nil?
293
+ # TODO A real workflowtype function
294
+ workflow_name = @options.workflow_name || @options.prefix_name
295
+ workflow_type_name = workflow_name.to_s + "." + execution_method.to_s
296
+
297
+ task_list = options[:task_list]
298
+ options[:task_list] = { :name => task_list } if options[:task_list]
299
+ options[:workflow_id] ||= UUIDTools::UUID.random_create.to_s
300
+ options[:domain] = @domain.name
301
+ options[:workflow_type] = {
302
+ :name => workflow_type_name.to_s,
303
+ :version => version.to_s
304
+ }
305
+ [:prefix_name, :workflow_name, :version, :execution_method, :data_converter].each {|key| options.delete(key)}
306
+ run_id = @service.start_workflow_execution(options)["runId"]
307
+ this_workflow = @domain.workflow_executions.at(options[:workflow_id], run_id)
308
+ this_workflow
309
+ end
310
+
311
+ def is_execution_method(method_name)
312
+ (@workflow_class.workflows.map(&:options).map(&:execution_method).map(&:to_sym).include? method_name) || method_name == @workflow_class.entry_point
313
+ end
314
+
315
+ def method_missing(method_name, *args, &block)
316
+ if is_execution_method(method_name)
317
+ start_execution(*args, &block)
318
+ else
319
+ super(method_name, *args, &block)
320
+ end
321
+ end
322
+ def request_cancel_workflow_execution(future)
323
+ workflow_execution = future.workflow_execution
324
+ run_id = workflow_execution.run_id.get
325
+ workflow_id = workflow_execution.workflow_id.get
326
+ state_machine = @decision_helper[workflow_id]
327
+ state_machine.run_id = run_id
328
+ state_machine.consume(:cancel)
329
+ end
330
+ end
331
+
332
+
333
+ # Instances of WorkflowFactory are generated by {#workflow_factory}.
334
+ class WorkflowFactory
335
+
336
+
337
+ # Creates a new WorkflowFactory with the provided parameters. The construction parameters will be used for any
338
+ # workflow clients generated by this workflow factory.
339
+ #
340
+ # @param service
341
+ # The service to use for workflow clients generated by this workflow factory
342
+ #
343
+ # @param domain
344
+ # The SWF [Domain](http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/SimpleWorkflow/Domain.html) to use for
345
+ # workflow clients generated by this workflow factory
346
+ #
347
+ # @param block
348
+ # A block of {StartWorkflowOptions} to use for clients generated by this workflow factory.
349
+ #
350
+ def initialize(service, domain, block)
351
+ @service = service
352
+ @domain = domain
353
+ @options = Utilities::interpret_block_for_options(StartWorkflowOptions, block)
354
+ @workflow_class = get_const(@options.workflow_name) rescue nil
355
+ if @workflow_class
356
+ workflow_type = @workflow_class.workflows.delete_if {|wf_type| wf_type.version.nil? }.first
357
+ @options.version = workflow_type.version
358
+ end
359
+ end
360
+
361
+
362
+ # Get a {WorkflowClient} with the parameters used in the construction of this {WorkflowFactory}.
363
+ #
364
+ # @return [WorkflowClient]
365
+ # A workflow client, created with the parameters used when creating the {WorkflowFactory}.
366
+ #
367
+ def get_client
368
+ WorkflowClient.new(@service, @domain, @workflow_class, @options)
369
+ end
370
+
371
+ end
372
+
373
+ end
374
+ end
@@ -0,0 +1,104 @@
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
+
20
+ # Represents a workflow clock that can create timers.
21
+ class WorkflowClock
22
+
23
+ # Gets or sets the replaying status of the workflow clock.
24
+ attr_accessor :replaying
25
+
26
+ # Gets or sets the current time in milliseconds for a replay.
27
+ attr_accessor :replay_current_time_millis
28
+
29
+
30
+ # Create a new {WorkflowClock} instance.
31
+ #
32
+ # @param [DecisionHelper] decision_helper
33
+ # The decision helper used by the workflow clock to schedule and execute timers.
34
+ #
35
+ def initialize(decision_helper)
36
+ #Map from timerIDs to OpenRequestInfo
37
+ @scheduled_timers = {}
38
+ @replaying = true
39
+ @replay_current_time_millis
40
+ @decision_helper = decision_helper
41
+ end
42
+
43
+
44
+ # Get the current time.
45
+ #
46
+ # @return [Time]
47
+ # A `Time` object initialized to the the current time in milliseconds for a replay. This is the same time that
48
+ # is provided by the `:replay_current_time_millis` attribute.
49
+ #
50
+ def current_time
51
+ @replay_current_time_millis
52
+ end
53
+
54
+ # Create a new timer that executes the supplied block after a specified number of seconds.
55
+ #
56
+ # @param delay_seconds
57
+ # The number of seconds to wait before executing the block. Set to 0 to execute the block immediately.
58
+ #
59
+ # @param block
60
+ # A block to execute after `delay_seconds` has expired.
61
+ #
62
+ def create_timer(delay_seconds, block)
63
+ raise IllegalArgumentException if delay_seconds < 0
64
+ if delay_seconds == 0
65
+ if block
66
+ return block.call
67
+ else
68
+ return
69
+ end
70
+ end
71
+ attributes = {}
72
+ timer_id = @decision_helper.get_next_id(:Timer)
73
+ attributes[:timer_id] = timer_id
74
+ attributes[:start_to_fire_timeout] = delay_seconds.to_s
75
+ open_request = OpenRequestInfo.new
76
+ open_request.blocking_promise = Future.new
77
+ if block
78
+ open_request.result = task do
79
+ open_request.blocking_promise.get
80
+ block.call
81
+ end
82
+ else
83
+ open_request.result = open_request.blocking_promise
84
+ end
85
+ external_task do |t|
86
+ t.initiate_task do |handle|
87
+ open_request.completion_handle = handle
88
+ @decision_helper.scheduled_timers[timer_id.to_s] = open_request
89
+ @decision_helper[timer_id.to_s] = TimerDecisionStateMachine.new(timer_id, attributes)
90
+ end
91
+ t.cancellation_handler do |handle, cause|
92
+ state_machine = @decision_helper[timer_id]
93
+ open_request = @decision_helper.scheduled_timers.delete(timer_id)
94
+ open_request.completion_handle.complete
95
+ state_machine.consume(:cancel)
96
+ state_machine.cancelled = true
97
+ end
98
+ end
99
+ return open_request.result.get
100
+ end
101
+ end
102
+
103
+ end
104
+ end