aws-flow 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +8 -8
  2. data/aws-flow.gemspec +3 -2
  3. data/bin/aws-flow-ruby +1 -1
  4. data/bin/aws-flow-utils +5 -0
  5. data/lib/aws/decider.rb +7 -0
  6. data/lib/aws/decider/async_retrying_executor.rb +1 -1
  7. data/lib/aws/decider/data_converter.rb +161 -0
  8. data/lib/aws/decider/decider.rb +27 -14
  9. data/lib/aws/decider/flow_defaults.rb +28 -0
  10. data/lib/aws/decider/implementation.rb +0 -1
  11. data/lib/aws/decider/options.rb +2 -2
  12. data/lib/aws/decider/starter.rb +207 -0
  13. data/lib/aws/decider/task_poller.rb +4 -4
  14. data/lib/aws/decider/utilities.rb +38 -0
  15. data/lib/aws/decider/version.rb +1 -1
  16. data/lib/aws/decider/worker.rb +8 -7
  17. data/lib/aws/decider/workflow_definition_factory.rb +1 -1
  18. data/lib/aws/runner.rb +146 -65
  19. data/lib/aws/templates.rb +4 -0
  20. data/lib/aws/templates/activity.rb +69 -0
  21. data/lib/aws/templates/base.rb +87 -0
  22. data/lib/aws/templates/default.rb +146 -0
  23. data/lib/aws/templates/starter.rb +256 -0
  24. data/lib/aws/utils.rb +270 -0
  25. data/spec/aws/decider/integration/activity_spec.rb +7 -1
  26. data/spec/aws/decider/integration/data_converter_spec.rb +39 -0
  27. data/spec/aws/decider/integration/integration_spec.rb +12 -5
  28. data/spec/aws/decider/integration/options_spec.rb +23 -9
  29. data/spec/aws/decider/integration/starter_spec.rb +209 -0
  30. data/spec/aws/decider/unit/data_converter_spec.rb +276 -0
  31. data/spec/aws/decider/unit/decider_spec.rb +1360 -1386
  32. data/spec/aws/decider/unit/options_spec.rb +21 -22
  33. data/spec/aws/decider/unit/retry_spec.rb +8 -0
  34. data/spec/aws/decider/unit/starter_spec.rb +159 -0
  35. data/spec/aws/runner/integration/runner_integration_spec.rb +2 -3
  36. data/spec/aws/runner/unit/runner_unit_spec.rb +128 -38
  37. data/spec/aws/templates/unit/activity_spec.rb +89 -0
  38. data/spec/aws/templates/unit/base_spec.rb +72 -0
  39. data/spec/aws/templates/unit/default_spec.rb +141 -0
  40. data/spec/aws/templates/unit/starter_spec.rb +271 -0
  41. data/spec/spec_helper.rb +9 -11
  42. metadata +41 -4
@@ -0,0 +1,4 @@
1
+ require 'aws/templates/base'
2
+ require 'aws/templates/activity'
3
+ require 'aws/templates/default'
4
+ require 'aws/templates/starter'
@@ -0,0 +1,69 @@
1
+ module AWS
2
+ module Flow
3
+ module Templates
4
+
5
+ # This template represents an Activity in SWF. It holds the name and
6
+ # scheduling options for the activity
7
+ class ActivityTemplate < TemplateBase
8
+ attr_reader :name, :options
9
+
10
+ def initialize(name, opts = {})
11
+ options = opts.dup
12
+ # Split the name into prefix name and activity method
13
+ prefix_name, @name = name.split(".")
14
+
15
+ # Raise if we don't have a fully qualified name for the activity
16
+ raise ArgumentError, "Activity name should be fully qualified: "\
17
+ "<prefix_name>.<activity_method>" unless @name
18
+
19
+ # Get all the property keys from the ActivityOptions class
20
+ keys = ActivityOptions.held_properties.push(:exponential_retry)
21
+
22
+ # Only select the options that are needed
23
+ options.select!{ |x| keys.include?(x) }
24
+
25
+ # Merge in default values for the activity in case they are not passed
26
+ # by the user
27
+ options = {
28
+ version: FlowConstants.defaults[:version],
29
+ prefix_name: "#{prefix_name}",
30
+ data_converter: FlowConstants.defaults[:data_converter],
31
+ exponential_retry: FlowConstants.defaults[:retry_policy]
32
+ }.merge(options)
33
+
34
+ @options = options
35
+ end
36
+
37
+ # Uses the ActivityClient given in the context (workflow class) passed
38
+ # in by the calling template to schedule this activity
39
+ def run(input, context)
40
+ # Get a duplicate of the options hash so as not to change what's
41
+ # stored in this object
42
+ options = @options.dup
43
+ # If a :tasklist key is passed as input to this template, then schedule
44
+ # this activity on that tasklist
45
+ if input.is_a?(Hash) && input[:task_list]
46
+ options.merge!(task_list: input[:task_list])
47
+ end
48
+ # Schedule the activity using the ActivityClient in the context
49
+ context.act_client.send(@name, input) { options }
50
+ end
51
+ end
52
+
53
+ # Initializes an activity template
54
+ # @param {String} name
55
+ # @param {Hash} options
56
+ def activity(name, opts = {})
57
+ AWS::Flow::Templates.send(:activity, name, opts)
58
+ end
59
+
60
+ # Initializes an activity template
61
+ # @param {String} name
62
+ # @param {Hash} options
63
+ def self.activity(name, opts = {})
64
+ ActivityTemplate.new(name, opts)
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,87 @@
1
+ module AWS
2
+ module Flow
3
+ module Templates
4
+
5
+ # A Template is a precanned workflow definition that can be combined with
6
+ # other templates to construct a workflow body. TemplateBase is a class
7
+ # that must be inherited by all templates. It provides the 'abstract'
8
+ # method #run that needs to be implemented by subclasses.
9
+ class TemplateBase
10
+
11
+ # This method needs to be implemented by the sub classes.
12
+ # @param {Hash} input
13
+ # This is the input to the template
14
+ # @param {AWS::Flow::Workflows} context
15
+ # A class that extends AWS::Flow::Workflows. The workflow that runs a
16
+ # template passes itself as an argument to provide the template with
17
+ # the right context to execute in.
18
+ def run(input, context)
19
+ raise NotImplementedError, "Please implement the #run method of your template."
20
+ end
21
+
22
+ end
23
+
24
+ # Root template is the top level template that is sent to a workflow to
25
+ # run. It contains a step (which is another template) that it passes the
26
+ # input and the context to. It also contains a result_step that it uses to
27
+ # report the result of the workflow.
28
+ class RootTemplate < TemplateBase
29
+ attr_reader :step, :input
30
+ attr_accessor :result_step
31
+
32
+ def initialize(step, result_step)
33
+ @step = step
34
+ @result_step = result_step
35
+ end
36
+
37
+ # Calls the run method on the step (top level template). Manages overall
38
+ # error handling and reporting of results for the workflow
39
+ def run(input, context)
40
+ result = nil
41
+ failure = nil
42
+ begin
43
+ result = @step.run(input, context)
44
+ rescue Exception => e
45
+ failure = e
46
+ ensure
47
+ if failure
48
+ # If there is a result_step, pass the failure as an input to it.
49
+ @result_step.run({failure: failure}, context) if @result_step
50
+ # Now fail the workflow
51
+ raise e
52
+ else
53
+ # Pass the result as an input to the result_step
54
+ @result_step.run(result, context) if @result_step
55
+ # Complete the workflow by returning the result
56
+ result
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # Initializes a root template
63
+ # @param {TemplateBase} step
64
+ # An AWS Flow Framework Template class that inherits TemplateBase. It
65
+ # contains the actual orchestration of workflow logic inside it.
66
+ # @param {ActivityTemplate} result_step
67
+ # An optional ActivityTemplate that can be used to report the result
68
+ # of the 'step'
69
+ def root(step, result_step = nil)
70
+ AWS::Flow::Templates.send(:root, step, result_step)
71
+ end
72
+
73
+ # Initializes a root template
74
+ # @param {TemplateBase} step
75
+ # An AWS Flow Framework Template class that inherits TemplateBase. It
76
+ # contains the actual orchestration of workflow logic inside it.
77
+ # @param {ActivityTemplate} result_step
78
+ # An optional ActivityTemplate that can be used to report the result
79
+ # of the 'step'
80
+ def self.root(step, result_step = nil)
81
+ RootTemplate.new(step, result_step)
82
+ end
83
+
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,146 @@
1
+ module AWS
2
+ module Flow
3
+ module Templates
4
+
5
+ # Default workflow class for the AWS Flow Framework for Ruby. It
6
+ # can run workflows defined by WorkflowTemplates.
7
+ class FlowDefaultWorkflowRuby
8
+ extend AWS::Flow::Workflows
9
+
10
+ # Create activity client and child workflow client
11
+ activity_client :act_client
12
+ child_workflow_client :child_client
13
+
14
+ # Create the workflow type with default options
15
+ workflow FlowConstants.defaults[:execution_method] do
16
+ {
17
+ version: FlowConstants.defaults[:version],
18
+ prefix_name: FlowConstants.defaults[:prefix_name],
19
+ default_task_list: FlowConstants.defaults[:task_list],
20
+ default_execution_start_to_close_timeout: FlowConstants.defaults[:execution_start_to_close_timeout]
21
+ }
22
+ end
23
+
24
+ # Define the workflow method :start. It will take in an input hash
25
+ # that contains the root template (:definition) and the arguments to the
26
+ # template (:args).
27
+ # @param input Hash
28
+ # A hash containing the following keys -
29
+ # definition: An object of type AWS::Flow::Templates::RootTemplate
30
+ # args: Hash of arguments to be passed to the definition
31
+ #
32
+ def start(input)
33
+
34
+ raise ArgumentError, "Workflow input must be a Hash" unless input.is_a?(Hash)
35
+ raise ArgumentError, "Input hash must contain key :definition" if input[:definition].nil?
36
+ raise ArgumentError, "Input hash must contain key :args" if input[:args].nil?
37
+
38
+ definition = input[:definition]
39
+ args = input[:args]
40
+
41
+ unless definition.is_a?(AWS::Flow::Templates::RootTemplate)
42
+ raise "Workflow Definition must be a AWS::Flow::Templates::RootTemplate"
43
+ end
44
+ raise "Input must be a Hash" unless args.is_a?(Hash)
45
+
46
+ # Run the root workflow template
47
+ definition.run(args, self)
48
+
49
+ end
50
+
51
+ end
52
+
53
+ # Proxy classes for user activities are created in this module
54
+ module ActivityProxies; end
55
+
56
+ # Used to convert a regular ruby class into a Ruby Flow Activity class,
57
+ # i.e. extends the AWS::Flow::Activities module. It converts all user
58
+ # defined instance methods into activities and assigns the following
59
+ # defaults to the ActivityType - version: "1.0"
60
+ def self.make_activity_class(klass)
61
+ return klass if klass.nil?
62
+
63
+ name = klass.name.split(":").last
64
+
65
+ proxy_name = name + "Proxy"
66
+ # Create a proxy activity class that will define activities for all
67
+ # instance methods of the class.
68
+ new_klass = self::ActivityProxies.const_set(proxy_name.to_sym, Class.new(Object))
69
+
70
+ # Extend the AWS::Flow::Activities module and create activities for all
71
+ # instance methods
72
+ new_klass.class_exec do
73
+ extend AWS::Flow::Activities
74
+
75
+ attr_reader :instance
76
+
77
+ @@klass = klass
78
+
79
+ def initialize
80
+ @instance = @@klass.new
81
+ end
82
+
83
+ # Creates activities for all instance methods of the held klass
84
+ @@klass.instance_methods(false).each do |method|
85
+ activity(method) do
86
+ {
87
+ version: "1.0",
88
+ prefix_name: name
89
+ }
90
+ end
91
+ end
92
+
93
+ # Redirect all method calls to the held instance
94
+ def method_missing(method, *args, &block)
95
+ @instance.send(method, *args, &block)
96
+ end
97
+
98
+ end
99
+ new_klass
100
+ end
101
+
102
+ # Default result reporting activity class for the AWS Flow Framework for
103
+ # Ruby
104
+ class FlowDefaultResultActivityRuby
105
+ extend AWS::Flow::Activities
106
+
107
+ attr_reader :result
108
+
109
+ # Create the activity type with default options
110
+ activity FlowConstants.defaults[:result_activity_method] do
111
+ {
112
+ version: FlowConstants.defaults[:result_activity_version],
113
+ prefix_name: FlowConstants.defaults[:result_activity_prefix],
114
+ default_task_list: FlowConstants.defaults[:task_list],
115
+ exponential_retry: FlowConstants.defaults[:retry_policy]
116
+ }
117
+ end
118
+
119
+ # Initialize the future upon instantiation
120
+ def initialize
121
+ @result = Future.new
122
+ end
123
+
124
+ # Set the future when the activity is run
125
+ def run(input)
126
+ @result.set(input)
127
+ input
128
+ end
129
+
130
+ end
131
+
132
+ # Returns the default result activity class
133
+ # @api private
134
+ def self.result_activity
135
+ return AWS::Flow::Templates.const_get(FlowConstants.defaults[:result_activity_prefix])
136
+ end
137
+
138
+ # Returns the default workflow class
139
+ # @api private
140
+ def self.default_workflow
141
+ return AWS::Flow::Templates.const_get(FlowConstants.defaults[:prefix_name])
142
+ end
143
+
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,256 @@
1
+ module AWS
2
+ module Flow
3
+
4
+ # @api private
5
+ module Templates
6
+
7
+ # Starts an Activity or a Workflow Template execution using the default
8
+ # workflow class FlowDefaultWorkflowRuby
9
+ #
10
+ # @param [String or AWS::Flow::Templates::TemplateBase] name_or_klass
11
+ # The Activity or the Workflow Template that needs to be scheduled via
12
+ # the default workflow. This argument can either be a string that
13
+ # represents a fully qualified activity name - <ActivityClass>.<method_name>
14
+ # or it can be an instance of AWS::Flow::Templates::TemplateBase
15
+ #
16
+ # @param [Hash] input
17
+ # Input hash for the workflow execution
18
+ #
19
+ # @param [Hash] opts
20
+ # Additional options to configure the workflow or activity execution.
21
+ #
22
+ # @option opts [true, false] :wait
23
+ # *Optional* This boolean flag can be set to true if the result of the
24
+ # task is required. Default value is false.
25
+ #
26
+ # @option opts [Integer] :wait_timeout
27
+ # *Optional* This sets the timeout value for :wait. Default value is
28
+ # nil.
29
+ #
30
+ # @option opts [Hash] :exponential_retry
31
+ # A hash of {AWS::Flow::ExponentialRetryOptions}. Default value is -
32
+ # { maximum_attempts: 3 }
33
+ #
34
+ # @option opts [String] *Optional* :domain
35
+ # Default value is FlowDefault
36
+ #
37
+ # @option opts [Integer] *Optional* :execution_start_to_close_timeout
38
+ # Default value is 3600 seconds (1 hour)
39
+ #
40
+ # @option opts [Integer] *Optional* :retention_in_days
41
+ # Default value is 7 days
42
+ #
43
+ # @option opts [String] *Optional* :workflow_id
44
+ #
45
+ # @option opts [Integer] *Optional* :task_priority
46
+ # Default value is 0
47
+ #
48
+ # @option opts [String] *Optional* :tag_list
49
+ # By default, the name of the activity task gets added to the workflow's
50
+ # tag_list
51
+ #
52
+ # @option opts *Optional* :data_converter
53
+ # Default value is {AWS::Flow::YAMLDataConverter}. To use the
54
+ # {AWS::Flow::S3DataConverter}, set the AWS_SWF_BUCKET_NAME environment
55
+ # variable name with a valid AWS S3 bucket name.
56
+ #
57
+ # @option opts *Optional* A hash of {AWS::Flow::ActivityOptions}
58
+ #
59
+ # Usage -
60
+ #
61
+ # AWS::Flow::start("<ActivityClassName>.<method_name>", <input_hash>,
62
+ # <options_hash> )
63
+ #
64
+ # Example -
65
+ #
66
+ # 1) Start an activity execution -
67
+ # AWS::Flow::start("HelloWorldActivity.say_hello", { name: "World" })
68
+ #
69
+ # 2) Start an activity execution with overriden options -
70
+ # AWS::Flow::start("HelloWorldActivity.say_hello", { name: "World" }, {
71
+ # exponential_retry: { maximum_attempts: 10 } }
72
+ # )
73
+ #
74
+ def self.start(name_or_klass, input, opts = {})
75
+
76
+ options = opts.dup
77
+
78
+ if name_or_klass.is_a?(String)
79
+ # Add activity name as a tag to the workflow execution
80
+ (options[:tag_list] ||= []) << name_or_klass
81
+
82
+ # If name_or_klass passed in is a string, we are assuming the user is
83
+ # trying to start a single activity task. Wrap the activity information
84
+ # in the activity template
85
+ name_or_klass = AWS::Flow::Templates.activity(name_or_klass, options)
86
+
87
+ # Keep only the required options in the hash
88
+ keys = [
89
+ :domain,
90
+ :retention_in_days,
91
+ :execution_start_to_close_timeout,
92
+ :task_priority,
93
+ :wait,
94
+ :wait_timeout,
95
+ :workflow_id,
96
+ :data_converter,
97
+ :tag_list
98
+ ]
99
+ options.select! { |x| keys.include?(x) }
100
+
101
+ end
102
+
103
+ # Wrap the template in a root template
104
+ root = AWS::Flow::Templates.root(name_or_klass)
105
+
106
+ # Get the default options and merge them with the options passed in. The
107
+ # order of the two hashes 'defaults' and 'options' is important here.
108
+ defaults = FlowConstants.defaults.select do |key|
109
+ [
110
+ :domain,
111
+ :prefix_name,
112
+ :execution_method,
113
+ :version,
114
+ :execution_start_to_close_timeout,
115
+ :data_converter,
116
+ :task_list
117
+ ].include?(key)
118
+ end
119
+ options = defaults.merge(options)
120
+
121
+ raise "input needs to be a Hash" unless input.is_a?(Hash)
122
+
123
+ # Set the input for the default workflow
124
+ workflow_input = {
125
+ definition: root,
126
+ args: input,
127
+ }
128
+
129
+ # Set the result_step for the root template if wait flag is
130
+ # set.
131
+ wait = options.delete(:wait)
132
+ wait_timeout = options.delete(:wait_timeout)
133
+ result_tasklist = set_result_activity(root) if wait
134
+
135
+ # Call #start_workflow with the correct options to start the workflow
136
+ # execution
137
+ begin
138
+ AWS::Flow::start_workflow(workflow_input, options)
139
+ rescue AWS::SimpleWorkflow::Errors::UnknownResourceFault => e
140
+ register_defaults(options[:domain])
141
+ AWS::Flow::start_workflow(workflow_input, options)
142
+ end
143
+
144
+ # Wait for result
145
+ get_result(result_tasklist, options[:domain], wait_timeout) if wait
146
+
147
+ end
148
+
149
+ # Sets the result activity with a unique tasklist name for the root template.
150
+ # @api private
151
+ def self.set_result_activity(root)
152
+ # We want the result to be sent to a specific tasklist so that no other
153
+ # worker gets the result of this workflow.
154
+ result_tasklist = "result_tasklist: #{SecureRandom.uuid}"
155
+
156
+ name = "#{FlowConstants.defaults[:result_activity_prefix]}."\
157
+ "#{FlowConstants.defaults[:result_activity_method]}"
158
+
159
+ # Set the result_step of the root template to the result activity and
160
+ # override the tasklist and timeouts.
161
+ root.result_step = activity(name, {
162
+ task_list: result_tasklist,
163
+ schedule_to_start_timeout: FlowConstants.defaults[:schedule_to_start_timeout],
164
+ start_to_close_timeout: FlowConstants.defaults[:start_to_close_timeout]
165
+ }
166
+ )
167
+ result_tasklist
168
+ end
169
+
170
+ # Gets the result of the workflow execution by starting an ActivityWorker
171
+ # on the FlowDefaultResultActivityRuby class. The result activity will set
172
+ # the instance variable future :result with the result of the template.
173
+ # It will block till either the result future is set or till the timeout
174
+ # expires - whichever comes first.
175
+ # @api private
176
+ def self.get_result(tasklist, domain, timeout=nil)
177
+
178
+ swf = AWS::SimpleWorkflow.new
179
+ domain = swf.domains[domain]
180
+
181
+ # Create a new instance of the FlowDefaultResultActivityRuby class and
182
+ # add it to the ActivityWorker. We pass in the instance instead of the
183
+ # class itself, so that we can locally access the instance variable set
184
+ # by the activity method.
185
+ activity = FlowDefaultResultActivityRuby.new
186
+
187
+ # Create the activity worker to poll on the result tasklist
188
+ worker = AWS::Flow::ActivityWorker.new(domain.client, domain, tasklist, activity) {{ use_forking: false }}
189
+
190
+ # Keep polling till we get the result or timeout. A 0 or nil timeout
191
+ # will let the loop run to completion.
192
+ begin
193
+ Timeout::timeout(timeout) do
194
+ until activity.result.set?
195
+ worker.run_once(false)
196
+ end
197
+ end
198
+ rescue Timeout::Error => e
199
+ activity.result.set
200
+ return
201
+ end
202
+
203
+ # Get the result from the future
204
+ result = activity.result.get
205
+ if result.is_a?(Hash) && result[:failure] && result[:failure].is_a?(Exception)
206
+ raise result[:failure]
207
+ end
208
+
209
+ result
210
+ end
211
+
212
+ # Registers the relevant defaults with the Simple Workflow Service
213
+ # @api private
214
+ def self.register_defaults(name=nil)
215
+ domain = name.nil? ? register_default_domain : AWS::SimpleWorkflow.new.domains[name]
216
+
217
+ register_default_workflow(domain)
218
+ register_default_result_activity(domain)
219
+ end
220
+
221
+ # Registers the default domain FlowDefault with the Simple Workflow
222
+ # Service
223
+ # @api private
224
+ def self.register_default_domain
225
+ AWS::Flow::Utilities.register_domain(FlowConstants.defaults[:domain])
226
+ end
227
+
228
+ # Registers the default workflow type FlowDefaultWorkflowRuby with the
229
+ # Simple Workflow Service
230
+ # @api private
231
+ def self.register_default_workflow(domain)
232
+ AWS::Flow::WorkflowWorker.new(
233
+ domain.client,
234
+ domain,
235
+ nil,
236
+ AWS::Flow::Templates.default_workflow
237
+ ).register
238
+ end
239
+
240
+ # Registers the default result activity type FlowDefaultResultActivityRuby
241
+ # with the Simple Workflow Service
242
+ # @api private
243
+ def self.register_default_result_activity(domain)
244
+ worker = AWS::Flow::ActivityWorker.new(
245
+ domain.client,
246
+ domain,
247
+ nil,
248
+ AWS::Flow::Templates.result_activity
249
+ ) {{ use_forking: false }}
250
+ worker.register
251
+ end
252
+
253
+ end
254
+
255
+ end
256
+ end