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,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