aws-flow 2.3.1 → 2.4.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 (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