simpler_workflow 0.2.7 → 0.3.0.beta

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