aws-swf 0.1.2

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.
@@ -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