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,270 @@
1
+ module AWS
2
+ module Flow
3
+ module Utils
4
+
5
+ require 'optparse'
6
+ require 'fileutils'
7
+ require 'json'
8
+
9
+ # Interprets the command-line paramters pased in from the shell.
10
+ #
11
+ # @api private
12
+ def self.parse_command_line(argv = ARGV)
13
+ options = {}
14
+ optparse = OptionParser.new do |opts|
15
+ opts.banner = "Usage: aws-flow-utils -c <command> [options]"
16
+
17
+ opts.separator ""
18
+ options[:deploy] = {enabled: false}
19
+ opts.on('-c', '--command COMMAND', [:eb,:local], "Specify the command to run. (eb, local)") do |f|
20
+ options[:deploy][:enabled] = true
21
+ options[:deploy][:eb] = true if f == :eb
22
+ options[:deploy][:local] = true if f == :local
23
+ end
24
+
25
+ opts.separator ""
26
+ opts.separator "Commands"
27
+ opts.separator ""
28
+ opts.separator "\tlocal: Generates an AWS Flow Framework for Ruby "\
29
+ "application skeleton."
30
+ opts.separator "\t eb: Generates an AWS Flow Framework for Ruby "\
31
+ "application skeleton compatible with AWS Elastic Beanstalk."
32
+ opts.separator ""
33
+ opts.separator "Specific options"
34
+ opts.separator ""
35
+
36
+ opts.on('-n', '--name NAME', "Set the name of the application") do |f|
37
+ options[:deploy][:name] = f
38
+ end
39
+
40
+ opts.on('-r', '--region [REGION]', "Set the AWS Region. Default "\
41
+ "value is taken from environment variable AWS_REGION if "\
42
+ "it is set.") do |f|
43
+ options[:deploy][:region] = f.downcase
44
+ end
45
+
46
+ opts.on('-p', '--path [PATH]', "Set the location where the AWS Flow "\
47
+ "for Ruby application will be created. (Default is '.')") do |f|
48
+ options[:deploy][:path] = f
49
+ end
50
+
51
+ opts.on('-a', '--act_path [PATH]', "Set the location where activity "\
52
+ "classes reside.") do |f|
53
+ options[:deploy][:act] = f
54
+ end
55
+
56
+ opts.on('-w', '--wf_path [PATH]', "Set the location where workflow "\
57
+ "classes reside.") do |f|
58
+ options[:deploy][:wf] = f
59
+ end
60
+
61
+ opts.on('-A', "--activities x,y,z", Array, "Set the names of Activity "\
62
+ "classes") do |f|
63
+ options[:deploy][:activities] = f
64
+ end
65
+
66
+ opts.on('-W', "--workflows x,y,z", Array, "Set the names of Workflow "\
67
+ "classes") do |f|
68
+ options[:deploy][:workflows] = f
69
+ end
70
+
71
+ end
72
+
73
+ optparse.parse!(argv)
74
+
75
+ return options
76
+ end
77
+
78
+ class FlowUtils
79
+ class << self
80
+ attr_accessor :utils
81
+ end
82
+ self.utils ||= []
83
+ end
84
+
85
+ # Initializes an AWS Flow Framework for Ruby application
86
+ class InitConfig
87
+ # Self register with the FlowUtils class
88
+ FlowUtils.utils << self
89
+
90
+ # Generates the appropriate 1-Click URL for beanstalk deployments based on
91
+ # the options passed by the user
92
+ def self.generate_url(options)
93
+ url = "https://console.aws.amazon.com/elasticbeanstalk/"
94
+ url = "#{url}?region=#{options[:region]}#/newApplication?"
95
+ url = "#{url}applicationName=#{options[:name]}"
96
+ url = "#{url}&solutionStackName=Ruby"
97
+ url = "#{url}&instanceType=m1.large"
98
+ puts "AWS Elastic Beanstalk 1-Click URL: #{url}"
99
+ end
100
+
101
+ # Generates the necessary file structure for a valid AWS Flow Ruby
102
+ # application that can be deployed to Elastic Beanstalk
103
+ def self.generate_files(options)
104
+ dirname = "#{options[:path]}/#{options[:name]}"
105
+ puts "Your AWS Flow Framework for Ruby application will be located at: "\
106
+ "#{File.absolute_path(dirname)}/"
107
+ create_app_dir(dirname)
108
+ create_flow_dir(options)
109
+ write_rack_config(dirname) if options[:eb]
110
+ write_worker_config(options, dirname)
111
+ write_gemfile(dirname)
112
+ end
113
+
114
+ # Creates the toplevel application directory
115
+ def self.create_app_dir(dirname)
116
+ FileUtils.mkdir_p(dirname)
117
+ end
118
+
119
+ # Creates the flow/ directory structure for the user applicaiton and
120
+ # copies the activity and workflow files if a location is provided.
121
+ def self.create_flow_dir(options)
122
+ dirname = "#{options[:path]}/#{options[:name]}/flow"
123
+ FileUtils.mkdir_p(dirname)
124
+
125
+ str = require_files(options[:act], dirname)
126
+ # Default to a helpful string if no activity files are given
127
+ str = str.empty? ? "# You can require your activity files here." : str
128
+ # Write the generated string to the activities.rb file
129
+ write_to_file("#{dirname}/activities.rb", str)
130
+
131
+ str = require_files(options[:wf], dirname)
132
+ # Default to a helpful string if no workflow files are given
133
+ str = str.empty? ? "# You can require your workflow files here." : str
134
+ # Write the generated string to the workflows.rb file
135
+ write_to_file("#{dirname}/workflows.rb", str)
136
+
137
+ end
138
+
139
+ # Helper method to copy files recursively into a directory and generate
140
+ # a require string
141
+ # @api private
142
+ def self.require_files(src, dest)
143
+ str = ""
144
+ return str if src.nil?
145
+ # If a valid location is passed in, copy the file(s) into the flow/
146
+ # directory.
147
+ FileUtils.cp_r(src, dest)
148
+ if File.directory?(src)
149
+ # If the given path is a directory, recursively get the relative
150
+ # paths of all the files in the directory and generated the
151
+ # require_relative string.
152
+ Dir.glob("#{dest}/#{File.basename src}/**/*.rb").each do |x|
153
+ a = Pathname.new(x)
154
+ rel_path = a.relative_path_from(Pathname.new("#{dest}"))
155
+ rel_path = rel_path.to_s.split(".rb").first
156
+ str = "require_relative '#{rel_path}'\n#{str}"
157
+ end
158
+ else
159
+ # If the given path is a file, just require the basename of the file
160
+ str = "require_relative '#{File.basename src}'"
161
+ end
162
+ str
163
+ end
164
+
165
+ # Creates a rack config for the beanstalk deployment
166
+ # @api private
167
+ def self.write_rack_config(dirname)
168
+ write_to_file(
169
+ "#{dirname}/config.ru",
170
+ "# Rack config file.\n\n"\
171
+ "system(\"pkill -9 ruby\")\n\nsystem(\"aws-flow-ruby -f worker.json&\")\n\n"\
172
+ "run Proc.new { |env| [200, {'Content-Type' => 'text/html'}, ['Done!']] }"
173
+ )
174
+ end
175
+
176
+ # Creates the json worker config for the user application
177
+ def self.write_worker_config(options, dirname)
178
+ spec = {}
179
+ spec.merge!(generate_worker_spec("activity", :activities, options))
180
+ spec.merge!(generate_worker_spec("workflow", :workflows, options))
181
+
182
+ spec = spec.empty? ? "" : JSON.pretty_generate(spec)
183
+ write_to_file("#{dirname}/worker.json", spec)
184
+ end
185
+
186
+ # Helper method to generate a worker config
187
+ # @api private
188
+ def self.generate_worker_spec(name, key, options)
189
+ spec = {}
190
+ if options[key]
191
+ spec = {
192
+ "#{name}_workers" => [
193
+ {
194
+ "#{name}_classes" => options[key],
195
+ "number_of_workers" => 10
196
+ }
197
+ ]
198
+ }
199
+ end
200
+ return spec
201
+ end
202
+
203
+ # Creates the Gemfile for the user application
204
+ def self.write_gemfile(dirname)
205
+ write_to_file(
206
+ "#{dirname}/Gemfile",
207
+ "source 'http://www.rubygems.org'\n\ngem 'aws-flow', '~> 2.4.0'"
208
+ )
209
+ end
210
+
211
+ # Helper method to write a string to a file
212
+ def self.write_to_file(name, string)
213
+ File.open(name, 'w') { |f| f.write(string) }
214
+ end
215
+
216
+ # Validates various options
217
+ def self.check_options(options)
218
+ raise ArgumentError, "You must specify a name for your application" unless options[:name]
219
+ options[:region] ||= ENV['AWS_REGION'].downcase if ENV['AWS_REGION']
220
+ options[:path] ||= "."
221
+
222
+ # We need the region for the 1-Click URL
223
+ if options[:eb]
224
+ raise ArgumentError, "You must specify an AWS region argument or export a "\
225
+ "variable AWS_REGION in your shell" unless options[:region]
226
+ end
227
+
228
+ # If a location for activity classes is provided, then ensure that it
229
+ # exists
230
+ if options[:act] && !File.exist?(options[:act])
231
+ raise ArgumentError, "Please provide a valid location where your activity "\
232
+ "classes are located."
233
+ end
234
+
235
+ # If a location for workflow classes is provided, then ensure that it
236
+ # exists
237
+ if options[:wf] && !File.exist?(options[:wf])
238
+ raise ArgumentError, "Please provide a valid location where your workflow "\
239
+ "classes are located."
240
+ end
241
+
242
+ end
243
+
244
+ # Main method that will be called by the FlowUtils utility.
245
+ def self.generate(options)
246
+ return unless options[:deploy][:enabled]
247
+ opts = options[:deploy]
248
+ check_options(opts)
249
+ puts "---"
250
+ generate_files(opts)
251
+ generate_url(opts) if opts[:eb]
252
+ puts "---"
253
+ end
254
+
255
+ end
256
+
257
+ # Invoked from the shell.
258
+ #
259
+ # @api private
260
+ def self.main
261
+ FlowUtils.utils.each { |x| x.generate(parse_command_line) }
262
+ end
263
+
264
+ end
265
+ end
266
+ end
267
+
268
+ if __FILE__ == $0
269
+ AWS::Flow::Utils.main
270
+ end
@@ -2,9 +2,16 @@ require_relative 'setup'
2
2
 
3
3
  describe Activities do
4
4
  before(:all) do
5
+ @bucket = ENV['AWS_SWF_BUCKET_NAME']
6
+ ENV['AWS_SWF_BUCKET_NAME'] = nil
5
7
  @swf, @domain = setup_swf
6
8
  end
7
9
 
10
+ after(:all) do
11
+ ENV['AWS_SWF_BUCKET_NAME'] = @bucket
12
+ end
13
+
14
+
8
15
  it "ensures that a real activity will get scheduled" do
9
16
  task_list = "activity_task_list"
10
17
  class Blah
@@ -341,4 +348,3 @@ describe Activities do
341
348
  end
342
349
  end
343
350
  end
344
-
@@ -0,0 +1,39 @@
1
+ require_relative 'setup'
2
+
3
+ describe S3DataConverter do
4
+
5
+ it "tests regular sized input" do
6
+ converter = FlowConstants.defaults[:data_converter]
7
+ list = {
8
+ input: "asdf",
9
+ output: "ddd",
10
+ test: 123,
11
+ }
12
+ s3_link = converter.dump(list)
13
+ converter.load(s3_link).should == list
14
+ expect_any_instance_of(AWS::S3).not_to receive(:buckets)
15
+ end
16
+
17
+ it "tests greater than 32k input" do
18
+ converter = FlowConstants.defaults[:data_converter]
19
+ list = {
20
+ input: "asdf",
21
+ test: "a"*33000,
22
+ }
23
+ s3_link = converter.dump(list)
24
+ converter.load(s3_link).should == list
25
+ end
26
+
27
+ it "tests cache" do
28
+ converter = FlowConstants.defaults[:data_converter]
29
+ list = {
30
+ input: "asdf",
31
+ test: "a"*33000
32
+ }
33
+ s3_link = converter.dump(list)
34
+ key = YAMLDataConverter.new.load(s3_link)
35
+
36
+ converter.cache[key[:s3_filename]].should_not be_nil
37
+ end
38
+
39
+ end
@@ -31,7 +31,11 @@ class TestHistoryAttributes
31
31
  end
32
32
 
33
33
  describe "RubyFlowDecider" do
34
+
34
35
  before(:all) do
36
+ @bucket = ENV['AWS_SWF_BUCKET_NAME']
37
+ ENV['AWS_SWF_BUCKET_NAME'] = nil
38
+
35
39
  class MyWorkflow
36
40
  extend AWS::Flow::Workflows
37
41
  version "1"
@@ -51,6 +55,9 @@ describe "RubyFlowDecider" do
51
55
  kill_executors
52
56
  kill_executors
53
57
  end
58
+ after(:all) do
59
+ ENV['AWS_SWF_BUCKET_NAME'] = @bucket
60
+ end
54
61
 
55
62
  it "runs an empty workflow, making sure base configuration stuff is correct" do
56
63
  target_workflow = @domain.workflow_types.page(:per_page => 1000).select { |x| x.name == "blank_workflow_test"}
@@ -240,7 +247,7 @@ describe "RubyFlowDecider" do
240
247
  history_events.last.should == "WorkflowExecutionFailed"
241
248
  workflow_execution.events.to_a.last.attributes.reason.should include("[TRUNCATED]")
242
249
  details = workflow_execution.events.to_a.last.attributes.details
243
- exception = FlowConstants.default_data_converter.load(details)
250
+ exception = FlowConstants.data_converter.load(details)
244
251
  exception.class.should == AWS::Flow::ActivityTaskFailedException
245
252
  end
246
253
 
@@ -295,7 +302,7 @@ describe "RubyFlowDecider" do
295
302
  last_event = workflow_execution.events.to_a.last
296
303
  last_event.event_type.should == "WorkflowExecutionFailed"
297
304
  details = workflow_execution.events.to_a.last.attributes.details
298
- exception = FlowConstants.default_data_converter.load(details)
305
+ exception = FlowConstants.data_converter.load(details)
299
306
  exception.class.should == RuntimeError
300
307
  exception.backtrace.first.should include ("[TRUNCATED]")
301
308
  end
@@ -314,7 +321,7 @@ describe "RubyFlowDecider" do
314
321
  last_event.event_type.should == "WorkflowExecutionFailed"
315
322
  workflow_execution.events.to_a.last.attributes.reason.should include("[TRUNCATED]")
316
323
  details = workflow_execution.events.to_a.last.attributes.details
317
- exception = FlowConstants.default_data_converter.load(details)
324
+ exception = FlowConstants.data_converter.load(details)
318
325
  exception.class.should == RuntimeError
319
326
  end
320
327
 
@@ -424,7 +431,7 @@ describe "RubyFlowDecider" do
424
431
  last_event.event_type.should == "WorkflowExecutionFailed"
425
432
  workflow_execution.events.to_a.last.attributes.reason.should include("[TRUNCATED]")
426
433
  details = workflow_execution.events.to_a.last.attributes.details
427
- exception = FlowConstants.default_data_converter.load(details)
434
+ exception = FlowConstants.data_converter.load(details)
428
435
  exception.class.should == AWS::Flow::ChildWorkflowFailedException
429
436
  exception.cause.class.should == RuntimeError
430
437
  end
@@ -469,7 +476,7 @@ describe "RubyFlowDecider" do
469
476
  last_event.event_type.should == "WorkflowExecutionFailed"
470
477
  workflow_execution.events.to_a.last.attributes.reason.should include("[TRUNCATED]")
471
478
  details = workflow_execution.events.to_a.last.attributes.details
472
- exception = FlowConstants.default_data_converter.load(details)
479
+ exception = FlowConstants.data_converter.load(details)
473
480
  exception.class.should == AWS::Flow::ChildWorkflowFailedException
474
481
  exception.cause.class.should == AWS::Flow::ChildWorkflowFailedException
475
482
  end
@@ -52,6 +52,15 @@ describe "task_priority" do
52
52
  worker.register
53
53
  activity_worker.register
54
54
 
55
+ type = @domain.workflow_types.to_a.find { |x| x.name == "WorkflowForTaskPriority.start" }
56
+ type.default_task_priority.should == 10
57
+
58
+ type = @domain.activity_types.to_a.find { |x| x.name == "ActivityForTaskPriority.run_1" }
59
+ type.default_task_priority.should == 20
60
+
61
+ type = @domain.activity_types.to_a.find { |x| x.name == "ActivityForTaskPriority.run_2" }
62
+ type.default_task_priority.should == 0
63
+
55
64
  client = AWS::Flow::workflow_client(@domain.client, @domain) { {from_class: "WorkflowForTaskPriority"} }
56
65
  execution = client.start_execution
57
66
 
@@ -63,10 +72,10 @@ describe "task_priority" do
63
72
  wait_for_execution(execution)
64
73
 
65
74
  event = execution.events.select { |x| x.event_type =~ /ActivityTaskScheduled/ }
66
- event.first.attributes[:task_priority].should == "20"
67
- event.last.attributes[:task_priority].should == "0"
75
+ event.first.attributes[:task_priority].should == 20
76
+ event.last.attributes[:task_priority].should == 0
68
77
  events = execution.events.select { |x| x.event_type =~ /DecisionTaskScheduled/ }
69
- events.first.attributes[:taskPriority].should == "10"
78
+ events.first.attributes[:taskPriority].should == 10
70
79
  end
71
80
 
72
81
  it "ensures that overriden values of task priority are assigned when workflow is started and when activity is scheduled" do
@@ -80,6 +89,8 @@ describe "task_priority" do
80
89
  activity_worker = ActivityWorker.new(@domain.client, @domain, task_list, ActivityForTaskPriority)
81
90
  worker.register
82
91
  activity_worker.register
92
+ type = @domain.workflow_types.to_a.find { |x| x.name == "WorkflowForTaskPriority.start" }
93
+ type.default_task_priority.should == 10
83
94
 
84
95
  client = AWS::Flow::workflow_client(@domain.client, @domain) { {from_class: "WorkflowForTaskPriority"} }
85
96
  execution = client.start_execution { { task_priority: "100" } }
@@ -91,9 +102,9 @@ describe "task_priority" do
91
102
  wait_for_execution(execution)
92
103
 
93
104
  event = execution.events.select { |x| x.event_type =~ /ActivityTaskScheduled/ }
94
- event.first.attributes[:task_priority].should == "200"
105
+ event.first.attributes[:task_priority].should == 200
95
106
  events = execution.events.select { |x| x.event_type =~ /DecisionTaskScheduled/ }
96
- events.first.attributes[:taskPriority].should == "100"
107
+ events.first.attributes[:taskPriority].should == 100
97
108
  end
98
109
 
99
110
  end
@@ -110,7 +121,7 @@ describe "task_priority" do
110
121
  }
111
122
  end
112
123
  def entry_point
113
- continue_as_new
124
+ continue_as_new { { task_list: "continue_as_new_foo" } }
114
125
  end
115
126
  end
116
127
 
@@ -128,7 +139,7 @@ describe "task_priority" do
128
139
  execution.events.map(&:event_type).last.should == "WorkflowExecutionContinuedAsNew"
129
140
  execution.status.should == :continued_as_new
130
141
  events = execution.events.select { |x| x.event_type == "WorkflowExecutionContinuedAsNew" }
131
- events.first.attributes.task_priority.should == "50"
142
+ events.first.attributes.task_priority.should == 50
132
143
  end
133
144
 
134
145
 
@@ -150,7 +161,7 @@ describe "task_priority" do
150
161
  execution.events.map(&:event_type).last.should == "WorkflowExecutionContinuedAsNew"
151
162
  execution.status.should == :continued_as_new
152
163
  events = execution.events.select { |x| x.event_type == "WorkflowExecutionContinuedAsNew" }
153
- events.first.attributes.task_priority.should == "100"
164
+ events.first.attributes.task_priority.should == 100
154
165
  end
155
166
  end
156
167
 
@@ -190,18 +201,21 @@ describe "task_priority" do
190
201
  parent_client = AWS::Flow::workflow_client(@domain.client, @domain) { { from_class: "ChildWorkflowsTestParentWorkflow" } }
191
202
  @child_worker = WorkflowWorker.new(@domain.client, @domain, "test2", ChildWorkflowsTestChildWorkflow)
192
203
  @parent_worker = WorkflowWorker.new(@domain.client, @domain, "test", ChildWorkflowsTestParentWorkflow)
204
+ @child_worker.register
205
+ @parent_worker.register
193
206
 
194
207
  @forking_executor = ForkingExecutor.new(:max_workers => 3)
195
208
  @forking_executor.execute { @parent_worker.start }
196
209
  @forking_executor.execute { @child_worker.start }
197
210
  @forking_executor.execute { @child_worker.start }
198
- sleep 2
199
211
 
200
212
  workflow_execution = parent_client.start_execution
201
213
  wait_for_execution(workflow_execution)
202
214
 
203
215
  events = workflow_execution.events.map(&:event_type)
204
216
  events.should include("ChildWorkflowExecutionStarted", "ChildWorkflowExecutionCompleted", "WorkflowExecutionCompleted")
217
+ events = workflow_execution.events.select { |x| x.event_type =~ /ChildWorkflowExecutionStarted/ }
218
+ events[0].attributes.workflow_execution.task_priority.should == 100
205
219
  @forking_executor.shutdown 0
206
220
  end
207
221