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,101 @@
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
+ # Every workflow implementation needs to be a subclass of this class.
20
+ #
21
+ # Usually there should be no need to instantiate the class manually, as instead, the @execute method is called to
22
+ # start the workflow (you can think of ths as having factory class methods).
23
+ class WorkflowDefinition
24
+
25
+ attr_reader :decision_helper
26
+
27
+ attr_reader :converter
28
+
29
+ def initialize(instance, workflow_method, signals, get_state_method, converter)
30
+ @instance = instance
31
+ @workflow_method = workflow_method
32
+ @get_state_method = get_state_method
33
+ @signals = signals
34
+ @converter = converter
35
+ end
36
+
37
+ def execute(input = nil)
38
+ #TODO Set up all the converter stuff
39
+ result = Future.new
40
+ method_output = Future.new
41
+ error_handler do |t|
42
+ t.begin do
43
+ if input.nil?
44
+ method_output.set(@instance.send(@workflow_method))
45
+ else
46
+ ruby_input = @converter.load input
47
+ # Have to have *ruby_input in order to be able to handle sending
48
+ # arbitrary arguments correctly, as otherwise it will seem as if
49
+ # @workflow_method will always have an arity of 1
50
+ method_output.set(@instance.send(@workflow_method, *ruby_input))
51
+ end
52
+ end
53
+ t.rescue(Exception) do |error|
54
+ @failure = WorkflowException.new(error.message, @converter.dump(error))
55
+ #TODO error handling stuff
56
+ end
57
+ t.ensure do
58
+ raise @failure if @failure
59
+ result.set(@converter.dump method_output.get)
60
+ end
61
+ end
62
+ return result
63
+ end
64
+
65
+ def get_workflow_state
66
+ return nil if @get_state_method.nil?
67
+ converter = @get_state_method.data_converter || @converter
68
+ method = @get_state_method.method_name
69
+ begin
70
+ result = @instance.send(method)
71
+ return converter.dump(result)
72
+ rescue Exception => e
73
+ raise WorkflowException.new(e.message, converter.dump(e))
74
+ end
75
+ end
76
+
77
+
78
+
79
+ def signal_received(signal_name, input)
80
+ method_pair = @signals[signal_name]
81
+ raise "No such signal for #{signal_name}" unless method_pair
82
+ converter = method_pair.data_converter
83
+ method_name = method_pair.method_name
84
+ error_handler do |t|
85
+ t.begin do
86
+ if input.class <= NoInput
87
+ @instance.send(method_name)
88
+ else
89
+ parameters = converter.load input
90
+ @instance.send(method_name, *parameters)
91
+ end
92
+ end
93
+ t.rescue(Exception) do |e|
94
+ WorkflowException.new("Got an error while sending #{method_name} with parameters #{parameters}", converter.dump(e))
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,53 @@
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
+ class WorkflowDefinitionFactory
20
+ attr_reader :converter
21
+ def initialize(klass, workflow_type, registration_options, implementation_options, workflow_method, signals, get_state_method)
22
+ @klass = klass
23
+ @workflow_type = workflow_type
24
+ @registration_options = registration_options
25
+ @implementation_options = implementation_options
26
+ @workflow_method = workflow_method
27
+ @signals = signals
28
+ @get_state_method = get_state_method
29
+ if ! implementation_options.nil?
30
+ @converter = implementation_options.data_converter
31
+ end
32
+ @converter ||= FlowConstants.default_data_converter
33
+
34
+ end
35
+
36
+ def get_workflow_definition(decision_context)
37
+ FlowFiber.current[:decision_context] = decision_context
38
+ this_instance = @klass.new
39
+ WorkflowDefinition.new(this_instance, @workflow_method, @signals, @get_state_method, @converter)
40
+ end
41
+
42
+ def delete_workflow_definition(definition)
43
+ FlowFiber.unset(FlowFiber.current, :decision_context)
44
+ # Indicating to GC that these values are no longer needed
45
+ FlowFiber.local_variables.each_pair do |key, value|
46
+ value = nil
47
+ FlowFiber.local_variables.delete(key)
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
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
+ # This method is for internal use only and may be changed or removed
19
+ # without prior notice. Use {#workflow_client} instead.
20
+ # @!visibility private
21
+ def workflow_factory(client, domain, &options)
22
+ WorkflowFactory.new(client, domain, options)
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,1299 @@
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
+ require 'yaml'
17
+
18
+ require 'aws/decider'
19
+ include AWS::Flow
20
+
21
+
22
+ $RUBYFLOW_DECIDER_DOMAIN = "rubyflow_decider_domain_06-12-2012"
23
+ $RUBYFLOW_DECIDER_TASK_LIST = 'test_task_list'
24
+ class TrivialConverter
25
+ def dump(x)
26
+ x
27
+ end
28
+ def load(x)
29
+ x
30
+ end
31
+ end
32
+
33
+ class FakePage
34
+ def initialize(object); @object = object; end
35
+ def page; @object; end
36
+ end
37
+
38
+ class FakeWorkflowExecution
39
+ def run_id
40
+ "1"
41
+ end
42
+ end
43
+
44
+ class FakeWorkflowExecutionCollecton
45
+ def at(workflow_id, run_id); "Workflow_execution"; end
46
+ end
47
+
48
+ class FakeDomain
49
+ def initialize(workflow_type_object)
50
+ @workflow_type_object = workflow_type_object
51
+ end
52
+ def page; FakePage.new(@workflow_type_object); end
53
+ def workflow_executions; FakeWorkflowExecutionCollecton.new; end
54
+ def name; "fake_domain"; end
55
+ end
56
+
57
+
58
+ describe Activity do
59
+ let(:trace) { [] }
60
+ let(:client) { client = GenericActivityClient.new(DecisionHelper.new, ActivityOptions.new) }
61
+
62
+ it "ensures that schedule_activity gets set up from the activity_client" do
63
+ client.reconfigure(:test) { |o| o.version = "blah" }
64
+ class GenericActivityClient
65
+ alias :old_schedule_activity :schedule_activity
66
+ def schedule_activity(name, activity_type, input, options)
67
+ :scheduled_activity
68
+ end
69
+ end
70
+ trace << client.test
71
+ trace.should == [:scheduled_activity]
72
+ class GenericActivityClient
73
+ alias :schedule_activity :old_schedule_activity
74
+ end
75
+ end
76
+
77
+ describe "multiple activities" do
78
+ it "ensures that you can schedule multiple activities with the same activity call" do
79
+ class GenericActivityClient
80
+ alias :old_schedule_activity :schedule_activity
81
+ def schedule_activity(name, activity_type, input, options)
82
+ "scheduled_activity_#{name}".to_sym
83
+ end
84
+ end
85
+ client = GenericActivityClient.new(DecisionHelper.new, ActivityOptions.new)
86
+ client.reconfigure(:test, :test2) { |o| o.version = "blah" }
87
+ trace << client.test
88
+ trace << client.test2
89
+ class GenericActivityClient
90
+ alias :schedule_activity :old_schedule_activity
91
+ end
92
+ trace.should == [:"scheduled_activity_.test", :"scheduled_activity_.test2"]
93
+ end
94
+ end
95
+ describe GenericActivityClient do
96
+
97
+ before(:each) do
98
+ @options = ActivityOptions.new
99
+ @decision_helper = DecisionHelper.new
100
+ @client = GenericActivityClient.new(@decision_helper, @options)
101
+ [:test, :test_nil].each do |method_name|
102
+ @client.reconfigure(method_name) { |o| o.version = 1 }
103
+ end
104
+ end
105
+ it "ensures that activities get generated correctly" do
106
+ scope = AsyncScope.new { @client.test }
107
+ scope.eventLoop
108
+ @decision_helper.decision_map.values.first.
109
+ get_decision[:schedule_activity_task_decision_attributes][:activity_type][:name].should =~
110
+ /test/
111
+ end
112
+
113
+ it "ensures that an activity that has no arguments is scheduled with no input value" do
114
+ scope = AsyncScope.new do
115
+
116
+ @client.test_nil
117
+ end
118
+ scope.eventLoop
119
+ @decision_helper.decision_map.values.first.
120
+ get_decision[:schedule_activity_task_decision_attributes].keys.should_not include :input
121
+ end
122
+
123
+ it "ensures that activities pass multiple activities fine" do
124
+ scope = AsyncScope.new do
125
+ @client.test(1, 2, 3)
126
+ end
127
+ scope.eventLoop
128
+ input = @decision_helper.decision_map.values.first.
129
+ get_decision[:schedule_activity_task_decision_attributes][:input]
130
+ @client.data_converter.load(input).should == [1, 2, 3]
131
+ end
132
+ end
133
+ end
134
+
135
+ describe WorkflowClient do
136
+ class FakeServiceClient
137
+ attr_accessor :trace
138
+ def respond_decision_task_completed(task_completed_request)
139
+ @trace ||= []
140
+ @trace << task_completed_request
141
+ end
142
+ def start_workflow_execution(options)
143
+ @trace ||= []
144
+ @trace << options
145
+ {"runId" => "blah"}
146
+ end
147
+ def register_workflow_type(options)
148
+ end
149
+ end
150
+ class TestWorkflow
151
+ extend Decider
152
+
153
+ entry_point :entry_point
154
+ def entry_point
155
+ return "This is the entry point"
156
+ end
157
+ end
158
+ before(:each) do
159
+ workflow_type_object = double("workflow_type", :name => "TestWorkflow.entry_point", :start_execution => "" )
160
+ @client = WorkflowClient.new(FakeServiceClient.new, FakeDomain.new(workflow_type_object), TestWorkflow, StartWorkflowOptions.new)
161
+ end
162
+ it "makes sure that configure works correctly" do
163
+ @client.reconfigure(:entry_point) {{ :task_list => "This nonsense" }}
164
+ @client.entry_point
165
+
166
+ end
167
+ end
168
+
169
+ describe ActivityDefinition do
170
+ class MyActivity
171
+ extend Activity
172
+ def test_three_arguments(a, b, c)
173
+ a + b + c
174
+ end
175
+ def test_no_arguments()
176
+ :no_arguments
177
+ end
178
+ def test_one_argument(arg)
179
+ arg
180
+ end
181
+ def test_getting_context
182
+ self.activity_execution_context
183
+ end
184
+ activity :test_three_arguments, :test_no_arguments, :test_one_argument
185
+ end
186
+ it "ensures that an activity definition can handle one argument" do
187
+ activity_definition = ActivityDefinition.new(MyActivity.new, :test_one_argument, nil , nil, TrivialConverter.new)
188
+ activity_definition.execute(5, nil).should == 5
189
+ end
190
+ it "ensures that you can get the activity context " do
191
+ activity_definition = ActivityDefinition.new(MyActivity.new, :test_getting_context, nil , nil, TrivialConverter.new)
192
+ (activity_definition.execute(nil, ActivityExecutionContext.new(nil, nil, nil)).is_a? ActivityExecutionContext).should == true
193
+ end
194
+ it "ensures that the activity context gets unset after the execute" do
195
+ activity_definition = ActivityDefinition.new(MyActivity.new, :test_getting_context, nil , nil, TrivialConverter.new)
196
+ activity_definition.execute(nil, ActivityExecutionContext.new(nil, nil, nil))
197
+ begin
198
+ activity_definition.execute(nil, nil)
199
+ rescue Exception => e
200
+ e.backtrace.should include "No activity execution context"
201
+ end
202
+ end
203
+ it "ensures that an activity definition can handle multiple arguments" do
204
+ activity_definition = ActivityDefinition.new(MyActivity.new, :test_three_arguments, nil , nil, TrivialConverter.new)
205
+ activity_definition.execute([1,2,3], nil).should == 6
206
+ end
207
+ it "ensures that an activity definition can handle no arguments" do
208
+ activity_definition = ActivityDefinition.new(MyActivity.new, :test_no_arguments, nil , nil, TrivialConverter.new)
209
+ activity_definition.execute(nil, nil).should == :no_arguments
210
+ end
211
+ end
212
+
213
+ describe WorkflowDefinitionFactory do
214
+ before(:each) do
215
+ class MyWorkflow
216
+ extend Decider
217
+ version "1"
218
+ def no_arguments
219
+ :no_arguments
220
+ end
221
+ def one_argument(arg)
222
+ arg
223
+ end
224
+ def multiple_arguments(arg1, arg2, arg3)
225
+ arg3
226
+ end
227
+ end
228
+ class WorkflowDefinition
229
+ attr_accessor :decision_helper, :workflow_method, :converter
230
+ end
231
+ end
232
+ let(:fake_decision_context) { stub(:decision_helper => nil) }
233
+ let(:workflow_definition) do
234
+ FlowFiber.stub(:current) { Hash.new(Hash.new) }
235
+ WorkflowDefinitionFactory.new(MyWorkflow, nil, nil, nil, nil, nil, nil).get_workflow_definition(fake_decision_context) end
236
+ it "makes sure that workflowDefinitionFactory#get_workflow_definition returns different instances" do
237
+ FlowFiber.stub(:current) { Hash.new(Hash.new) }
238
+ workflow_factory = WorkflowDefinitionFactory.new(MyWorkflow, nil, nil, nil, nil, nil ,nil)
239
+ first_definition = workflow_factory.get_workflow_definition(fake_decision_context)
240
+ second_definition = workflow_factory.get_workflow_definition(fake_decision_context)
241
+ (first_definition.object_id == second_definition.object_id).should == false
242
+ end
243
+ describe "Testing the input/output" do
244
+ before(:each) do
245
+ workflow_definition.converter = TrivialConverter.new
246
+ end
247
+ it "ensures that a workflow definition can handle multiple arguments" do
248
+ workflow_definition.workflow_method = :multiple_arguments
249
+ AsyncScope.new do
250
+ workflow_definition.execute([1, 2, 3]).get
251
+ end.eventLoop
252
+ end
253
+ it "ensures that a workflow definition can handle no arguments" do
254
+ workflow_definition.workflow_method = :no_arguments
255
+ AsyncScope.new do
256
+ workflow_definition.execute(nil).get.should == :no_arguments
257
+ end.eventLoop
258
+ end
259
+ it "ensures that a workflow definition can handle one argument" do
260
+ workflow_definition.workflow_method = :one_argument
261
+ AsyncScope.new do
262
+ workflow_definition.execute(5).get.should == 5
263
+ end.eventLoop
264
+ end
265
+ end
266
+ end
267
+ p
268
+ describe ForkingExecutor do
269
+ it "makes sure that forking executors basic execute works" do
270
+ test_file_name = "ForkingExecutorTestFile"
271
+ begin
272
+ forking_executor = ForkingExecutor.new
273
+ File.exists?(test_file_name).should == false
274
+ forking_executor.execute do
275
+ File.new(test_file_name, 'w')
276
+ end
277
+ sleep 3
278
+ File.exists?(test_file_name).should == true
279
+ ensure
280
+
281
+ File.unlink(test_file_name)
282
+ end
283
+ end
284
+
285
+ it "ensures that you cannot execute more tasks on a shutdown executor" do
286
+ forking_executor = ForkingExecutor.new
287
+ forking_executor.execute do
288
+ end
289
+ forking_executor.execute do
290
+ end
291
+ forking_executor.shutdown(1)
292
+ expect { forking_executor.execute { "yay" } }.to raise_error
293
+ RejectedExecutionException
294
+ end
295
+
296
+ end
297
+
298
+ describe AsyncDecider do
299
+ before(:each) do
300
+ @decision_helper = DecisionHelper.new
301
+ @history_helper = double(HistoryHelper)
302
+ end
303
+
304
+ end
305
+ describe YAMLDataConverter do
306
+ let(:converter) {YAMLDataConverter.new}
307
+ describe "ensures that x == load(dump(x)) is true" do
308
+ {
309
+ Fixnum => 5,
310
+ String => "Hello World",
311
+ Hash => {:test => "good"},
312
+ Array => ["Hello", "World", 5],
313
+ Symbol => :test,
314
+ NilClass => nil
315
+ }.each_pair do |klass, exemplar|
316
+ it "tests #{klass}" do
317
+ 1.upto(10).each do |i|
318
+ converted_exemplar = exemplar
319
+ i.times {converted_exemplar = converter.dump converted_exemplar}
320
+ i.times {converted_exemplar = converter.load converted_exemplar}
321
+ converted_exemplar.should == exemplar
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+
328
+ describe WorkflowFactory do
329
+ it "ensures that you can create a workflow_client without access to the Workflow definition" do
330
+ workflow_type_object = double("workflow_type", :name => "NonExistantWorkflow.some_entry_method", :start_execution => "" )
331
+ class FakeServiceClient
332
+ attr_accessor :trace
333
+ def respond_decision_task_completed(task_completed_request)
334
+ @trace ||= []
335
+ @trace << task_completed_request
336
+ end
337
+ def start_workflow_execution(options)
338
+ @trace ||= []
339
+ @trace << options
340
+ {"runId" => "blah"}
341
+ end
342
+ def register_workflow_type(options)
343
+ end
344
+ end
345
+ domain = FakeDomain.new(workflow_type_object)
346
+ swf_client = FakeServiceClient.new
347
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
348
+ options.workflow_name = "NonExistantWorkflow"
349
+ options.execution_method = "some_entry_method"
350
+ end
351
+ # We want to make sure that we get to trying to start the execution on the
352
+ # workflow_type. The workflow_type will be nil, since we return an empty
353
+ # array in the domain.
354
+ my_workflow_factory.get_client.start_execution
355
+ end
356
+ end
357
+
358
+ describe "FakeHistory" do
359
+ before(:all) do
360
+ class WorkflowClock
361
+ alias_method :old_current_time, :current_time
362
+ def current_time
363
+ Time.now
364
+ end
365
+ end
366
+
367
+ class SynchronousWorkflowWorker < WorkflowWorker
368
+ def start
369
+ poller = SynchronousWorkflowTaskPoller.new(@service, nil, DecisionTaskHandler.new(@workflow_definition_map), @task_list)
370
+ poller.poll_and_process_single_task
371
+ end
372
+ end
373
+
374
+ class FakeServiceClient
375
+ attr_accessor :trace
376
+ def respond_decision_task_completed(task_completed_request)
377
+ @trace ||= []
378
+ @trace << task_completed_request
379
+ end
380
+ def start_workflow_execution(options)
381
+ {"runId" => "blah"}
382
+ end
383
+ end
384
+
385
+ class Hash
386
+ def to_h; self; end
387
+ end
388
+
389
+ class TestHistoryEvent < AWS::SimpleWorkflow::HistoryEvent
390
+ def initialize(event_type, event_id, attributes)
391
+ @event_type = event_type
392
+ @attributes = attributes
393
+ @event_id = event_id
394
+ @created_at = Time.now
395
+ end
396
+ end
397
+
398
+ class FakeWorkflowType < WorkflowType
399
+ attr_accessor :domain, :name, :version
400
+ def initialize(domain, name, version)
401
+ @domain = domain
402
+ @name = name
403
+ @version = version
404
+ end
405
+ end
406
+
407
+ class TestHistoryWrapper
408
+ def initialize(workflow_type, events)
409
+ @workflow_type = workflow_type
410
+ @events = events
411
+ end
412
+ def workflow_execution
413
+ FakeWorkflowExecution.new
414
+ end
415
+ def task_token
416
+ "1"
417
+ end
418
+ def previous_started_event_id
419
+ 1
420
+ end
421
+ attr_reader :events, :workflow_type
422
+ end
423
+ end
424
+ after(:all) do
425
+ class WorkflowClock
426
+ alias_method :current_time, :old_current_time
427
+ end
428
+
429
+ end
430
+
431
+
432
+ it "reproduces a bug found by a customer" do
433
+ class BadWorkflow
434
+ class << self
435
+ attr_accessor :task_list
436
+ end
437
+ extend Decider
438
+
439
+ version "1"
440
+ entry_point :entry_point
441
+ def entry_point
442
+ # pass
443
+ end
444
+ end
445
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
446
+ domain = FakeDomain.new(workflow_type_object)
447
+
448
+
449
+ swf_client = FakeServiceClient.new
450
+ task_list = "BadWorkflow_tasklist"
451
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
452
+ worker.add_workflow_implementation(BadWorkflow)
453
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
454
+ options.workflow_name = "BadWorkflow"
455
+ options.execution_start_to_close_timeout = 3600
456
+ options.task_list = task_list
457
+ options.task_start_to_close_timeout = 10
458
+ options.child_policy = :request_cancel
459
+ end
460
+ my_workflow = my_workflow_factory.get_client
461
+ workflow_execution = my_workflow.start_execution(5)
462
+
463
+
464
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
465
+ def get_decision_tasks
466
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
467
+ TestHistoryWrapper.new(fake_workflow_type,
468
+ [TestHistoryEvent.new("WorkflowExecutionStarted", 1, {:parent_initiated_event_id=>0, :child_policy=>:request_cancel, :execution_start_to_close_timeout=>3600, :task_start_to_close_timeout=>5, :workflow_type=> fake_workflow_type, :task_list=>"BadWorkflow"}),
469
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {:parent_initiated_event_id=>0, :child_policy=>:request_cancel, :execution_start_to_close_timeout=>3600, :task_start_to_close_timeout=>5, :workflow_type=> fake_workflow_type, :task_list=>"BadWorkflow"}),
470
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {:scheduled_event_id=>2, :identity=>"some_identity"}),
471
+ TestHistoryEvent.new("DecisionTaskTimedOut", 4, {:scheduled_event_id=>2, :timeout_type=>"START_TO_CLOSE", :started_event_id=>3})
472
+ ])
473
+
474
+ end
475
+ end
476
+ worker.start
477
+ # @forking_executor.execute { activity_worker.start }
478
+
479
+ # debugger
480
+ # worker.start
481
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
482
+ "CompleteWorkflowExecution"
483
+ end
484
+
485
+ it "reproduces the ActivityTaskTimedOut problem" do
486
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
487
+ def get_decision_tasks
488
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
489
+ TestHistoryWrapper.new(fake_workflow_type,
490
+ [
491
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
492
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
493
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
494
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {}),
495
+ TestHistoryEvent.new("ActivityTaskScheduled", 5, {:activity_id => "Activity1"}),
496
+ TestHistoryEvent.new("ActivityTaskStarted", 6, {}),
497
+ TestHistoryEvent.new("ActivityTaskTimedOut", 7, {:scheduled_event_id => 5, :timeout_type => "START_TO_CLOSE"}),
498
+ ])
499
+ end
500
+ end
501
+ class BadWorkflow
502
+ class << self
503
+ attr_accessor :task_list
504
+ end
505
+ extend Decider
506
+ version "1"
507
+ entry_point :entry_point
508
+ activity_client :activity do |options|
509
+ options.prefix_name = "BadActivity"
510
+ options.version = "1"
511
+ options.default_task_heartbeat_timeout = "3600"
512
+ options.default_task_list = "BadWorkflow"
513
+ options.default_task_schedule_to_close_timeout = "30"
514
+ options.default_task_schedule_to_start_timeout = "30"
515
+ options.default_task_start_to_close_timeout = "10"
516
+ end
517
+ def entry_point
518
+ activity.run_activity1
519
+ activity.run_activity2
520
+ end
521
+ end
522
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
523
+ domain = FakeDomain.new(workflow_type_object)
524
+
525
+
526
+ swf_client = FakeServiceClient.new
527
+ task_list = "BadWorkflow_tasklist"
528
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
529
+ worker.add_workflow_implementation(BadWorkflow)
530
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
531
+ options.workflow_name = "BadWorkflow"
532
+ options.execution_start_to_close_timeout = 3600
533
+ options.task_list = task_list
534
+ options.task_start_to_close_timeout = 10
535
+ options.child_policy = :request_cancel
536
+ end
537
+
538
+ my_workflow = my_workflow_factory.get_client
539
+ workflow_execution = my_workflow.start_execution(5)
540
+ worker.start
541
+
542
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
543
+ "FailWorkflowExecution"
544
+ swf_client.trace.first[:decisions].first[:fail_workflow_execution_decision_attributes][:details].should =~
545
+ /AWS::Flow::ActivityTaskTimedOutException/
546
+ end
547
+
548
+ it "makes sure that exponential retry can take arguments" do
549
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
550
+ def get_decision_tasks
551
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
552
+ TestHistoryWrapper.new(fake_workflow_type,
553
+ [
554
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
555
+ ])
556
+ end
557
+ end
558
+ class BadWorkflow
559
+ class << self
560
+ attr_accessor :task_list
561
+ end
562
+ extend Decider
563
+ version "1"
564
+ entry_point :entry_point
565
+
566
+ activity_client :activity do |options|
567
+ options.prefix_name = "BadActivity"
568
+ options.version = "1"
569
+ options.default_task_heartbeat_timeout = "3600"
570
+ options.default_task_list = "BadWorkflow"
571
+ options.default_task_schedule_to_close_timeout = "30"
572
+ options.default_task_schedule_to_start_timeout = "30"
573
+ options.default_task_start_to_close_timeout = "10"
574
+ end
575
+ def entry_point
576
+ activity.exponential_retry(:run_activity1, 5) do |o|
577
+ o.maximum_attempts = 3
578
+ end
579
+ end
580
+ end
581
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
582
+ domain = FakeDomain.new(workflow_type_object)
583
+
584
+ swf_client = FakeServiceClient.new
585
+ task_list = "BadWorkflow_tasklist"
586
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
587
+ worker.add_workflow_implementation(BadWorkflow)
588
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
589
+ options.workflow_name = "BadWorkflow"
590
+ options.execution_start_to_close_timeout = 3600
591
+ options.task_list = task_list
592
+ options.task_start_to_close_timeout = 10
593
+ options.child_policy = :request_cancel
594
+ end
595
+ my_workflow = my_workflow_factory.get_client
596
+ workflow_execution = my_workflow.start_execution
597
+ worker.start
598
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
599
+ "ScheduleActivityTask"
600
+ end
601
+
602
+ it "makes sure that overriding works correctly" do
603
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
604
+ def get_decision_tasks
605
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
606
+ TestHistoryWrapper.new(fake_workflow_type,
607
+ [
608
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
609
+ ])
610
+ end
611
+ end
612
+ class BadWorkflow
613
+ class << self
614
+ attr_accessor :task_list
615
+ end
616
+ extend Decider
617
+ version "1"
618
+ entry_point :entry_point
619
+ activity_client :activity do |options|
620
+ options.prefix_name = "BadActivity"
621
+ options.version = "1"
622
+ options.default_task_heartbeat_timeout = "3600"
623
+ options.default_task_list = "BadWorkflow"
624
+ options.default_task_schedule_to_close_timeout = "30"
625
+ options.default_task_schedule_to_start_timeout = "30"
626
+ options.default_task_start_to_close_timeout = "10"
627
+ end
628
+ def entry_point
629
+ activity.exponential_retry(:run_activity1, 5) do |o|
630
+ o.maximum_attempts = 3
631
+ end
632
+ end
633
+ end
634
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
635
+ domain = FakeDomain.new(workflow_type_object)
636
+
637
+ swf_client = FakeServiceClient.new
638
+ task_list = "BadWorkflow_tasklist"
639
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
640
+ worker.add_workflow_implementation(BadWorkflow)
641
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
642
+ options.workflow_name = "BadWorkflow"
643
+ options.execution_start_to_close_timeout = 3600
644
+ options.task_list = task_list
645
+ options.task_start_to_close_timeout = 10
646
+ options.child_policy = :request_cancel
647
+ end
648
+ my_workflow = my_workflow_factory.get_client
649
+ workflow_execution = my_workflow.start_execution
650
+ worker.start
651
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
652
+ "ScheduleActivityTask"
653
+ end
654
+
655
+ it "makes sure that exponential_retry blocks correctly" do
656
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
657
+ def get_decision_tasks
658
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
659
+ TestHistoryWrapper.new(fake_workflow_type,
660
+ [
661
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
662
+ ])
663
+ end
664
+ end
665
+ class BadWorkflow
666
+ class << self
667
+ attr_accessor :task_list, :trace
668
+ end
669
+ @trace = []
670
+ extend Decider
671
+ version "1"
672
+ entry_point :entry_point
673
+ activity_client :activity do |options|
674
+ options.prefix_name = "BadActivity"
675
+ options.version = "1"
676
+ options.default_task_heartbeat_timeout = "3600"
677
+ options.default_task_list = "BadWorkflow"
678
+ options.default_task_schedule_to_close_timeout = "30"
679
+ options.default_task_schedule_to_start_timeout = "30"
680
+ options.default_task_start_to_close_timeout = "10"
681
+ end
682
+ def entry_point
683
+ BadWorkflow.trace << :start
684
+ activity.exponential_retry(:run_activity1, 5) do |o|
685
+ o.maximum_attempts = 3
686
+ end
687
+ BadWorkflow.trace << :middle
688
+ activity.exponential_retry(:run_activity2, 5) do |o|
689
+ o.maximum_attempts = 3
690
+ end
691
+ activity.run_activity1
692
+ BadWorkflow.trace << :end
693
+ end
694
+ end
695
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
696
+ domain = FakeDomain.new(workflow_type_object)
697
+
698
+ swf_client = FakeServiceClient.new
699
+ task_list = "BadWorkflow_tasklist"
700
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
701
+ worker.add_workflow_implementation(BadWorkflow)
702
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
703
+ options.workflow_name = "BadWorkflow"
704
+ options.execution_start_to_close_timeout = 3600
705
+ options.task_list = task_list
706
+ options.task_start_to_close_timeout = 10
707
+ options.child_policy = :request_cancel
708
+ end
709
+ my_workflow = my_workflow_factory.get_client
710
+ workflow_execution = my_workflow.start_execution
711
+ worker.start
712
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
713
+ "ScheduleActivityTask"
714
+ BadWorkflow.trace.should == [:start]
715
+ end
716
+
717
+ it "makes sure that exponential_retry blocks correctly when done through configure" do
718
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
719
+ def get_decision_tasks
720
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
721
+ TestHistoryWrapper.new(fake_workflow_type,
722
+ [
723
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
724
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
725
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
726
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {}),
727
+ TestHistoryEvent.new("ActivityTaskScheduled", 5, {:activity_id => "Activity1"}),
728
+ TestHistoryEvent.new("ActivityTaskStarted", 6, {}),
729
+ TestHistoryEvent.new("ActivityTaskTimedOut", 7, {:scheduled_event_id => 5, :timeout_type => "START_TO_CLOSE"}),
730
+ ])
731
+ end
732
+ end
733
+ class BadWorkflow
734
+ class << self
735
+ attr_accessor :task_list, :trace
736
+ end
737
+ @trace = []
738
+ extend Decider
739
+ version "1"
740
+ entry_point :entry_point
741
+ activity_client :activity do |options|
742
+ options.prefix_name = "BadActivity"
743
+ options.version = "1"
744
+ options.default_task_heartbeat_timeout = "3600"
745
+ options.default_task_list = "BadWorkflow"
746
+ options.default_task_schedule_to_close_timeout = "90"
747
+ options.default_task_schedule_to_start_timeout = "90"
748
+ options.default_task_start_to_close_timeout = "90"
749
+ end
750
+ def entry_point
751
+ BadWorkflow.trace << :start
752
+
753
+ activity.reconfigure(:run_activity1) do |o|
754
+ o.exponential_retry do |retry_options|
755
+ retry_options.maximum_attempts = 3
756
+ end
757
+ end
758
+ activity.run_activity1
759
+ BadWorkflow.trace << :middle
760
+ activity.run_activity1
761
+ end
762
+ end
763
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
764
+ domain = FakeDomain.new(workflow_type_object)
765
+
766
+ swf_client = FakeServiceClient.new
767
+ task_list = "BadWorkflow_tasklist"
768
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
769
+ worker.add_workflow_implementation(BadWorkflow)
770
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
771
+ options.workflow_name = "BadWorkflow"
772
+ options.execution_start_to_close_timeout = 3600
773
+ options.task_list = task_list
774
+ options.task_start_to_close_timeout = 30
775
+ options.child_policy = :request_cancel
776
+ end
777
+ my_workflow = my_workflow_factory.get_client
778
+ workflow_execution = my_workflow.start_execution
779
+ worker.start
780
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
781
+ "StartTimer"
782
+ BadWorkflow.trace.should == [:start]
783
+ end
784
+
785
+ it "makes sure that exponential_retry blocks correctly when done through the activity_client" do
786
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
787
+ def get_decision_tasks
788
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
789
+ TestHistoryWrapper.new(fake_workflow_type,
790
+ [
791
+
792
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {:created_at => Time.now}),
793
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {:created_at => Time.now}),
794
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {:created_at => Time.now}),
795
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {:created_at => Time.now}),
796
+ TestHistoryEvent.new("ActivityTaskScheduled", 5, {:activity_id => "Activity1", :created_at => Time.now}),
797
+ TestHistoryEvent.new("ActivityTaskStarted", 6, {:created_at => Time.now}),
798
+ TestHistoryEvent.new("ActivityTaskTimedOut", 7, {:scheduled_event_id => 5, :timeout_type => "START_TO_CLOSE", :created_at => Time.now}),
799
+ ])
800
+ end
801
+ end
802
+ class BadWorkflow
803
+ class << self
804
+ attr_accessor :task_list, :trace
805
+ end
806
+ @trace = []
807
+ extend Decider
808
+ version "1"
809
+ entry_point :entry_point
810
+ activity_client :activity do |options|
811
+ options.prefix_name = "BadActivity"
812
+ options.version = "1"
813
+ options.default_task_heartbeat_timeout = "3600"
814
+ options.default_task_list = "BadWorkflow"
815
+ options.default_task_schedule_to_close_timeout = "30"
816
+ options.default_task_schedule_to_start_timeout = "30"
817
+ options.default_task_start_to_close_timeout = "30"
818
+ options.exponential_retry do |retry_options|
819
+ retry_options.maximum_attempts = 3
820
+ end
821
+ end
822
+ def entry_point
823
+ BadWorkflow.trace << :start
824
+ activity.run_activity1
825
+ BadWorkflow.trace << :middle
826
+ activity.run_activity1
827
+
828
+ end
829
+ end
830
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
831
+ domain = FakeDomain.new(workflow_type_object)
832
+
833
+ swf_client = FakeServiceClient.new
834
+ task_list = "BadWorkflow_tasklist"
835
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
836
+ worker.add_workflow_implementation(BadWorkflow)
837
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
838
+ options.workflow_name = "BadWorkflow"
839
+ options.execution_start_to_close_timeout = 3600
840
+ options.task_list = task_list
841
+ options.task_start_to_close_timeout = 30
842
+ options.child_policy = :request_cancel
843
+ end
844
+ my_workflow = my_workflow_factory.get_client
845
+ workflow_execution = my_workflow.start_execution
846
+ worker.start
847
+
848
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
849
+ "StartTimer"
850
+ BadWorkflow.trace.should == [:start]
851
+ end
852
+ it "makes sure that multiple schedules followed by a timeout work" do
853
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
854
+ def get_decision_tasks
855
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
856
+ TestHistoryWrapper.new(fake_workflow_type,
857
+ [
858
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
859
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
860
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
861
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {}),
862
+ TestHistoryEvent.new("ActivityTaskScheduled", 5, {:activity_id => "Activity1"}),
863
+ TestHistoryEvent.new("ActivityTaskScheduled", 6, {:activity_id => "Activity2"}),
864
+ TestHistoryEvent.new("ActivityTaskScheduled", 7, {:activity_id => "Activity3"}),
865
+ TestHistoryEvent.new("ActivityTaskScheduled", 8, {:activity_id => "Activity4"}),
866
+ TestHistoryEvent.new("ActivityTaskTimedOut", 9, {:scheduled_event_id => 5, :timeout_type => "START_TO_CLOSE"}),
867
+ TestHistoryEvent.new("ActivityTaskTimedOut", 10, {:scheduled_event_id => 6, :timeout_type => "START_TO_CLOSE"}),
868
+ TestHistoryEvent.new("ActivityTaskTimedOut", 11, {:scheduled_event_id => 7, :timeout_type => "START_TO_CLOSE"}),
869
+ TestHistoryEvent.new("ActivityTaskTimedOut", 12, {:scheduled_event_id => 8, :timeout_type => "START_TO_CLOSE"}),
870
+ TestHistoryEvent.new("DecisionTaskScheduled", 13, {}),
871
+ TestHistoryEvent.new("DecisionTaskStarted", 14, {}),
872
+
873
+ ])
874
+ end
875
+ end
876
+ class BadWorkflow
877
+ class << self
878
+ attr_accessor :task_list, :trace
879
+ end
880
+ @trace = []
881
+ extend Decider
882
+ version "1"
883
+ entry_point :entry_point
884
+ activity_client :activity do |options|
885
+ options.prefix_name = "BadActivity"
886
+ options.version = "1"
887
+ options.default_task_heartbeat_timeout = "3600"
888
+ options.default_task_list = "BadWorkflow"
889
+ options.default_task_schedule_to_close_timeout = "30"
890
+ options.default_task_schedule_to_start_timeout = "30"
891
+ options.default_task_start_to_close_timeout = "30"
892
+ options.exponential_retry do |retry_options|
893
+ retry_options.maximum_attempts = 3
894
+ end
895
+ end
896
+ def entry_point
897
+ BadWorkflow.trace << :start
898
+ [:run_activity1, :run_activity2, :run_activity3, :run_activity4].each do |act|
899
+ activity.send_async(act)
900
+ end
901
+ BadWorkflow.trace << :middle
902
+ activity.run_activity3
903
+ BadWorkflow.trace << :end
904
+ end
905
+ end
906
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
907
+ domain = FakeDomain.new(workflow_type_object)
908
+
909
+ swf_client = FakeServiceClient.new
910
+ task_list = "BadWorkflow_tasklist"
911
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
912
+ worker.add_workflow_implementation(BadWorkflow)
913
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
914
+ options.workflow_name = "BadWorkflow"
915
+ options.execution_start_to_close_timeout = 3600
916
+ options.task_list = task_list
917
+ options.task_start_to_close_timeout = 30
918
+ options.child_policy = :request_cancel
919
+ end
920
+ my_workflow = my_workflow_factory.get_client
921
+ workflow_execution = my_workflow.start_execution
922
+ worker.start
923
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
924
+ "StartTimer"
925
+ swf_client.trace.first[:decisions].length.should == 4
926
+ BadWorkflow.trace.should == [:start, :middle]
927
+ end
928
+
929
+ it "makes sure that timeout followed by success is handled correctly" do
930
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
931
+ def get_decision_tasks
932
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
933
+ TestHistoryWrapper.new(fake_workflow_type,
934
+ [
935
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
936
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
937
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
938
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {}),
939
+ TestHistoryEvent.new("ActivityTaskScheduled", 5, {:activity_id => "Activity1"}),
940
+ TestHistoryEvent.new("ActivityTaskTimedOut", 6, {:scheduled_event_id => 5, :timeout_type => "START_TO_CLOSE"}),
941
+ TestHistoryEvent.new("DecisionTaskScheduled", 7, {}),
942
+ TestHistoryEvent.new("DecisionTaskStarted", 8, {}),
943
+ TestHistoryEvent.new("DecisionTaskCompleted", 10, {}),
944
+ TestHistoryEvent.new("TimerStarted", 11, {:decision_task_completed_event_id => 10, :timer_id => "Timer1", :start_to_fire_timeout => 1}),
945
+ TestHistoryEvent.new("TimerFired", 12, {:timer_id => "Timer1", :started_event_id => 11}),
946
+ TestHistoryEvent.new("DecisionTaskScheduled", 13, {}),
947
+ TestHistoryEvent.new("DecisionTaskStarted", 14, {}),
948
+ TestHistoryEvent.new("DecisionTaskCompleted", 15, {}),
949
+ TestHistoryEvent.new("ActivityTaskScheduled", 16, {:activity_id => "Activity2"}),
950
+ TestHistoryEvent.new("ActivityTaskCompleted", 17, {:scheduled_event_id => 16 }),
951
+ TestHistoryEvent.new("DecisionTaskScheduled", 18, {}),
952
+ TestHistoryEvent.new("DecisionTaskStarted", 19, {}),
953
+
954
+ ])
955
+ end
956
+ end
957
+ class BadWorkflow
958
+ class << self
959
+ attr_accessor :task_list, :trace
960
+ end
961
+ @trace = []
962
+ extend Decider
963
+ version "1"
964
+ entry_point :entry_point
965
+ activity_client :activity do |options|
966
+ options.prefix_name = "BadActivity"
967
+ options.version = "1"
968
+ options.default_task_heartbeat_timeout = "3600"
969
+ options.default_task_list = "BadWorkflow"
970
+ options.default_task_schedule_to_close_timeout = "30"
971
+ options.default_task_schedule_to_start_timeout = "30"
972
+ options.default_task_start_to_close_timeout = "30"
973
+ options.exponential_retry do |retry_options|
974
+ retry_options.maximum_attempts = 3
975
+ end
976
+ end
977
+ def entry_point
978
+ BadWorkflow.trace << :start
979
+ activity.run_activity1
980
+ BadWorkflow.trace << :middle
981
+
982
+ BadWorkflow.trace << :end
983
+ end
984
+ end
985
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
986
+ domain = FakeDomain.new(workflow_type_object)
987
+
988
+ swf_client = FakeServiceClient.new
989
+ task_list = "BadWorkflow_tasklist"
990
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
991
+ worker.add_workflow_implementation(BadWorkflow)
992
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
993
+ options.workflow_name = "BadWorkflow"
994
+ options.execution_start_to_close_timeout = 3600
995
+ options.task_list = task_list
996
+ options.task_start_to_close_timeout = 30
997
+ options.child_policy = :request_cancel
998
+ end
999
+ my_workflow = my_workflow_factory.get_client
1000
+ workflow_execution = my_workflow.start_execution
1001
+ worker.start
1002
+
1003
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
1004
+ "CompleteWorkflowExecution"
1005
+ BadWorkflow.trace.should == [:start, :middle, :end]
1006
+ end
1007
+
1008
+ it "makes sure that signal works correctly" do
1009
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
1010
+ def get_decision_tasks
1011
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
1012
+ TestHistoryWrapper.new(fake_workflow_type,
1013
+ [
1014
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
1015
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
1016
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
1017
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {}),
1018
+ TestHistoryEvent.new("WorkflowExecutionSignaled", 5, {:signal_name => "this_signal"}),
1019
+ ])
1020
+ end
1021
+ end
1022
+ class BadWorkflow
1023
+ class << self
1024
+ attr_accessor :task_list, :trace
1025
+ end
1026
+ @trace = []
1027
+ extend Decider
1028
+ version "1"
1029
+ entry_point :entry_point
1030
+ activity_client :activity do |options|
1031
+ options.prefix_name = "BadActivity"
1032
+ options.version = "1"
1033
+ options.default_task_heartbeat_timeout = "3600"
1034
+ options.default_task_list = "BadWorkflow"
1035
+ options.default_task_schedule_to_close_timeout = "30"
1036
+ options.default_task_schedule_to_start_timeout = "30"
1037
+ options.default_task_start_to_close_timeout = "30"
1038
+ options.exponential_retry do |retry_options|
1039
+ retry_options.maximum_attempts = 3
1040
+ end
1041
+ end
1042
+ def this_signal
1043
+ @wait.broadcast
1044
+ end
1045
+ signal :this_signal
1046
+ def entry_point
1047
+ BadWorkflow.trace << :start
1048
+ @wait ||= FiberConditionVariable.new
1049
+ @wait.wait
1050
+ BadWorkflow.trace << :middle
1051
+ BadWorkflow.trace << :end
1052
+ end
1053
+ end
1054
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
1055
+ domain = FakeDomain.new(workflow_type_object)
1056
+
1057
+ swf_client = FakeServiceClient.new
1058
+ task_list = "BadWorkflow_tasklist"
1059
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
1060
+ worker.add_workflow_implementation(BadWorkflow)
1061
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
1062
+ options.workflow_name = "BadWorkflow"
1063
+ options.execution_start_to_close_timeout = 3600
1064
+ options.task_list = task_list
1065
+ end
1066
+ my_workflow = my_workflow_factory.get_client
1067
+ workflow_execution = my_workflow.start_execution
1068
+ worker.start
1069
+ swf_client.trace.first[:decisions].first[:decision_type].should ==
1070
+ "CompleteWorkflowExecution"
1071
+ BadWorkflow.trace.should == [:start, :middle, :end]
1072
+ end
1073
+
1074
+ it "makes sure that signal works correctly" do
1075
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
1076
+ def get_decision_tasks
1077
+ fake_workflow_type = FakeWorkflowType.new(nil, "BadWorkflow.entry_point", "1")
1078
+ TestHistoryWrapper.new(fake_workflow_type,
1079
+ [
1080
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
1081
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
1082
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
1083
+ ])
1084
+ end
1085
+ end
1086
+ class BadWorkflow
1087
+ class << self
1088
+ attr_accessor :task_list, :trace
1089
+ end
1090
+ @trace = []
1091
+ extend Decider
1092
+ version "1"
1093
+ entry_point :entry_point
1094
+ def entry_point
1095
+ raise "This is an expected error"
1096
+ end
1097
+ end
1098
+ workflow_type_object = double("workflow_type", :name => "BadWorkflow.entry_point", :start_execution => "" )
1099
+ domain = FakeDomain.new(workflow_type_object)
1100
+
1101
+ swf_client = FakeServiceClient.new
1102
+
1103
+ task_list = "BadWorkflow_tasklist"
1104
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list, BadWorkflow)
1105
+
1106
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
1107
+ options.workflow_name = "BadWorkflow"
1108
+ options.execution_start_to_close_timeout = 3600
1109
+ options.task_list = task_list
1110
+ end
1111
+ my_workflow = my_workflow_factory.get_client
1112
+ workflow_execution = my_workflow.start_execution
1113
+ worker.start
1114
+ swf_client.trace.first[:decisions].first[:fail_workflow_execution_decision_attributes][:details].should =~ /This is an expected error/
1115
+ end
1116
+ it "makes sure that you can do retry with the easier Fixnum semantic"do
1117
+
1118
+ class SynchronousWorkflowTaskPoller < WorkflowTaskPoller
1119
+ def get_decision_tasks
1120
+ fake_workflow_type = FakeWorkflowType.new(nil, "FixnumWorkflow.entry_point", "1")
1121
+ TestHistoryWrapper.new(fake_workflow_type,
1122
+ [
1123
+ TestHistoryEvent.new("WorkflowExecutionStarted", 1, {}),
1124
+ TestHistoryEvent.new("DecisionTaskScheduled", 2, {}),
1125
+ TestHistoryEvent.new("DecisionTaskStarted", 3, {}),
1126
+ TestHistoryEvent.new("DecisionTaskCompleted", 4, {}),
1127
+ TestHistoryEvent.new("ActivityTaskScheduled", 5, {:activity_id => "Activity1"}),
1128
+ TestHistoryEvent.new("ActivityTaskStarted", 6, {}),
1129
+ TestHistoryEvent.new("ActivityTaskTimedOut", 7, {:scheduled_event_id => 5, :timeout_type => "START_TO_CLOSE"}),
1130
+ ])
1131
+ end
1132
+ end
1133
+ workflow_type_object = double("workflow_type", :name => "FixnumWorkflow.entry_point", :start_execution => "" )
1134
+ domain = FakeDomain.new(workflow_type_object)
1135
+
1136
+ class FixnumActivity
1137
+ extend Activity
1138
+ activity :run_activity1
1139
+ def run_activity1; raise StandardError; end
1140
+ end
1141
+ class FixnumWorkflow
1142
+ extend Workflows
1143
+ workflow(:entry_point) { {:version => "1"} }
1144
+ activity_client(:activity) { {:version => "1", :prefix_name => "FixnumActivity" } }
1145
+ def entry_point
1146
+
1147
+ activity.retry(:run_activity1, 5) {{:maximum_attempts => 5, :should_jitter => false}}
1148
+ end
1149
+ end
1150
+ swf_client = FakeServiceClient.new
1151
+ task_list = "FixnumWorkflow_tasklist"
1152
+ my_workflow_factory = workflow_factory(swf_client, domain) do |options|
1153
+ options.workflow_name = "FixnumWorkflow"
1154
+ options.execution_start_to_close_timeout = 3600
1155
+ options.task_list = task_list
1156
+ end
1157
+ worker = SynchronousWorkflowWorker.new(swf_client, domain, task_list)
1158
+ worker.add_workflow_implementation(FixnumWorkflow)
1159
+ my_workflow = my_workflow_factory.get_client
1160
+ workflow_execution = my_workflow.start_execution
1161
+ worker.start
1162
+ swf_client.trace.first[:decisions].first[:start_timer_decision_attributes][:start_to_fire_timeout].should == "5"
1163
+ end
1164
+
1165
+ end
1166
+ describe "Misc tests" do
1167
+ it "makes sure that Workflows is equivalent to Decider" do
1168
+ class TestDecider
1169
+ extend Workflows
1170
+ end
1171
+ TestDecider.methods.map(&:to_sym).should include :signal
1172
+ end
1173
+ end
1174
+
1175
+ describe FlowConstants do
1176
+
1177
+ it "will test the default retry function with regular cases" do
1178
+ test_first = [Time.now, Time.now, Time.now]
1179
+ test_time_of_failure = [0, 10, 100]
1180
+ test_attempts = [{}, {Exception=>1}, {ActivityTaskTimedOutException=>5, Exception=>2}]
1181
+ test_output = [0, 1, 64]
1182
+ arr = test_first.zip(test_time_of_failure, test_attempts, test_output)
1183
+ arr.each do |first, time_of_failure, attempts, output|
1184
+ result = FlowConstants.exponential_retry_function.call(first, time_of_failure, attempts)
1185
+ (result == output).should == true
1186
+ end
1187
+ end
1188
+
1189
+ it "will test for exceptions" do
1190
+ expect { FlowConstants.exponential_retry_function.call(-1, 1, {}) }.to raise_error(ArgumentError, "first is not an instance of Time")
1191
+ expect { FlowConstants.exponential_retry_function.call(Time.now, -1, {}) }.to raise_error(ArgumentError, "time_of_failure can't be negative")
1192
+ expect { FlowConstants.exponential_retry_function.call(Time.now, 1, {Exception=>-1}) }.to raise_error(ArgumentError, "number of attempts can't be negative")
1193
+ expect { FlowConstants.exponential_retry_function.call(Time.now, 1, {Exception=>-1, ActivityTaskTimedOutException=>-10}) }.to raise_error(ArgumentError, "number of attempts can't be negative")
1194
+ expect { FlowConstants.exponential_retry_function.call(Time.now, 1, {Exception=>2, ActivityTaskTimedOutException=>-10}) }.to raise_error(ArgumentError, "number of attempts can't be negative")
1195
+ end
1196
+
1197
+ end
1198
+
1199
+
1200
+
1201
+ describe "testing changing default values in RetryOptions and RetryPolicy" do
1202
+
1203
+ it "will test exponential retry with a new retry function" do
1204
+ my_retry_func = lambda do |first, time_of_failure, attempts|
1205
+ 10
1206
+ end
1207
+ options = {
1208
+ :should_jitter => false
1209
+ }
1210
+ retry_policy = RetryPolicy.new(my_retry_func, RetryOptions.new(options))
1211
+ result = retry_policy.next_retry_delay_seconds(Time.now, 0, {Exception=>10}, Exception, 1)
1212
+ result.should == 10
1213
+ end
1214
+
1215
+ it "will test the jitter function" do
1216
+ my_retry_func = lambda do |first, time_of_failure, attempts|
1217
+ 10
1218
+ end
1219
+ options = {
1220
+ :should_jitter => true
1221
+ }
1222
+ retry_policy = RetryPolicy.new(my_retry_func, RetryOptions.new(options, true))
1223
+ result = retry_policy.next_retry_delay_seconds(Time.now, 0, {Exception=>10}, Exception, 1)
1224
+ result.should >= 10 && result.should < 15
1225
+ end
1226
+
1227
+
1228
+ it "will test whether we get the same jitter for a particular execution id" do
1229
+
1230
+ (FlowConstants.jitter_function.call(1, 100)).should equal(FlowConstants.jitter_function.call(1, 100))
1231
+
1232
+ end
1233
+
1234
+ it "will test the default exceptions included for retry" do
1235
+ options = RetryOptions.new
1236
+ options.exceptions_to_include.should include Exception
1237
+ end
1238
+
1239
+ it "will test the default exceptions included for retry" do
1240
+ my_retry_func = lambda do |first, time_of_failure, attempts|
1241
+ 10
1242
+ end
1243
+ options = {
1244
+ :exceptions_to_include => [ActivityTaskTimedOutException],
1245
+ :exceptions_to_exclude => [ActivityTaskFailedException]
1246
+ }
1247
+ retry_policy = RetryPolicy.new(my_retry_func, RetryOptions.new(options))
1248
+ result = retry_policy.isRetryable(ActivityTaskTimedOutException.new("a", "b", "c", "d"))
1249
+ result.should == true
1250
+
1251
+ result = retry_policy.isRetryable(ActivityTaskFailedException.new("a", "b", "c", RuntimeError.new))
1252
+ result.should == false
1253
+ end
1254
+
1255
+
1256
+ it "will make sure exception is raised if the called exception is there in both included and excluded exceptions" do
1257
+ my_retry_func = lambda do |first, time_of_failure, attempts|
1258
+ 10
1259
+ end
1260
+ options = {
1261
+ :exceptions_to_include => [ActivityTaskFailedException],
1262
+ :exceptions_to_exclude => [ActivityTaskFailedException]
1263
+ }
1264
+ retry_policy = RetryPolicy.new(my_retry_func, RetryOptions.new(options))
1265
+ expect {retry_policy.isRetryable(ActivityTaskFailedException.new("a", "b", "c", ActivityTaskFailedException))}.to raise_error
1266
+ end
1267
+
1268
+ it "will test max_attempts" do
1269
+ my_retry_func = lambda do |first, time_of_failure, attempts|
1270
+ 10
1271
+ end
1272
+ options = {
1273
+ :maximum_attempts => 5,
1274
+ :should_jitter => false
1275
+ }
1276
+ retry_policy = RetryPolicy.new(my_retry_func, RetryOptions.new(options))
1277
+ result = retry_policy.next_retry_delay_seconds(Time.now, 0, {Exception=>10}, Exception.new, 1)
1278
+ result.should == -1
1279
+
1280
+ result = retry_policy.next_retry_delay_seconds(Time.now, 0, {Exception=>4}, Exception.new, 1)
1281
+ result.should == 10
1282
+ end
1283
+
1284
+ it "will test retries_per_exception" do
1285
+ my_retry_func = lambda do |first, time_of_failure, attempts|
1286
+ 10
1287
+ end
1288
+ options = {
1289
+ :retries_per_exception => {Exception => 5},
1290
+ :should_jitter => false
1291
+ }
1292
+ retry_policy = RetryPolicy.new(my_retry_func, RetryOptions.new(options))
1293
+ result = retry_policy.next_retry_delay_seconds(Time.now, 0, {Exception=>10}, Exception.new, 1)
1294
+ result.should == -1
1295
+
1296
+ result = retry_policy.next_retry_delay_seconds(Time.now, 0, {Exception=>4}, Exception.new, 1)
1297
+ result.should == 10
1298
+ end
1299
+ end