simpler_workflow 0.2.7 → 0.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ test.log
data/.travis.yml CHANGED
@@ -1,9 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
3
+ - 2.0.0
4
4
  - 1.9.2
5
5
  - 1.9.3
6
- - jruby-18mode # JRuby in 1.8 mode
7
6
  - jruby-19mode # JRuby in 1.9 mode
8
- - rbx-18mode
9
7
  - rbx-19mode
@@ -54,12 +54,7 @@ module SimplerWorkflow
54
54
  autoload :Domain, 'simpler_workflow/domain'
55
55
  autoload :Workflow, 'simpler_workflow/workflow'
56
56
  autoload :Activity, 'simpler_workflow/activity'
57
+ autoload :ActivityRegistry, 'simpler_workflow/activity_registry'
57
58
  autoload :OptionsAsMethods, 'simpler_workflow/options_as_methods'
58
59
  autoload :DefaultExceptionReporter, 'simpler_workflow/default_exception_reporter'
59
60
  end
60
-
61
- class Map
62
- def Map.from_json(json)
63
- from_hash(JSON.parse(json))
64
- end
65
- end
@@ -2,30 +2,27 @@ module SimplerWorkflow
2
2
  class Activity
3
3
  include OptionsAsMethods
4
4
 
5
+ DEFAULT_OPTIONS = {
6
+ :default_task_list => name,
7
+ :default_task_start_to_close_timeout => 5 * 60,
8
+ :default_task_schedule_to_start_timeout => 5 * 60,
9
+ :default_task_schedule_to_close_timeout => 10 * 60,
10
+ :default_task_heartbeat_timeout => :none
11
+ }
12
+
5
13
  attr_reader :domain, :name, :version, :options, :next_activity
6
14
 
7
- def initialize(domain, name, version, options = {})
8
- Activity.activities[[name, version]] ||= begin
9
- default_options = {
10
- :default_task_list => name,
11
- :default_task_start_to_close_timeout => 5 * 60,
12
- :default_task_schedule_to_start_timeout => 5 * 60,
13
- :default_task_schedule_to_close_timeout => 10 * 60,
14
- :default_task_heartbeat_timeout => :none
15
- }
16
- @options = default_options.merge(options)
17
- @domain = domain
18
- @name = name
19
- @version = version
20
- @failure_policy = :fail
21
- self
22
- end
15
+ def initialize(domain, name, version)
16
+ @options = DEFAULT_OPTIONS.dup
17
+ @domain = domain
18
+ @name = name
19
+ @version = version
20
+ @failure_policy = :fail
23
21
  end
24
22
 
25
23
  def on_success(activity, version = nil)
26
24
  case activity
27
25
  when Hash
28
- activity.symbolize_keys!
29
26
  name = activity[:name].to_sym
30
27
  version = activity[:version]
31
28
  when String
@@ -33,7 +30,8 @@ module SimplerWorkflow
33
30
  when Symbol
34
31
  name = activity
35
32
  end
36
- @next_activity = { :name => name, :version => version }
33
+
34
+ @next_activity = Activity[domain, name, version]
37
35
  end
38
36
 
39
37
  def on_fail(failure_policy)
@@ -48,6 +46,10 @@ module SimplerWorkflow
48
46
  @perform_task = block
49
47
  end
50
48
 
49
+ def name
50
+ @name.to_s
51
+ end
52
+
51
53
  def perform_task(task)
52
54
  logger.info("Performing task #{name}")
53
55
  @perform_task.call(task)
@@ -61,7 +63,31 @@ module SimplerWorkflow
61
63
  end
62
64
 
63
65
  def to_activity_type
64
- domain.activity_types[name.to_s, version]
66
+ domain.activity_types[name, version]
67
+ end
68
+
69
+ def persist_attributes
70
+ activities.persist_attributes(self)
71
+ end
72
+
73
+ def simple_db_attributes
74
+ attributes = {
75
+ domain: domain.name,
76
+ name: name,
77
+ version: version,
78
+ failure_policy: failure_policy
79
+ }
80
+
81
+ if (next_activity)
82
+ attributes[:next_activity_name] = next_activity.name
83
+ attributes[:next_activity_version] = next_activity.version
84
+ end
85
+
86
+ attributes
87
+ end
88
+
89
+ def simple_db_name
90
+ "#{name}-#{version}"
65
91
  end
66
92
 
67
93
  def start_activity_loop
@@ -129,25 +155,26 @@ module SimplerWorkflow
129
155
  domain.activity_tasks.count(name).to_i
130
156
  end
131
157
 
132
- def self.[](name, version = nil)
133
- case name
134
- when String
135
- name = name.to_sym
136
- when Hash
137
- name.symbolize_keys!
138
- version = name[:version]
139
- name = name[:name]
140
- end
141
- activities[[name, version]]
158
+ def self.[](*activity_tuple)
159
+ activities[*activity_tuple]
142
160
  end
143
161
 
144
- def self.register(name, version, activity)
145
- activities[[name, version]] = activity
162
+ def self.[]=(*activity_tuple)
163
+ activities.[]=(*activity_tuple)
164
+ end
165
+
166
+ def self.register(domain, name, version, activity)
167
+ activities.register(domain, name, version, activity)
146
168
  end
147
169
 
148
170
  protected
171
+
172
+ def activities
173
+ self.class.activities
174
+ end
175
+
149
176
  def self.activities
150
- @activities ||= {}
177
+ @activities ||= ActivityRegistry.new
151
178
  end
152
179
 
153
180
  def self.swf
@@ -0,0 +1,95 @@
1
+ module SimplerWorkflow
2
+ class ActivityRegistry
3
+ def register(*activity_tuple)
4
+ domain = activity_tuple.shift
5
+ activity = activity_tuple.pop if activity_tuple.last.is_a?(Activity)
6
+ raise "Activity missing from registration" unless activity
7
+
8
+ registry_for_domain(domain)[activity_tuple] = activity
9
+ end
10
+
11
+ alias :[]= :register
12
+
13
+ def get(*activity_tuple)
14
+ domain = activity_tuple.shift
15
+
16
+ if AWS::SimpleWorkflow::ActivityType === domain
17
+ name = domain.name.to_sym
18
+ version = domain.version
19
+ domain = domain.domain
20
+ else
21
+ name = activity_tuple.first
22
+
23
+ case name
24
+ when Hash
25
+ version = name[:version]
26
+ name = name[:name].to_sym
27
+ when String, Symbol
28
+ name = name.to_sym
29
+ version = activity_tuple.last
30
+ end
31
+ end
32
+
33
+ registry_for_domain(domain)[[name, version]]
34
+ end
35
+
36
+ alias :[] :get
37
+
38
+ def persist_attributes(activity)
39
+ domain = Domain.for(activity.domain)
40
+
41
+ sdb_domain(domain).items.create(activity.simple_db_name, activity.simple_db_attributes)
42
+ end
43
+
44
+ protected
45
+ def registries
46
+ @registries ||= {}
47
+ end
48
+
49
+ def registry_for_domain(domain)
50
+ domain = Domain.for(domain)
51
+
52
+ unless sdb_domain(domain).exists?
53
+ sdb.domains.create(sdb_domain_name(domain))
54
+ end
55
+
56
+ registries[domain.name.to_sym] ||= Hash.new do |registry, (name, version)|
57
+ activity = Activity.new(domain, name, version)
58
+ attributes = sdb_attributes(domain, activity.simple_db_name)
59
+
60
+ unless attributes.empty?
61
+ activity.on_fail(attributes[:failure_policy]) if attributes.has_key?(:failure_policy)
62
+ activity.on_fail(attributes['failure_policy']) if attributes.has_key?('failure_policy')
63
+ activity.on_success(name: attributes[:next_activity_name], version: attributes[:next_activity_version]) if attributes.has_key?(:next_activity_name)
64
+ activity.on_success(name: attributes['next_activity_name'], version: attributes['next_activity_version']) if attributes.has_key?('next_activity_name')
65
+ end
66
+ registry[[name, version]] = activity
67
+ end
68
+ end
69
+
70
+ def sdb_domain_name(domain)
71
+ "swf-#{domain.name}-activities"
72
+ end
73
+
74
+ def sdb_domain(domain)
75
+ sdb.domains[sdb_domain_name(domain)]
76
+ end
77
+
78
+ def sdb_attributes(domain, sdb_name)
79
+ if item = sdb_domain(domain).items[sdb_name]
80
+ h = item.attributes.to_h
81
+ h.each { |k, v| h[k] = v.first }
82
+ else
83
+ {}
84
+ end
85
+ end
86
+
87
+ def self.sdb
88
+ @sdb ||= AWS::SimpleDB.new
89
+ end
90
+
91
+ def sdb
92
+ self.class.sdb
93
+ end
94
+ end
95
+ end
@@ -18,21 +18,33 @@ module SimplerWorkflow
18
18
  end
19
19
 
20
20
  def Domain.[](domain_name)
21
- Domain.domains(domain_name)
21
+ Domain.domains(domain_name.to_sym)
22
+ end
23
+
24
+ def Domain.for(domain)
25
+ case domain
26
+ when String, Symbol
27
+ Domain[domain]
28
+ when Domain
29
+ domain
30
+ when AWS::SimpleWorkflow::Domain
31
+ Domain[domain.name]
32
+ end
22
33
  end
23
34
 
24
35
  def register_workflow(name, version, &block)
25
36
  unless workflow = Workflow[name, version]
26
37
  workflow = Workflow.new(self, name, version)
27
- end
28
38
 
29
- workflow.instance_eval(&block) if block
39
+ workflow.instance_eval(&block) if block_given?
30
40
 
31
- begin
32
- self.domain.workflow_types.register(name.to_s, version, workflow.options)
33
- rescue ::AWS::SimpleWorkflow::Errors::TypeAlreadyExistsFault => e
34
- # Instance already registered...
41
+ begin
42
+ self.domain.workflow_types.register(name, version, workflow.options)
43
+ rescue ::AWS::SimpleWorkflow::Errors::TypeAlreadyExistsFault
44
+ # Instance already registered...
45
+ end
35
46
  end
47
+
36
48
  workflow
37
49
  end
38
50
 
@@ -54,18 +66,18 @@ module SimplerWorkflow
54
66
  domain.activity_types
55
67
  end
56
68
 
57
- def register_activity(name, version, &block)
58
- unless activity = Activity[name, version]
59
- logger.info("Registering Activity[#{name},#{version}]")
60
- activity = Activity.new(self, name, version)
61
- end
69
+ def register_activity(name, version = nil, &block)
70
+ logger.info("Registering Activity[#{name},#{version}]")
71
+ activity = activities[self, name, version]
62
72
 
63
73
  activity.instance_eval(&block) if block
64
74
 
75
+ activity.persist_attributes
76
+
65
77
  begin
66
78
  self.domain.activity_types.register(name.to_s, version, activity.options)
67
79
  rescue ::AWS::SimpleWorkflow::Errors::TypeAlreadyExistsFault
68
- # Nothing to do, should probably log something here...
80
+ SimplerWorkflow.logger.info("Activity[#{name}, #{version}] already registered with SWF.")
69
81
  end
70
82
 
71
83
  activity
@@ -1,3 +1,3 @@
1
1
  module SimplerWorkflow
2
- VERSION = "0.2.7"
2
+ VERSION = "0.3.0.beta"
3
3
  end
@@ -21,11 +21,8 @@ module SimplerWorkflow
21
21
  end
22
22
 
23
23
  def initial_activity(name, version = nil)
24
- if activity = Activity[name.to_sym, version]
25
- @initial_activity_type = activity.to_activity_type
26
- elsif activity = domain.activity_types[name.to_s, version]
27
- @initial_activity_type = activity
28
- end
24
+ activity = Activity[domain, name.to_sym, version]
25
+ @initial_activity_type = activity.to_activity_type
29
26
  end
30
27
 
31
28
  def decision_loop
@@ -47,26 +44,11 @@ module SimplerWorkflow
47
44
  SimplerWorkflow.after_fork.call
48
45
  end
49
46
 
50
-
51
47
  loop do
52
48
  begin
53
49
  logger.info("Waiting for a decision task for #{name.to_s}, #{version} listening to #{task_list}")
54
50
  domain.decision_tasks.poll_for_single_task(task_list) do |decision_task|
55
- decision_task.extend AWS::SimpleWorkflow::DecisionTaskAdditions
56
- logger.info("Received decision task")
57
- decision_task.new_events.each do |event|
58
- logger.info("Processing #{event.event_type}")
59
- case event.event_type
60
- when 'WorkflowExecutionStarted'
61
- start_execution(decision_task, event)
62
- when 'ActivityTaskCompleted'
63
- activity_completed(decision_task, event)
64
- when 'ActivityTaskFailed'
65
- activity_failed(decision_task, event)
66
- when 'ActivityTaskTimedOut'
67
- activity_timed_out(decision_task, event)
68
- end
69
- end
51
+ handle_decision_task(decision_task)
70
52
  end
71
53
  Process.exit 0 if @time_to_exit
72
54
  rescue Timeout::Error => e
@@ -77,9 +59,7 @@ module SimplerWorkflow
77
59
  end
78
60
  rescue => e
79
61
  context = {
80
- :workflow_execution => decision_task.workflow_execution,
81
- :workflow => to_workflow_type,
82
- :decision_task => decision_task
62
+ :workflow => to_workflow_type
83
63
  }
84
64
  SimplerWorkflow.exception_reporter.report(e, context)
85
65
  raise e
@@ -89,69 +69,7 @@ module SimplerWorkflow
89
69
  end
90
70
 
91
71
  def task_list
92
- @options[:default_task_list][:name].to_s
93
- end
94
-
95
- def start_execution(decision_task, event)
96
- logger.info "Starting the execution of the job."
97
- if @on_start_execution && @on_start_execution.respond_to?(:call)
98
- @on_start_execution.call(decision_task, event)
99
- else
100
- decision_task.schedule_activity_task initial_activity_type, :input => event.attributes.input
101
- end
102
- end
103
-
104
- def activity_completed(decision_task, event)
105
- if @on_activity_completed && @on_activity_completed.respond_to?(:call)
106
- @on_activity_completed.call(decision_task, event)
107
- else
108
- if event.attributes.keys.include?(:result)
109
- result = Map.from_json(event.attributes.result)
110
- next_activity = result[:next_activity]
111
- activity_type = domain.activity_types[next_activity[:name], next_activity[:version]]
112
- decision_task.schedule_activity_task activity_type, :input => scheduled_event(decision_task, event).attributes.input
113
- else
114
- logger.info("Workflow #{name}, #{version} completed")
115
- decision_task.complete_workflow_execution :result => 'success'
116
- end
117
- end
118
- end
119
-
120
- def activity_failed(decision_task, event)
121
- logger.info("Activity failed.")
122
- if @on_activity_failed && @on_activity_failed.respond_to?(:call)
123
- @on_activity_failed.call(decision_task, event)
124
- else
125
- if event.attributes.keys.include?(:details)
126
- details = Map.from_json(event.attributes.details)
127
- case details.failure_policy.to_sym
128
- when :abort, :cancel
129
- decision_task.cancel_workflow_execution
130
- when :fail
131
- decision_task.fail_workflow_execution
132
- when :retry
133
- logger.info("Retrying activity #{last_activity(decision_task, event).name} #{last_activity(decision_task, event).version}")
134
- decision_task.schedule_activity_task last_activity(decision_task, event), :input => last_input(decision_task, event)
135
- end
136
- else
137
- decision_task.cancel_workflow_execution
138
- end
139
- end
140
- end
141
-
142
- def activity_timed_out(decision_task, event)
143
- logger.info("Activity timed out.")
144
- if @on_activity_timed_out && @on_activity_timed_out.respond_to?(:call)
145
- @on_activity_timed_out.call(decision_task, event)
146
- else
147
- case event.attributes.timeoutType
148
- when 'START_TO_CLOSE', 'SCHEDULE_TO_START', 'SCHEDULE_TO_CLOSE'
149
- logger.info("Retrying activity #{last_activity(decision_task, event).name} #{last_activity(decision_task, event).version} due to timeout.")
150
- decision_task.schedule_activity_task last_activity(decision_task, event), :input => last_input(decision_task, event)
151
- when 'HEARTBEAT'
152
- decision_task.cancel_workflow_execution
153
- end
154
- end
72
+ options[:default_task_list][:name].to_s
155
73
  end
156
74
 
157
75
  def to_workflow_type
@@ -164,19 +82,19 @@ module SimplerWorkflow
164
82
  end
165
83
 
166
84
  def on_start_execution(&block)
167
- @on_start_execution = block
85
+ event_handlers['WorkflowExecutionStarted'] = WorkflowEventHandler.new(&block)
168
86
  end
169
87
 
170
88
  def on_activity_completed(&block)
171
- @on_activity_completed = block
89
+ event_handlers['ActivityTaskCompleted'] = WorkflowEventHandler.new(&block)
172
90
  end
173
91
 
174
92
  def on_activity_failed(&block)
175
- @on_activity_failed = block
93
+ event_handlers['ActivityTaskFailed'] = WorkflowEventHandler.new(&block)
176
94
  end
177
95
 
178
96
  def on_activity_timed_out(&block)
179
- @on_activity_timed_out = block
97
+ event_handlers['ActivityTaskTimedOut'] = WorkflowEventHandler.new(&block)
180
98
  end
181
99
 
182
100
  def self.[](name, version)
@@ -187,6 +105,18 @@ module SimplerWorkflow
187
105
  workflows[[name, version]] = workflow
188
106
  end
189
107
 
108
+ def scheduled_event(decision_task, event)
109
+ decision_task.scheduled_event(event)
110
+ end
111
+
112
+ def last_activity(decision_task, event)
113
+ scheduled_event(decision_task, event).attributes.activity_type
114
+ end
115
+
116
+ def last_input(decision_task, event)
117
+ scheduled_event(decision_task, event).attributes.input
118
+ end
119
+
190
120
  protected
191
121
  def self.workflows
192
122
  @workflows ||= {}
@@ -196,20 +126,121 @@ module SimplerWorkflow
196
126
  SimplerWorkflow.swf
197
127
  end
198
128
 
199
- def scheduled_event(decision_task, event)
200
- decision_task.scheduled_event(event)
129
+ def logger
130
+ SimplerWorkflow.logger
201
131
  end
202
132
 
203
- def last_activity(decision_task, event)
204
- scheduled_event(decision_task, event).attributes.activity_type
133
+ def handle_decision_task(decision_task)
134
+ decision_task.extend AWS::SimpleWorkflow::DecisionTaskAdditions
135
+ logger.info("Received decision task")
136
+ decision_task.new_events.each do |event|
137
+ logger.info("Processing #{event.event_type}")
138
+ event_handlers.fetch(event.event_type, DefaultEventHandler.new(self)).process(decision_task, event)
139
+ end
205
140
  end
206
141
 
207
- def last_input(decision_task, event)
208
- scheduled_event(decision_task, event).attributes.input
142
+ def event_handlers
143
+ @event_handlers ||= Map[
144
+ :WorkflowExecutionStarted , WorkflowExecutionStartedHandler.new(self) ,
145
+ :ActivityTaskCompleted , ActivityTaskCompletedHandler.new(self) ,
146
+ :ActivityTaskFailed , ActivityTaskFailedHandler.new(self) ,
147
+ :ActivityTaskTimedOut , ActivityTaskTimedOutHandler.new(self)
148
+ ]
209
149
  end
210
150
 
211
- def logger
212
- $logger || Rails.logger
151
+ class DefaultEventHandler
152
+ attr_accessor :workflow
153
+
154
+ def initialize(workflow)
155
+ @workflow = workflow
156
+ end
157
+
158
+ def scheduled_event(*args)
159
+ workflow.scheduled_event(*args)
160
+ end
161
+
162
+ def domain
163
+ workflow.domain
164
+ end
165
+
166
+ def last_activity(*args)
167
+ workflow.last_activity(*args)
168
+ end
169
+
170
+ def last_input(*args)
171
+ workflow.last_input(*args)
172
+ end
173
+
174
+ def initial_activity_type
175
+ workflow.initial_activity_type
176
+ end
177
+
178
+ def process(*args); end
179
+ end
180
+
181
+ class WorkflowEventHandler
182
+ attr_accessor :handler
183
+
184
+ def initialize(&block)
185
+ @handler = block
186
+ end
187
+
188
+ def process(decision_task, event)
189
+ handler.call(decision_task, event)
190
+ end
191
+ end
192
+
193
+ class ActivityTaskTimedOutHandler < DefaultEventHandler
194
+ def process(decision_task, event)
195
+ case event.attributes.timeoutType
196
+ when 'START_TO_CLOSE', 'SCHEDULE_TO_START', 'SCHEDULE_TO_CLOSE'
197
+ last_activity_type = last_activity(decision_task, event)
198
+ SimplerWorkflow.logger.info("Retrying activity #{last_activity_type.name} #{last_activity_type.version} due to timeout.")
199
+ decision_task.schedule_activity_task last_activity_type, :input => last_input(decision_task, event)
200
+ when 'HEARTBEAT'
201
+ decision_task.fail_workflow_execution
202
+ end
203
+ end
204
+ end
205
+
206
+ class ActivityTaskFailedHandler < DefaultEventHandler
207
+ def process(decision_task, event)
208
+ last_activity_type = last_activity(decision_task, event)
209
+ failed_activity = domain.activities[last_activity_type]
210
+
211
+ case failed_activity.failure_policy
212
+ when :abort, :cancel
213
+ SimplerWorkflow.logger.info("Cancelling workflow execution.")
214
+ decision_task.cancel_workflow_execution
215
+ when :retry
216
+ SimplerWorkflow.logger.info("Retrying activity #{last_activity_type.name} #{last_activity_type.version}")
217
+ decision_task.schedule_activity_task last_activity_type, :input => last_input(decision_task, event)
218
+ else
219
+ SimplerWorkflow.logger.info("Failing the workflow execution.")
220
+ decision_task.fail_workflow_execution
221
+ end
222
+ end
223
+ end
224
+
225
+ class ActivityTaskCompletedHandler < DefaultEventHandler
226
+ def process(decision_task, event)
227
+ last_activity_type = last_activity(decision_task, event)
228
+
229
+ completed_activity = domain.activities[last_activity_type]
230
+
231
+ if next_activity = completed_activity.next_activity
232
+ activity_type = domain.activity_types[next_activity.name, next_activity.version]
233
+ decision_task.schedule_activity activity_type, input: scheduled_event(decision_task, event).attributes.input
234
+ else
235
+ decision_task.complete_workflow_execution(result: 'success')
236
+ end
237
+ end
238
+ end
239
+
240
+ class WorkflowExecutionStartedHandler < DefaultEventHandler
241
+ def process(decision_task, event)
242
+ decision_task.schedule_activity_task initial_activity_type, input: event.attributes.input
243
+ end
213
244
  end
214
245
  end
215
246
  end
@@ -26,10 +26,16 @@ EOM
26
26
  gem.name = "simpler_workflow"
27
27
  gem.require_paths = ["lib"]
28
28
  gem.version = SimplerWorkflow::VERSION
29
+ gem.required_ruby_version = '>= 1.9.0'
29
30
 
30
- gem.add_dependency 'aws-sdk', '~> 1.6.0'
31
+ gem.add_dependency 'aws-sdk'
31
32
  gem.add_dependency 'map'
33
+ gem.add_development_dependency 'map'
32
34
  gem.add_development_dependency 'rake'
33
35
  gem.add_development_dependency 'rspec'
34
36
  gem.add_development_dependency 'travis-lint'
37
+ gem.add_development_dependency 'pry'
38
+ gem.add_development_dependency 'pry-nav'
39
+ gem.add_development_dependency 'logging'
40
+
35
41
  end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ module SimplerWorkflow
4
+ describe Activity do
5
+ let(:client) { AWS.config.simple_workflow_client }
6
+ let(:describe_domain_response) { client.stub_for(:describe_domain) }
7
+ let(:list_domains_response) { client.stub_for(:list_domains) }
8
+
9
+ let(:decision_task) { mock(AWS::SimpleWorkflow::DecisionTask) }
10
+
11
+ let(:domain) { SimplerWorkflow.domain('test-domain') }
12
+
13
+ let(:domain_desc) {{
14
+ 'configuration' => { 'workflowExecutionRetentionPeriodInDays' => '2' },
15
+ 'domainInfo' => {
16
+ 'name' => domain.name,
17
+ 'description' => 'desc',
18
+ 'status' => 'REGISTERED',
19
+ },
20
+ }}
21
+
22
+ let(:domains_desc) {
23
+ {
24
+ 'domainInfos' => [
25
+ domain_desc['domainInfo']
26
+ ]
27
+ }
28
+ }
29
+
30
+ let(:sdb) { AWS::SimpleDB.new }
31
+
32
+ before :each do
33
+ describe_domain_response.stub(:data).and_return(domain_desc)
34
+ client.stub(:describe_domain).and_return(describe_domain_response)
35
+ list_domains_response.stub(:data).and_return(domains_desc)
36
+ client.stub(:list_domains).and_return(list_domains_response)
37
+ end
38
+
39
+ context "Registering a new activity" do
40
+ context "default activity" do
41
+ subject(:activity) { domain.register_activity('test-activity', '1.0.0') }
42
+
43
+ its(:name) { should == 'test-activity' }
44
+ its(:version) { should == '1.0.0' }
45
+ its(:domain) { should == domain }
46
+ its(:failure_policy) { should == :fail }
47
+ end
48
+
49
+ context "Setting the failure policy" do
50
+ subject(:activity) do
51
+ domain.register_activity('test-activity', '1.0.1') do
52
+ on_fail :retry
53
+ end
54
+ end
55
+
56
+ its(:failure_policy) { should == :retry }
57
+ end
58
+
59
+ context "Setting the next activity" do
60
+ subject(:activity) do
61
+ domain.register_activity('test-success', '1.0.0') do
62
+ on_success 'next-activity', '1.0.0'
63
+ end
64
+ end
65
+
66
+ its(:next_activity) { should == Activity[domain, 'next-activity', '1.0.0'] }
67
+ end
68
+
69
+ context "performing a task" do
70
+ subject(:activity) do
71
+ domain.register_activity('test-task', '1.0.0') do
72
+ perform_activity do |task|
73
+ task.complete! 'result' => "success"
74
+ end
75
+ end
76
+ end
77
+
78
+ it "should execute the task handler." do
79
+ task = mock(AWS::SimpleWorkflow::ActivityTask)
80
+ task.should_receive(:complete!).with("result" => "success")
81
+
82
+ activity.perform_task(task)
83
+ end
84
+ end
85
+
86
+ context "We should always return an activity from the registry" do
87
+ subject(:activity) { Activity[domain, 'not-a-real-activity', '1.0.0'] }
88
+
89
+ its(:name) { should == 'not-a-real-activity' }
90
+ its(:version) { should == '1.0.0' }
91
+ its(:failure_policy) { should == :fail }
92
+ end
93
+
94
+ context "We are retrieving an activity that was register in a different process" do
95
+ subject(:activity) { Activity[domain, 'registered-somewhere-else', '1.0.0'] }
96
+
97
+ it "should build the activity from the SDB data..." do
98
+ Activity.activities.should_receive(:sdb_attributes).with(domain, "registered-somewhere-else-1.0.0").and_return({
99
+ :failure_policy => 'retry',
100
+ :next_activity_name => 'yet-another-activity',
101
+ :next_activity_version => '1.0.0'
102
+ })
103
+
104
+ Activity.activities.should_receive(:sdb_attributes).with(domain, 'yet-another-activity-1.0.0').and_return({})
105
+
106
+ activity.failure_policy.should == :retry
107
+ activity.next_activity.should == Activity[domain, 'yet-another-activity', '1.0.0']
108
+ activity.next_activity.failure_policy.should == :fail
109
+ end
110
+
111
+ end
112
+
113
+ context "Just in case we get strings from amazon SDB..." do
114
+ subject(:activity) { Activity[domain, 'registered-somewhere-else', '2.0.0'] }
115
+
116
+ it "should build the activity from the SDB data... with Strings this time..." do
117
+ Activity.activities.should_receive(:sdb_attributes).with(domain, "registered-somewhere-else-2.0.0").and_return({
118
+ 'failure_policy' => 'retry',
119
+ 'next_activity_name' => 'yet-another-activity',
120
+ 'next_activity_version' => '2.0.0'
121
+ })
122
+
123
+ Activity.activities.should_receive(:sdb_attributes).with(domain, 'yet-another-activity-2.0.0').and_return({})
124
+
125
+ activity.failure_policy.should == :retry
126
+ activity.next_activity.should == Activity[domain, 'yet-another-activity', '2.0.0']
127
+ activity.next_activity.failure_policy.should == :fail
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
data/spec/spec_helper.rb CHANGED
@@ -7,6 +7,9 @@
7
7
  #
8
8
 
9
9
  require 'simpler_workflow'
10
+ require 'aws/simple_workflow'
11
+ require 'pry'
12
+ require 'logging'
10
13
 
11
14
  RSpec.configure do |config|
12
15
  config.treat_symbols_as_metadata_keys_with_true_values = true
@@ -17,8 +20,12 @@ RSpec.configure do |config|
17
20
  AWS.stub!
18
21
  AWS.config(:access_key_id => 'TESTKEY', :secret_access_key => 'TESTSECRET')
19
22
  end
20
- end
21
23
 
22
- def stub_list_domains
24
+ $logger = Logging.logger("test.log")
25
+ end
23
26
 
27
+ class SimplerWorkflow::Workflow::WorkflowEventHandler
28
+ def workflow_event_handler?
29
+ is_a?(SimplerWorkflow::Workflow::WorkflowEventHandler)
30
+ end
24
31
  end
@@ -0,0 +1,245 @@
1
+ require 'spec_helper'
2
+
3
+ module SimplerWorkflow
4
+ describe Workflow do
5
+ let(:client) { AWS.config.simple_workflow_client }
6
+ let(:describe_domain_response) { client.stub_for(:describe_domain) }
7
+ let(:list_domains_response) { client.stub_for(:list_domains) }
8
+
9
+ let(:decision_task) { mock(AWS::SimpleWorkflow::DecisionTask) }
10
+
11
+ let(:domain) { SimplerWorkflow.domain('test-domain') }
12
+
13
+ let(:domain_desc) {{
14
+ 'configuration' => { 'workflowExecutionRetentionPeriodInDays' => '2' },
15
+ 'domainInfo' => {
16
+ 'name' => domain.name,
17
+ 'description' => 'desc',
18
+ 'status' => 'REGISTERED',
19
+ },
20
+ }}
21
+
22
+ let(:domains_desc) {
23
+ {
24
+ 'domainInfos' => [
25
+ domain_desc['domainInfo']
26
+ ]
27
+ }
28
+ }
29
+
30
+ before :each do
31
+ describe_domain_response.stub(:data).and_return(domain_desc)
32
+ client.stub(:describe_domain).and_return(describe_domain_response)
33
+ list_domains_response.stub(:data).and_return(domains_desc)
34
+ client.stub(:list_domains).and_return(list_domains_response)
35
+ end
36
+
37
+ context "Registering a new workflow." do
38
+ before :each do
39
+ Workflow.send :public, :event_handlers
40
+ end
41
+
42
+ context "default workflows" do
43
+ let(:workflow) { domain.register_workflow('test-workflow', '1.0.0') }
44
+
45
+ let(:event_handlers) { workflow.event_handlers }
46
+
47
+ it "should allow the registration of a domain." do
48
+ workflow.name.should == 'test-workflow'
49
+ workflow.version.should == '1.0.0'
50
+ end
51
+
52
+ it "should have a default tasklist" do
53
+ workflow.task_list.should == workflow.name
54
+ end
55
+
56
+ it "should have a default task start to close timeout" do
57
+ workflow.options[:default_task_start_to_close_timeout].should == "120"
58
+ end
59
+
60
+ it "should have a default execution start to close timeout" do
61
+ workflow.options[:default_execution_start_to_close_timeout].should == "120"
62
+ end
63
+
64
+ it "should have a default child policy of terminate" do
65
+ workflow.options[:default_child_policy].should == 'TERMINATE'
66
+ end
67
+
68
+ it 'should have default handlers' do
69
+ event_handlers.should_not be_nil
70
+ end
71
+
72
+ %w(WorkflowExecutionStarted ActivityTaskCompleted ActivityTaskFailed ActivityTaskTimedOut).each do |event|
73
+ it "should have a default event handler for #{event}" do
74
+ handler = event_handlers[event]
75
+ handler.should_not be_nil
76
+ handler.class.name.should == "SimplerWorkflow::Workflow::#{event}Handler"
77
+ end
78
+
79
+ it "should call the right handler for #{event}" do
80
+ new_event = Map.new
81
+ new_event.set(:event_type, event)
82
+
83
+ decision_task.should_receive(:new_events).and_return([new_event])
84
+
85
+ event_handlers[new_event.event_type].should_receive(:process).with(decision_task, new_event)
86
+
87
+ workflow.send :handle_decision_task, decision_task
88
+ end
89
+ end
90
+
91
+ context "The workflow's initial activity" do
92
+ before :each do
93
+ workflow.initial_activity :test_activity, '1.0.0'
94
+ end
95
+
96
+
97
+ it "should store the initial activity" do
98
+ workflow.send(:initial_activity_type).should == domain.activity_types[:test_activity, '1.0.0']
99
+ end
100
+
101
+ it "should start a workflow based on the declared initial activity" do
102
+ event = stub( :attributes => stub( :input => "Mary had a little lamb"))
103
+ decision_task.should_receive(:schedule_activity_task).with(domain.activity_types[:test_activity, '1.0.0'], input: event.attributes.input)
104
+
105
+ event_handlers[:WorkflowExecutionStarted].process(decision_task, event)
106
+ end
107
+ end
108
+
109
+ context "An activity completed." do
110
+ it "should complete the execution if we have results but to not provide a next activity" do
111
+ event = Map.new
112
+ event.set(:attributes, :result, '{"blah":"Hello"}')
113
+
114
+ scheduled_activity = domain.register_activity(:completion_activity, '1.0.0')
115
+
116
+ scheduled_event = Map.new
117
+ scheduled_event.set(:attributes, :input, "mary had a little lamb")
118
+ scheduled_event.set(:attributes, :activity_type, scheduled_activity.to_activity_type)
119
+
120
+ decision_task.should_receive(:scheduled_event).with(event).and_return(scheduled_event)
121
+ decision_task.should_receive(:complete_workflow_execution).with(result: 'success')
122
+
123
+ event_handlers[:ActivityTaskCompleted].process(decision_task, event)
124
+ end
125
+
126
+ it "should schedule the next activity if the current one declares one" do
127
+ event = Map.new
128
+ event.set(:attributes, :result, "success")
129
+
130
+ test_activity = domain.register_activity(:test_activity, '1.0.0')
131
+
132
+ scheduled_activity = domain.register_activity(:success_activity, '1.0.0') do
133
+ on_success :test_activity, '1.0.0'
134
+ end
135
+
136
+ next_activity = test_activity.to_activity_type
137
+
138
+ scheduled_event = Map.new
139
+ scheduled_event.set(:attributes, :input, "mary had a little lamb")
140
+ scheduled_event.set(:attributes, :activity_type, scheduled_activity.to_activity_type)
141
+
142
+ decision_task.should_receive(:scheduled_event).with(event).twice.and_return(scheduled_event)
143
+ decision_task.should_receive(:schedule_activity).with(next_activity, input: scheduled_event.attributes.input)
144
+
145
+ event_handlers[:ActivityTaskCompleted].process(decision_task, event)
146
+ end
147
+ end
148
+
149
+ context "An activity task failed" do
150
+ it "should fail the execution if instructed to do so" do
151
+ event = Map.new
152
+
153
+ test_activity = domain.register_activity(:failed_activity, '1.0.0')
154
+
155
+ scheduled_event = Map.new
156
+ scheduled_event.set(:attributes, :input, "Mary had a little lamb")
157
+ scheduled_event.set(:attributes, :activity_type, test_activity.to_activity_type)
158
+
159
+ decision_task.should_receive(:fail_workflow_execution)
160
+ decision_task.should_receive(:scheduled_event).with(event).and_return(scheduled_event)
161
+
162
+ event_handlers[:ActivityTaskFailed].process(decision_task, event)
163
+ end
164
+
165
+ it "should cancel the execution if instructed to abort" do
166
+ event = Map.new
167
+ test_activity = domain.register_activity(:failed_activity, '1.0.1') do
168
+ on_fail :abort
169
+ end
170
+
171
+ scheduled_event = Map.new
172
+ scheduled_event.set(:attributes, :input, "Mary had a little lamb")
173
+ scheduled_event.set(:attributes, :activity_type, test_activity.to_activity_type)
174
+
175
+ decision_task.should_receive(:scheduled_event).with(event).and_return(scheduled_event)
176
+ decision_task.should_receive(:cancel_workflow_execution)
177
+
178
+ event_handlers[:ActivityTaskFailed].process(decision_task, event)
179
+ end
180
+
181
+ it "should cancel the execution if instructed to do so" do
182
+ event = Map.new
183
+
184
+ test_activity = domain.register_activity(:failed_activity, '1.0.2') do
185
+ on_fail :cancel
186
+ end
187
+
188
+ scheduled_event = Map.new
189
+ scheduled_event.set(:attributes, :input, "Mary had a little lamb")
190
+ scheduled_event.set(:attributes, :activity_type, test_activity.to_activity_type)
191
+
192
+ decision_task.should_receive(:scheduled_event).with(event).and_return(scheduled_event)
193
+ decision_task.should_receive(:cancel_workflow_execution)
194
+
195
+ event_handlers[:ActivityTaskFailed].process(decision_task, event)
196
+ end
197
+
198
+ it "should reschedule the activity if requested" do
199
+ event = Map.new
200
+
201
+ test_activity = domain.register_activity(:failed_activity, '1.0.3') do
202
+ on_fail :retry
203
+ end
204
+
205
+ scheduled_event = Map.new
206
+ scheduled_event.set(:attributes, :input, "Mary had a little lamb")
207
+ scheduled_event.set(:attributes, :activity_type, test_activity.to_activity_type)
208
+
209
+ decision_task.should_receive(:scheduled_event).with(event).twice.and_return(scheduled_event)
210
+ decision_task.should_receive(:schedule_activity_task).with(test_activity.to_activity_type, input: scheduled_event.attributes.input)
211
+
212
+ event_handlers[:ActivityTaskFailed].process(decision_task, event)
213
+ end
214
+
215
+ end
216
+
217
+ context "an activity timed out" do
218
+ %w(START_TO_CLOSE SCHEDULE_TO_CLOSE SCHEDULE_TO_START).each do |timeout_type|
219
+ it "should retry a timed out decision task on #{timeout_type}" do
220
+ activity_type = domain.activity_types[:test_activity, "1.0.0"]
221
+ event = Map.new
222
+ event.set(:attributes, :timeoutType, timeout_type)
223
+ scheduled_event = Map.new
224
+ scheduled_event.set(:attributes, :input, "Mary had a little lamb")
225
+ scheduled_event.set(:attributes, :activity_type, activity_type)
226
+
227
+ decision_task.should_receive(:scheduled_event).twice.and_return(scheduled_event)
228
+ decision_task.should_receive(:schedule_activity_task).with(activity_type, input: scheduled_event.attributes.input)
229
+ event_handlers[:ActivityTaskTimedOut].process(decision_task, event)
230
+ end
231
+ end
232
+
233
+ it "should fail a workflow execution when the heartbeat fails" do
234
+ event = Map.new
235
+ event.set(:attributes, :timeoutType, 'HEARTBEAT')
236
+
237
+ decision_task.should_receive(:fail_workflow_execution)
238
+
239
+ event_handlers[:ActivityTaskTimedOut].process(decision_task, event)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
metadata CHANGED
@@ -1,34 +1,41 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simpler_workflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
5
- prerelease:
4
+ prerelease: 6
5
+ version: 0.3.0.beta
6
6
  platform: ruby
7
7
  authors:
8
8
  - Frederic Jean
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-04 00:00:00.000000000 Z
12
+ date: 2013-03-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ none: false
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
15
21
  name: aws-sdk
22
+ prerelease: false
16
23
  requirement: !ruby/object:Gem::Requirement
17
24
  none: false
18
25
  requirements:
19
- - - ~>
26
+ - - ! '>='
20
27
  - !ruby/object:Gem::Version
21
- version: 1.6.0
28
+ version: '0'
22
29
  type: :runtime
23
- prerelease: false
30
+ - !ruby/object:Gem::Dependency
24
31
  version_requirements: !ruby/object:Gem::Requirement
25
32
  none: false
26
33
  requirements:
27
- - - ~>
34
+ - - ! '>='
28
35
  - !ruby/object:Gem::Version
29
- version: 1.6.0
30
- - !ruby/object:Gem::Dependency
36
+ version: '0'
31
37
  name: map
38
+ prerelease: false
32
39
  requirement: !ruby/object:Gem::Requirement
33
40
  none: false
34
41
  requirements:
@@ -36,15 +43,31 @@ dependencies:
36
43
  - !ruby/object:Gem::Version
37
44
  version: '0'
38
45
  type: :runtime
39
- prerelease: false
46
+ - !ruby/object:Gem::Dependency
40
47
  version_requirements: !ruby/object:Gem::Requirement
41
48
  none: false
42
49
  requirements:
43
50
  - - ! '>='
44
51
  - !ruby/object:Gem::Version
45
52
  version: '0'
53
+ name: map
54
+ prerelease: false
55
+ requirement: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
46
62
  - !ruby/object:Gem::Dependency
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
47
69
  name: rake
70
+ prerelease: false
48
71
  requirement: !ruby/object:Gem::Requirement
49
72
  none: false
50
73
  requirements:
@@ -52,15 +75,15 @@ dependencies:
52
75
  - !ruby/object:Gem::Version
53
76
  version: '0'
54
77
  type: :development
55
- prerelease: false
78
+ - !ruby/object:Gem::Dependency
56
79
  version_requirements: !ruby/object:Gem::Requirement
57
80
  none: false
58
81
  requirements:
59
82
  - - ! '>='
60
83
  - !ruby/object:Gem::Version
61
84
  version: '0'
62
- - !ruby/object:Gem::Dependency
63
85
  name: rspec
86
+ prerelease: false
64
87
  requirement: !ruby/object:Gem::Requirement
65
88
  none: false
66
89
  requirements:
@@ -68,15 +91,31 @@ dependencies:
68
91
  - !ruby/object:Gem::Version
69
92
  version: '0'
70
93
  type: :development
71
- prerelease: false
94
+ - !ruby/object:Gem::Dependency
72
95
  version_requirements: !ruby/object:Gem::Requirement
73
96
  none: false
74
97
  requirements:
75
98
  - - ! '>='
76
99
  - !ruby/object:Gem::Version
77
100
  version: '0'
78
- - !ruby/object:Gem::Dependency
79
101
  name: travis-lint
102
+ prerelease: false
103
+ requirement: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ type: :development
110
+ - !ruby/object:Gem::Dependency
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ name: pry
118
+ prerelease: false
80
119
  requirement: !ruby/object:Gem::Requirement
81
120
  none: false
82
121
  requirements:
@@ -84,13 +123,38 @@ dependencies:
84
123
  - !ruby/object:Gem::Version
85
124
  version: '0'
86
125
  type: :development
126
+ - !ruby/object:Gem::Dependency
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ name: pry-nav
87
134
  prerelease: false
135
+ requirement: !ruby/object:Gem::Requirement
136
+ none: false
137
+ requirements:
138
+ - - ! '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ type: :development
142
+ - !ruby/object:Gem::Dependency
88
143
  version_requirements: !ruby/object:Gem::Requirement
89
144
  none: false
90
145
  requirements:
91
146
  - - ! '>='
92
147
  - !ruby/object:Gem::Version
93
148
  version: '0'
149
+ name: logging
150
+ prerelease: false
151
+ requirement: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ! '>='
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ type: :development
94
158
  description: A wrapper around Amazon's Simple Workflow Service
95
159
  email:
96
160
  - fred@snugghome.com
@@ -112,21 +176,23 @@ files:
112
176
  - lib/aws/simple_workflow/decision_task_additions.rb
113
177
  - lib/simpler_workflow.rb
114
178
  - lib/simpler_workflow/activity.rb
179
+ - lib/simpler_workflow/activity_registry.rb
115
180
  - lib/simpler_workflow/default_exception_reporter.rb
116
181
  - lib/simpler_workflow/domain.rb
117
182
  - lib/simpler_workflow/options_as_methods.rb
118
183
  - lib/simpler_workflow/tasks.rb
119
184
  - lib/simpler_workflow/version.rb
120
185
  - lib/simpler_workflow/workflow.rb
121
- - lib/simpler_workflow/workflow_collection.rb
122
186
  - lib/tasks/simpler_workflow.rake
123
187
  - simpler_workflow.gemspec
188
+ - spec/activity_spec.rb
124
189
  - spec/domain_spec.rb
125
190
  - spec/simpler_workflow_spec.rb
126
191
  - spec/spec_helper.rb
192
+ - spec/workflow_spec.rb
127
193
  homepage: https://github.com/fredjean/simpler_workflow
128
194
  licenses: []
129
- post_install_message: ! "simpler_workflow 0.2.7\n========================\n\nHave
195
+ post_install_message: ! "simpler_workflow 0.3.0.beta\n========================\n\nHave
130
196
  a look at https://github.com/fredjean/simpler_workflow/wiki/MIgrating-to-0.2.0 if
131
197
  you\nare upgrading from a 0.1.x version of the gem. There is a fundamental change
132
198
  in how the \nactivity and decision loops are run. You may need to adjust your application
@@ -140,19 +206,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
206
  requirements:
141
207
  - - ! '>='
142
208
  - !ruby/object:Gem::Version
143
- version: '0'
144
- segments:
145
- - 0
146
- hash: 618930098461104391
209
+ version: 1.9.0
147
210
  required_rubygems_version: !ruby/object:Gem::Requirement
148
211
  none: false
149
212
  requirements:
150
- - - ! '>='
213
+ - - ! '>'
151
214
  - !ruby/object:Gem::Version
152
- version: '0'
153
- segments:
154
- - 0
155
- hash: 618930098461104391
215
+ version: 1.3.1
156
216
  requirements: []
157
217
  rubyforge_project:
158
218
  rubygems_version: 1.8.23
@@ -161,6 +221,8 @@ specification_version: 3
161
221
  summary: A wrapper and DSL around Amazon's Simple Workflow Service with the goal of
162
222
  making it almost pleasant to define workflows.
163
223
  test_files:
224
+ - spec/activity_spec.rb
164
225
  - spec/domain_spec.rb
165
226
  - spec/simpler_workflow_spec.rb
166
227
  - spec/spec_helper.rb
228
+ - spec/workflow_spec.rb
@@ -1,16 +0,0 @@
1
- module SimplerWorkflow
2
- class WorkflowCollection
3
- def [](name, version)
4
- registry[[name,version]]
5
- end
6
-
7
- def []=(name, version, value)
8
- registry[[name, version]] = value
9
- end
10
-
11
- protected
12
- def registry
13
- @registry ||= {}
14
- end
15
- end
16
- end