aws-swf 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,105 @@
1
+ require 'swf/task_handler'
2
+
3
+ module SWF
4
+
5
+ class MissingWorkflowStartedEvent < StandardError; end
6
+
7
+ # subclass must call .register(name, version), and define #handle(runner, task)
8
+ class DecisionTaskHandler
9
+ extend TaskHandler
10
+
11
+ @@handler_classes_by_name_version = {}
12
+
13
+ # Register statically self (subclass) to handle workflow_type with given name and version
14
+ def self.register(name, version)
15
+ @@handler_classes_by_name_version [ [name.to_s, version.to_s] ] = self
16
+ end
17
+
18
+ def self.fail!(task, args={})
19
+ task.fail_workflow_execution(args)
20
+ end
21
+
22
+ def self.find_handler_class(task)
23
+ type = task.workflow_type
24
+ @@handler_classes_by_name_version[ [type.name, type.version] ]
25
+ end
26
+
27
+ def self.configuration_help_message
28
+ "Each decision task handler running on this task list in this domain must know how to handle this workflow_type's name and version.\n" +
29
+ "I only know: #{@@handler_classes_by_name_version.inspect}"
30
+ end
31
+
32
+ attr_reader :runner, :decision_task
33
+
34
+ def initialize(runner, decision_task)
35
+ @runner = runner
36
+ @decision_task = decision_task
37
+ end
38
+
39
+ def call_handle
40
+ handle
41
+ end
42
+
43
+ def events
44
+ # make events into an array to avoid token timeout issues
45
+ # see https://forums.aws.amazon.com/thread.jspa?threadID=98925
46
+ @events ||= decision_task.events.to_a
47
+ end
48
+
49
+ def new_events
50
+ enum_for(:_new_events)
51
+ end
52
+
53
+ def _new_events(&block)
54
+ events.each {|e|
55
+ yield(e) if e.new?
56
+ }
57
+ end
58
+
59
+ # slot time of 51.2 microseconds is way too little
60
+ # even 1 second still results in collision growing
61
+ # but it doesn't seem to get to 10 so we'll leave
62
+ # it at that for now
63
+ def slot_time
64
+ #5.12e-5 # you wish
65
+ 1
66
+ end
67
+
68
+ # exponential backoff handles rate limiting exceptions
69
+ # when querying tags on a workflow execution.
70
+ def tags
71
+ runner.tag_lists[decision_task.workflow_execution] ||= begin
72
+ collision = 0
73
+ begin
74
+ decision_task.workflow_execution.tags
75
+ rescue => e
76
+ collision += 1 if collision < 10
77
+ max_slot_delay = 2**collision - 1
78
+ sleep(slot_time * rand(0 .. max_slot_delay))
79
+ retry
80
+ end
81
+ end
82
+ end
83
+
84
+
85
+ def workflow_started_event
86
+ @workflow_started_event ||= begin
87
+ events.find {|e| e.event_type == 'WorkflowExecutionStarted' } or raise MissingWorkflowStartedEvent, "Missing WorkflowExecutionStarted event in #{runner}"
88
+ end
89
+ end
90
+
91
+ def workflow_task_list
92
+ @workflow_task_list ||= workflow_started_event.attributes.task_list
93
+ end
94
+
95
+ def workflow_input
96
+ @workflow_input ||= JSON.parse(workflow_started_event.attributes.input)
97
+ end
98
+
99
+ def event_input(event)
100
+ JSON.parse(event.attributes.input)
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,52 @@
1
+ require 'swf'
2
+
3
+ module SWF
4
+ class Runner
5
+
6
+ attr_reader :domain_name, :task_list_name
7
+
8
+ def initialize(domain_name, task_list_name)
9
+ @domain_name = domain_name
10
+ @task_list_name = task_list_name
11
+ end
12
+
13
+ def be_decider
14
+ domain.decision_tasks.poll(task_list) {|decision_task|
15
+ DecisionTaskHandler.handle(self, decision_task)
16
+ }
17
+ end
18
+
19
+ def be_worker
20
+ domain.activity_tasks.poll(task_list) {|activity_task|
21
+ ActivityTaskHandler.handle(self, activity_task)
22
+ }
23
+ end
24
+
25
+ # these are static for workflow executions
26
+ # so no need to refetch per decision_task
27
+ def tag_lists
28
+ @tag_lists ||= {}
29
+ end
30
+
31
+ def domain
32
+ @domain ||= begin
33
+ SWF.domain_name = domain_name
34
+ SWF.domain
35
+ end
36
+ end
37
+
38
+ def task_list
39
+ @task_list ||= begin
40
+ SWF.task_list = task_list_name
41
+ end
42
+ end
43
+
44
+
45
+ def effect_activity_type(name, version, options={})
46
+ @activity_types ||= {}
47
+ @activity_types[[name, version]] ||= domain.activity_types.find {|t| [t.name, t.version] == [name, version] }
48
+ @activity_types[[name, version]] ||= domain.activity_types.create(name, version, options)
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+
3
+ module SWF
4
+
5
+ # use extend, not include
6
+ module TaskHandler
7
+
8
+ def handle(runner, task)
9
+ handler_class = nil
10
+ handler = nil
11
+ begin
12
+
13
+ handler_class = get_handler_class_or_fail task
14
+ return unless handler_class
15
+ handler = handler_class.new(runner, task)
16
+ handler.call_handle
17
+
18
+ rescue => e
19
+
20
+ puts "HANDLER #{self} ERROR:"
21
+ begin
22
+ details_json = JSON.pretty_unparse({
23
+ handler_class: handler_class && handler_class.name,
24
+ handler: handler.to_s,
25
+ error: e.inspect,
26
+ backtrace: e.backtrace,
27
+ })
28
+ puts details_json
29
+ fail!(task, reason: "handler raised error", details: details_json[0...32768])
30
+ rescue
31
+ msg = "FAIL to handle fail!!"
32
+ puts msg
33
+ # failing again will cause #<RuntimeError: already responded>
34
+ #fail!(task, reason: msg)
35
+ raise
36
+ end
37
+
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def get_handler_class_or_fail(task)
44
+ find_handler_class(task).tap {|handler_class|
45
+ unless handler_class
46
+ details_text = "This is a configuration issue.\n#{configuration_help_message}"
47
+ puts details_text
48
+ fail!(task, reason: "unknown type", details: details_text)
49
+ return
50
+ end
51
+ }
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,27 @@
1
+ require 'swf'
2
+
3
+ module SWF
4
+ module Workflows
5
+ extend self
6
+
7
+ def effect_workflow_type(name, version, options={})
8
+ @workflow_types ||= {}
9
+ @workflow_types[[name, version]] ||= SWF.domain.workflow_types.find {|t| [t.name, t.version] == [name, version] }
10
+ @workflow_types[[name, version]] ||= SWF.domain.workflow_types.create(name, version, options)
11
+ end
12
+
13
+ def start(options, execution_options)
14
+ execution_options[:task_list] ||= SWF.task_list
15
+ execution_options.merge!({
16
+ input: options.to_json
17
+ })
18
+ workflow_type.start_execution(execution_options)
19
+ end
20
+
21
+ def wait_for_workflow_execution_complete(workflow_execution)
22
+ sleep 1 while :open == (status = workflow_execution.status)
23
+ raise "workflow_execution #{workflow_execution} did not succeed: #{workflow_execution.status}" unless status == :completed
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ require './lib/swf/activity_task_handler.rb'
2
+
3
+ subject_class = SWF::ActivityTaskHandler
4
+ describe subject_class do
5
+
6
+ describe ' .register, .find_handler_class' do
7
+ before { mock_subclass }
8
+
9
+ let(:mock_subclass) {
10
+ Class.new(subject_class) {
11
+ register
12
+ def handle_fake_activity; end
13
+ }
14
+ }
15
+
16
+ it "registers and finds a subclass with a handle_* method matching a given activity name" do
17
+ subject_class.find_handler_class(double(:task, activity_type: double(name: 'fake_activity', version: 'fake_version'))).should == mock_subclass
18
+ end
19
+ end
20
+
21
+ let(:subject){ subject_class.new(:runner_placeholder, task) }
22
+ let(:input) { {"some" => "data"} }
23
+ let(:task){ double(:task, activity_type: double(name: 'fake_activity'), input: input.to_json) }
24
+ describe '#initialize' do
25
+ it 'sets runner & decision_task' do
26
+ subject.runner.should == :runner_placeholder
27
+ subject.activity_task.should == task
28
+ end
29
+ end
30
+
31
+ describe '#call_handle' do
32
+ it "calls #handle_* method" do
33
+ subject.should_receive(:handle_fake_activity)
34
+
35
+ subject.call_handle
36
+ end
37
+ end
38
+
39
+ describe '#activity_task_input' do
40
+ it 'JSON parses activity_task.input' do
41
+ subject.activity_task_input.should == input
42
+ end
43
+ end
44
+
45
+ describe '.fail!' do
46
+ it 'fails the activity task' do
47
+ task = double(:task).tap {|o| o.should_receive(:fail!).with(:args_placeholder) }
48
+ subject_class.fail! task, :args_placeholder
49
+ end
50
+ end
51
+
52
+ describe '.configuration_help_message' do
53
+ it { subject_class.configuration_help_message.should be_is_a String }
54
+ it { subject_class.configuration_help_message.length.should > 100 }
55
+ it { subject_class.configuration_help_message.should include 'activity' }
56
+ end
57
+
58
+ end
@@ -0,0 +1,107 @@
1
+ require './lib/swf/boot'
2
+
3
+ describe SWF::Boot do
4
+ before do
5
+ $stdout.stub(:write)
6
+
7
+ SWF::Boot.stub(:settings) {
8
+ { domain_name: 'phony_domain', task_list_name: 'phony_task_list'}
9
+ }
10
+ end
11
+
12
+ describe '.swf_runner' do
13
+ it "creates a new Runner object with the proper arguments" do
14
+ SWF::Runner.should_receive(:new).with('phony_domain', 'phony_task_list')
15
+ SWF::Boot.swf_runner
16
+ end
17
+ end
18
+
19
+ describe '.startup' do
20
+ before do
21
+ SWF::Boot.stub(:swf_runner) { double(:runner, be_worker: 'worker', be_decider: 'decider') }
22
+ end
23
+
24
+ context 'without waiting for children' do
25
+ it 'forks deciders' do
26
+ Process.should_receive(:fork).exactly(5).times do |&blk|
27
+ Process.should_receive(:daemon).with(true)
28
+ SWF::Boot.should_receive(:swf_runner).once
29
+ blk.call
30
+ end
31
+ Process.should_receive(:detach).with('decider').exactly(5).times
32
+ SWF::Boot.startup(5,0)
33
+ end
34
+ it 'forks workers' do
35
+ Process.should_receive(:fork).exactly(5).times do |&blk|
36
+ Process.should_receive(:daemon).with(true)
37
+ SWF::Boot.should_receive(:swf_runner).once
38
+ blk.call
39
+ end
40
+ Process.should_receive(:detach).with('worker').exactly(5).times
41
+ SWF::Boot.startup(0,5)
42
+ end
43
+ end
44
+
45
+ context 'with waiting for children' do
46
+ it 'forks deciders' do
47
+ Process.should_receive(:fork).exactly(5).times do |&blk|
48
+ Process.should_not_receive(:daemon)
49
+ SWF::Boot.should_receive(:swf_runner).once
50
+ blk.call
51
+ end
52
+ Process.should_receive(:wait).with('decider').exactly(5).times
53
+ SWF::Boot.startup(5,0,true)
54
+ end
55
+ it 'forks workers' do
56
+ Process.should_receive(:fork).exactly(5).times do |&blk|
57
+ Process.should_not_receive(:daemon)
58
+ SWF::Boot.should_receive(:swf_runner).once
59
+ blk.call
60
+ end
61
+ Process.should_receive(:wait).with('worker').exactly(5).times
62
+ SWF::Boot.startup(0,5,true)
63
+ end
64
+ end
65
+
66
+ context 'error handling' do
67
+ before do
68
+ SWF::Boot.stub(:swf_runner) {
69
+ double(:runner).tap {|runner|
70
+ runner.stub(:be_worker) { raise StandardError }
71
+ runner.stub(:be_decider) { raise StandardError }
72
+ }
73
+ }
74
+ end
75
+ it 'fails on deciders' do
76
+ Process.should_receive(:fork).exactly(5).times do |&blk|
77
+ Process.should_receive(:daemon)
78
+ SWF::Boot.should_receive(:swf_runner).twice
79
+ ->{blk.call}.should raise_exception(SWF::Boot::DeciderStartupFailure)
80
+ end
81
+ Process.should_receive(:detach).exactly(5).times
82
+ SWF::Boot.startup(5,0,false)
83
+ end
84
+
85
+ it 'fails on workers' do
86
+ Process.should_receive(:fork).exactly(5).times do |&blk|
87
+ Process.should_receive(:daemon)
88
+ SWF::Boot.should_receive(:swf_runner).twice
89
+ ->{blk.call}.should raise_exception(SWF::Boot::WorkerStartupFailure)
90
+ end
91
+ Process.should_receive(:detach).exactly(5).times
92
+ SWF::Boot.startup(0,5,false)
93
+
94
+ end
95
+ end
96
+ end
97
+
98
+ describe '.terminate_children' do
99
+ it 'calls Process.kill for each pid passed' do
100
+ [1,2,3].each {|pid|
101
+ Process.should_receive(:kill).with("TERM", pid).once
102
+ }
103
+ SWF::Boot.terminate_children([1,2,3])
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,141 @@
1
+ require './lib/swf/decision_task_handler.rb'
2
+
3
+ subject_class = SWF::DecisionTaskHandler
4
+ describe subject_class do
5
+
6
+ describe ' .register, .find_handler_class' do
7
+ before { mock_subclass }
8
+
9
+ let(:mock_subclass) {
10
+ Class.new(subject_class) {
11
+ register :fake_name, "fake_version"
12
+ }
13
+ }
14
+
15
+ it "registers and finds a subclass for a given workflow name/version" do
16
+ subject_class.find_handler_class(double(:task, workflow_type: double(name: 'fake_name', version: 'fake_version'))).should == mock_subclass
17
+ end
18
+ end
19
+
20
+ let(:decision_task) { double(:decision_task, events: []) }
21
+ let(:subject){ subject_class.new(:runner_placeholder, decision_task ) }
22
+
23
+ describe '#initialize' do
24
+ it 'sets runner & decision_task' do
25
+ subject.runner.should == :runner_placeholder
26
+ subject.decision_task.should == decision_task
27
+ end
28
+ end
29
+
30
+ describe '#call_handle' do
31
+ it "calls #handle" do
32
+ subject.should_receive(:handle)
33
+ subject.call_handle
34
+ end
35
+ end
36
+
37
+ describe '#events' do
38
+ it 'calls decision_task.events if no @events' do
39
+ subject.instance_variable_set(:@events, nil)
40
+ subject.decision_task.should_receive(:events)
41
+ subject.events.should == []
42
+ end
43
+ it 'otherwise just returns @events' do
44
+ subject.instance_variable_set(:@events, :foobar)
45
+ subject.decision_task.should_not_receive(:events)
46
+ subject.events.should == :foobar
47
+ end
48
+ end
49
+
50
+ let(:new_events) {[
51
+ double(:event,
52
+ event_type: 'new_event',
53
+ new?: true
54
+ ),
55
+ double(:event,
56
+ event_type: 'another_new_event',
57
+ new?: true
58
+ )
59
+ ]}
60
+
61
+ let(:old_events) {[
62
+ double(:event,
63
+ event_type: 'old_event',
64
+ new?: false
65
+ ),
66
+ double(:event,
67
+ event_type: 'another_old_event',
68
+ new?: false
69
+ )
70
+ ]}
71
+
72
+ describe '#new_events' do
73
+ before do
74
+ subject.decision_task.stub(:events) {
75
+ new_events + old_events
76
+ }
77
+ end
78
+
79
+ it 'enumerates over new events' do
80
+ subject.send(:new_events).each {|e|
81
+ new_events.include?(e).should be_true
82
+ old_events.include?(e).should be_false
83
+ }
84
+ end
85
+
86
+ end
87
+
88
+ let(:workflow_started_input){ {"foo" => "bar"} }
89
+ let(:task_list) { 'foobar' }
90
+ let(:workflow_started_event) {
91
+ double(:event,
92
+ event_type: 'WorkflowExecutionStarted',
93
+ attributes: double(:attributes,
94
+ task_list: task_list,
95
+ input: workflow_started_input.to_json
96
+ )
97
+ )
98
+ }
99
+
100
+ describe '#workflow_started_event' do
101
+ it 'raises FeatureMatrix::Workflows::MissingWorkflowStartedEvent if there is no WorkflowExecutionStartedEvent' do
102
+ ->{ subject.send(:workflow_started_event) }.should raise_exception(SWF::MissingWorkflowStartedEvent)
103
+ end
104
+ it 'otherwise returns the workflow started event' do
105
+ subject.decision_task.stub(:events){ [ workflow_started_event] }
106
+ subject.send(:workflow_started_event).should == workflow_started_event
107
+ end
108
+ end
109
+
110
+ context 'with a workflow_started_event' do
111
+ before do
112
+ subject.stub(:workflow_started_event) { workflow_started_event }
113
+ end
114
+
115
+ describe '#workflow_task_list' do
116
+ it 'returns the task list' do
117
+ subject.send(:workflow_task_list).should == workflow_started_event.attributes.task_list
118
+ end
119
+ end
120
+ describe '#workflow_input' do
121
+ it 'returns the workflow input as a hash' do
122
+ subject.send(:workflow_input).should == workflow_started_input
123
+ end
124
+ end
125
+ end
126
+
127
+
128
+ describe '.fail!' do
129
+ it 'fails the workflow execution' do
130
+ task = double(:task).tap {|o| o.should_receive(:fail_workflow_execution).with(:args_placeholder) }
131
+ subject_class.fail! task, :args_placeholder
132
+ end
133
+ end
134
+
135
+ describe '.configuration_help_message' do
136
+ it { subject_class.configuration_help_message.should be_is_a String }
137
+ it { subject_class.configuration_help_message.length.should > 100 }
138
+ it { subject_class.configuration_help_message.should include 'decision' }
139
+ end
140
+
141
+ end