aws-flow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. data/Gemfile +8 -0
  2. data/LICENSE.TXT +15 -0
  3. data/NOTICE.TXT +14 -0
  4. data/Rakefile +39 -0
  5. data/aws-flow-core/Gemfile +9 -0
  6. data/aws-flow-core/LICENSE.TXT +15 -0
  7. data/aws-flow-core/NOTICE.TXT +14 -0
  8. data/aws-flow-core/Rakefile +27 -0
  9. data/aws-flow-core/aws-flow-core.gemspec +12 -0
  10. data/aws-flow-core/lib/aws/flow.rb +26 -0
  11. data/aws-flow-core/lib/aws/flow/async_backtrace.rb +134 -0
  12. data/aws-flow-core/lib/aws/flow/async_scope.rb +195 -0
  13. data/aws-flow-core/lib/aws/flow/begin_rescue_ensure.rb +386 -0
  14. data/aws-flow-core/lib/aws/flow/fiber.rb +77 -0
  15. data/aws-flow-core/lib/aws/flow/flow_utils.rb +50 -0
  16. data/aws-flow-core/lib/aws/flow/future.rb +109 -0
  17. data/aws-flow-core/lib/aws/flow/implementation.rb +151 -0
  18. data/aws-flow-core/lib/aws/flow/simple_dfa.rb +85 -0
  19. data/aws-flow-core/lib/aws/flow/tasks.rb +405 -0
  20. data/aws-flow-core/test/aws/async_backtrace_spec.rb +41 -0
  21. data/aws-flow-core/test/aws/async_scope_spec.rb +118 -0
  22. data/aws-flow-core/test/aws/begin_rescue_ensure_spec.rb +665 -0
  23. data/aws-flow-core/test/aws/external_task_spec.rb +197 -0
  24. data/aws-flow-core/test/aws/factories.rb +52 -0
  25. data/aws-flow-core/test/aws/fiber_condition_variable_spec.rb +163 -0
  26. data/aws-flow-core/test/aws/fiber_spec.rb +78 -0
  27. data/aws-flow-core/test/aws/flow_spec.rb +255 -0
  28. data/aws-flow-core/test/aws/future_spec.rb +210 -0
  29. data/aws-flow-core/test/aws/rubyflow.rb +22 -0
  30. data/aws-flow-core/test/aws/simple_dfa_spec.rb +63 -0
  31. data/aws-flow-core/test/aws/spec_helper.rb +36 -0
  32. data/aws-flow.gemspec +13 -0
  33. data/lib/aws/decider.rb +67 -0
  34. data/lib/aws/decider/activity.rb +408 -0
  35. data/lib/aws/decider/activity_definition.rb +111 -0
  36. data/lib/aws/decider/async_decider.rb +673 -0
  37. data/lib/aws/decider/async_retrying_executor.rb +153 -0
  38. data/lib/aws/decider/data_converter.rb +40 -0
  39. data/lib/aws/decider/decider.rb +511 -0
  40. data/lib/aws/decider/decision_context.rb +60 -0
  41. data/lib/aws/decider/exceptions.rb +178 -0
  42. data/lib/aws/decider/executor.rb +149 -0
  43. data/lib/aws/decider/flow_defaults.rb +70 -0
  44. data/lib/aws/decider/generic_client.rb +178 -0
  45. data/lib/aws/decider/history_helper.rb +173 -0
  46. data/lib/aws/decider/implementation.rb +82 -0
  47. data/lib/aws/decider/options.rb +607 -0
  48. data/lib/aws/decider/state_machines.rb +373 -0
  49. data/lib/aws/decider/task_handler.rb +76 -0
  50. data/lib/aws/decider/task_poller.rb +207 -0
  51. data/lib/aws/decider/utilities.rb +187 -0
  52. data/lib/aws/decider/worker.rb +324 -0
  53. data/lib/aws/decider/workflow_client.rb +374 -0
  54. data/lib/aws/decider/workflow_clock.rb +104 -0
  55. data/lib/aws/decider/workflow_definition.rb +101 -0
  56. data/lib/aws/decider/workflow_definition_factory.rb +53 -0
  57. data/lib/aws/decider/workflow_enabled.rb +26 -0
  58. data/test/aws/decider_spec.rb +1299 -0
  59. data/test/aws/factories.rb +45 -0
  60. data/test/aws/integration_spec.rb +3108 -0
  61. data/test/aws/spec_helper.rb +23 -0
  62. metadata +138 -0
@@ -0,0 +1,60 @@
1
+ #--
2
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/apache2.0
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ #++
15
+
16
+ module AWS
17
+ module Flow
18
+ class DecisionContext
19
+ attr_accessor :activity_client, :workflow_client, :workflow_clock, :workflow_context, :decision_helper
20
+ def initialize(activity_client, workflow_client, workflow_clock, workflow_context, decision_helper)
21
+ @activity_client = activity_client
22
+ @workflow_client = workflow_client
23
+ @workflow_clock = workflow_clock
24
+ @workflow_context = workflow_context
25
+ @decision_helper = decision_helper
26
+ end
27
+ end
28
+
29
+
30
+ # The context for a workflow
31
+ class WorkflowContext
32
+
33
+ attr_accessor :continue_as_new_options
34
+ # The decision task method for this workflow.
35
+ attr_accessor :decision_task
36
+
37
+ # The {WorkflowClock} for this workflow.
38
+ attr_accessor :workflow_clock
39
+
40
+ # Creates a new `WorkflowContext`
41
+ #
42
+ # @param decision_task
43
+ # The decision task method for this workflow. This is accessible after instance creation by using the
44
+ # {#decision_task} attribute.
45
+ #
46
+ # @param workflow_clock
47
+ # The {WorkflowClock} to use to schedule timers for this workflow. This is accessible after instance
48
+ # creation by using the {#workflow_clock} attribute.
49
+ #
50
+ def initialize(decision_task, workflow_clock)
51
+ @decision_task = decision_task
52
+ @workflow_clock = workflow_clock
53
+ end
54
+ def workflow_execution
55
+ @decision_task.workflow_execution
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,178 @@
1
+ #--
2
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/apache2.0
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ #++
15
+
16
+ module AWS
17
+ module Flow
18
+
19
+
20
+ # Exception used to communicate failure during fulfillment of a decision sent to SWF. This exception and all its
21
+ # subclasses are expected to be thrown by the framework.
22
+ class DecisionException < Exception
23
+ attr_accessor :event_id
24
+ end
25
+
26
+
27
+ # An exception that serves as the base for {ChildWorkflowFailedException}, {FailWorkflowExecutionException}, and
28
+ # {ActivityFailureException}.
29
+ class FlowException < Exception
30
+ # A string containing the reason for the exception.
31
+ attr_accessor :reason
32
+
33
+ # A string containing details for the exception.
34
+ attr_accessor :details
35
+
36
+ # Creates a new FlowException.
37
+ #
38
+ # @param reason [String]
39
+ # The reason for the exception. This is made available to the exception receiver through the {#reason}
40
+ # attribute.
41
+ #
42
+ # @param details [String]
43
+ # The details of the exception. This is made available to the exception receiver through the {#details}
44
+ # attribute.
45
+ #
46
+ def initialize(reason = "Something went wrong in Flow",
47
+ details = "But this indicates that it got corrupted getting out")
48
+ @reason = reason
49
+ @details = details
50
+ details = details.message if details.is_a? Exception
51
+ self.set_backtrace(details)
52
+ end
53
+ end
54
+
55
+
56
+
57
+ class ChildWorkflowException < FlowException
58
+ def detail_termination(message, event_id, workflow_execution, workflow_type)
59
+ "#{message} for workflow_execution #{workflow_execution.to_s} with event_id #{event_id}"
60
+ end
61
+ end
62
+
63
+ class ChildWorkflowTerminatedException < ChildWorkflowException
64
+ def initialize(event_id, workflow_execution, workflow_type)
65
+ @reason = "WF exception terminated"
66
+ @detail = detail_termination("Terminated", event_id, workflow_execution, workflow_type)
67
+ # TODO: we'll likely want to provide more info later, but it'll take a bit to plumb it
68
+ # @detail = "Terminate for workflow_execution #{workflow_execution.to_s} of workflow_type #{workflow_type.to_s} with event_id #{event_id}"
69
+ end
70
+ end
71
+ class ChildWorkflowTimedOutException < ChildWorkflowException
72
+ def initialize(event_id, workflow_execution, workflow_type)
73
+ @reason = "WF exception timed out"
74
+ @detail = detail_termination("Timed out", event_id, workflow_execution, workflow_type)
75
+
76
+ end
77
+ end
78
+ # Unhandled exceptions in child workflows are reported back to the parent workflow implementation by throwing a
79
+ # `ChildWorkflowFailedException`. The original exception can be retrieved from the {#reason} attribute of this
80
+ # exception. The exception also provides information in the {#details} attribute that is useful for debugging
81
+ # purposes, such as the unique identifiers of the child execution.
82
+ #
83
+ # @abstract An exception raised when the child workflow execution has failed.
84
+ #
85
+ class ChildWorkflowFailedException < FlowException
86
+
87
+ attr_accessor :cause, :details
88
+ # Creates a new `ChildWorkflowFailedException`
89
+ #
90
+ # @param event_id
91
+ # The event id for the exception.
92
+ #
93
+ # @param execution
94
+ # The child workflow execution that raised the exception.
95
+ #
96
+ # @param workflow_type
97
+ # The workflow type of the child workflow that raised the exception.
98
+ #
99
+ # @param (see FlowException#initialize)
100
+ #
101
+ def initialize(event_id, execution, workflow_type, reason, details)
102
+ @cause = details
103
+ # TODO This should probably do more with the event_id, execution, workflow_type
104
+ super(reason, details)
105
+ end
106
+ end
107
+
108
+
109
+ # @abstract An exception raised when the workflow execution has failed.
110
+ class FailWorkflowExecutionException < FlowException
111
+ end
112
+
113
+
114
+ # This exception is used by the framework internally to communicate activity failure. When an activity fails due to
115
+ # an unhandled exception, it is wrapped in ActivityFailureException and reported to Amazon SWF. You need to deal
116
+ # with this exception only if you use the activity worker extensibility points. Your application code will never
117
+ # need to deal with this exception.
118
+ #
119
+ # @abstract An exception raised when the activity has failed.
120
+ class ActivityFailureException < FlowException
121
+ end
122
+ class WorkflowException < FlowException; end
123
+ class SignalExternalWorkflowException < FlowException
124
+ def initialize(event_id, workflow_execution, cause)
125
+ super("Signalling the external workflow failed", cause)
126
+ end
127
+ end
128
+
129
+ class StartChildWorkflowFailedException < FlowException
130
+ def initialize(event_id, workflow_execution, workflow_type, cause)
131
+ super("failed to start child workflow", cause)
132
+ end
133
+ end
134
+ class StartTimerFailedException < FlowException
135
+ def initialize(event_id, timer_id, user_context, cause)
136
+ super("Timerid #{timer_id} got messed up", cause)
137
+ end
138
+ end
139
+ # This exception is thrown if an activity was timed out by Amazon SWF. This could happen if the activity task could
140
+ # not be assigned to the worker within the require time period or could not be completed by the worker in the
141
+ # required time. You can set these timeouts on the activity using the @ActivityRegistrationOptions annotation or
142
+ # using the ActivitySchedulingOptions parameter when calling the activity method.
143
+ #
144
+ # @abstract An exception raised when the activity task has timed out.
145
+ class ActivityTaskTimedOutException < ActivityFailureException
146
+
147
+ # Creates a new ActivityTaskTimeoutException
148
+ def initialize(id, activity_id, reason, details)
149
+ @id = id
150
+ @activity_id = activity_id
151
+ super(reason, details)
152
+ end
153
+ end
154
+
155
+ class ScheduleActivityTaskFailedException < FlowException
156
+ def initialize(event_id, activity_type, activity_id, cause)
157
+ super("Schedule activity task failed", cause)
158
+ end
159
+ end
160
+
161
+ # Unhandled exceptions in activities are reported back to the workflow implementation by throwing an
162
+ # `ActivityTaskFailedException`. The original exception can be retrieved from the reason attribute of this
163
+ # exception. The exception also provides information in the `details` attribute that is useful for debugging
164
+ # purposes, such as the unique activity identifier in the history.
165
+ #
166
+ # @abstract An exception raised when the activity task has failed.
167
+ class ActivityTaskFailedException < ActivityFailureException
168
+ attr_accessor :cause, :details
169
+ # Creates a new ActivityTaskFailedException
170
+ def initialize(id, activity_id, reason, details)
171
+ @id = id
172
+ @activity_id = activity_id
173
+ @cause = details
174
+ super(reason, details)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,149 @@
1
+ #--
2
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/apache2.0
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ #++
15
+
16
+ require 'tmpdir'
17
+ require 'logger'
18
+
19
+ module AWS
20
+ module Flow
21
+
22
+ class LogMock
23
+ attr_accessor :log_level
24
+ def initialize()
25
+ end
26
+ def info(s)
27
+ p "info: #{s}" if @log_level > 4
28
+ end
29
+ def debug(s)
30
+ p "debug: #{s}" if @log_level > 3
31
+ end
32
+ def warn(s)
33
+ p "warn: #{s}" if @log_level > 2
34
+ end
35
+ def error(s)
36
+ p "error: #{s}" if @log_level > 1
37
+ p s.backtrace if s.respond_to?(:backtrace)
38
+ end
39
+ end
40
+ class RejectedExecutionException < Exception; end
41
+
42
+ class ForkingExecutor
43
+
44
+ class << self
45
+ attr_accessor :executors
46
+ end
47
+ attr_accessor :max_workers, :pids, :is_shutdown
48
+
49
+ def initialize(options = {})
50
+ @log = options[:logger]
51
+ @log ||= Logger.new("#{Dir.tmpdir}/forking_log")
52
+ @semaphore = Mutex.new
53
+ log_level = options[:log_level] || 4
54
+ @log.level = Logger::DEBUG
55
+ @log.info("LOG INITIALIZED")
56
+ @max_workers = options[:max_workers] || 1
57
+ @pids = []
58
+ @is_shutdown = false
59
+ ForkingExecutor.executors ||= []
60
+ ForkingExecutor.executors << self
61
+ end
62
+
63
+ def execute(&block)
64
+ @log.error "Here are the pids that are currently running #{@pids}"
65
+ raise RejectedExecutionException if @is_shutdown
66
+ block_on_max_workers
67
+ @log.debug "PARENT BEFORE FORK #{Process.pid}"
68
+ child_pid = fork do
69
+ begin
70
+ @log.debug "CHILD #{Process.pid}"
71
+ # TODO: which signals to ignore?
72
+ # ignore signals in the child
73
+ %w{ TERM INT HUP SIGUSR2 }.each { |signal| Signal.trap(signal, 'SIG_IGN') }
74
+ block.call
75
+ @log.debug "CHILD #{Process.pid} AFTER block.call"
76
+ Process.exit!(0)
77
+ rescue => e
78
+ @log.error e
79
+ @log.error "Definitely dying off right here"
80
+ Process.exit!(1)
81
+ end
82
+ end
83
+ @log.debug "PARENT AFTER FORK #{Process.pid}, child_pid=#{child_pid}"
84
+ @pids << child_pid
85
+ end
86
+
87
+ def shutdown(timeout_seconds)
88
+ @is_shutdown = true
89
+ remove_completed_pids
90
+
91
+ unless @pids.empty?
92
+ @log.info "Exit requested, waiting up to #{timeout_seconds} seconds for child processes to finish"
93
+
94
+ # check every second for child processes to finish
95
+ timeout_seconds.times do
96
+ sleep 1
97
+ remove_completed_pids
98
+ break if @pids.empty?
99
+ end
100
+
101
+ # forcibly kill all remaining children
102
+ unless @pids.empty?
103
+ @log.warn "Child processes still running, sending KILL signal: #{@pids.join(',')}"
104
+ @pids.each { |pid| Process.kill('KILL', pid) }
105
+ end
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # Remove all child processes from @pids list that have finished
112
+ # Block for at least one child to finish if block argument is set to true
113
+ def remove_completed_pids(block=false)
114
+ loop do
115
+ # waitpid2 throws an Errno::ECHILD if there are no child processes,
116
+ # so we don't even call it if there aren't any pids to wait on
117
+ break if @pids.empty?
118
+ # non-blocking wait - only returns a non-null pid
119
+ # if the child process has exited
120
+ pid, status = Process.waitpid2(-1, block ? 0 : Process::WNOHANG)
121
+ @log.debug "#{pid}"
122
+ # no more children have finished
123
+ break unless pid
124
+
125
+ if status.success?
126
+ @log.debug "Worker #{pid} exited successfully"
127
+ else
128
+ @log.error "Worker #{pid} exited with non-zero status code"
129
+ end
130
+ @pids.delete(pid)
131
+ break if pid
132
+ end
133
+ end
134
+
135
+ def block_on_max_workers
136
+ @log.debug "block_on_max_workers workers=#{@pids.size}, max_workers=#{@max_workers}"
137
+ start_time = Time.now
138
+ if @pids.size > @max_workers
139
+ @log.info "Reached maximum number of workers (#{@max_workers}), \
140
+ waiting for some to finish before polling again"
141
+ begin
142
+ remove_completed_pids(true)
143
+ end while @pids.size > @max_workers
144
+ end
145
+ end
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,70 @@
1
+ #--
2
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/apache2.0
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+ #++
15
+
16
+ module AWS
17
+ module Flow
18
+ class FlowConstants
19
+ class << self
20
+ attr_reader :exponential_retry_maximum_retry_interval_seconds, :exponential_retry_retry_expiration_seconds, :exponential_retry_backoff_coefficient, :exponential_retry_maximum_attempts, :exponential_retry_function, :default_data_converter, :exponential_retry_exceptions_to_include, :exponential_retry_exceptions_to_exclude, :jitter_function, :should_jitter
21
+ # # The maximum exponential retry interval, in seconds. Use the value -1 (the default) to set <i>no maximum</i>.
22
+ # attr_reader :exponential_retry_maximum_retry_interval_seconds
23
+
24
+ # # The maximum time that can pass, in seconds, before the exponential retry attempt is considered a failure. Use
25
+ # # the value -1 (the default) to set <i>no maximum</i>.
26
+ # attr_reader :exponential_retry_retry_expiration_seconds
27
+
28
+ # # The coefficient used to determine how much to back off the interval timing for an exponential retry scenario.
29
+ # # The default value, 2.0, causes each attempt to wait twice as long as the previous attempt.
30
+ # attr_reader :exponential_retry_backoff_coefficient
31
+
32
+ # # The maximum number of attempts to make for an exponential retry of a failed task. The default value is
33
+ # # Float::INFINITY.
34
+ # attr_reader :exponential_retry_maximum_attempts
35
+
36
+ # # The default exponential retry function.
37
+ # attr_reader :exponential_retry_function
38
+
39
+ # The DataConverter for instances of this class.
40
+ attr_reader :default_data_converter
41
+ end
42
+ INFINITY = -1
43
+ @exponential_retry_maximum_attempts = Float::INFINITY
44
+ @exponential_retry_maximum_retry_interval_seconds = -1
45
+ @exponential_retry_retry_expiration_seconds = -1
46
+ @exponential_retry_backoff_coefficient = 2.0
47
+ @exponential_retry_initial_retry_interval = 2
48
+ @should_jitter = true
49
+ @exponential_retry_exceptions_to_exclude = []
50
+ @exponential_retry_exceptions_to_include = [Exception]
51
+ @exponential_retry_function = lambda do |first, time_of_failure, attempts|
52
+ raise ArgumentError.new("first is not an instance of Time") unless first.instance_of?(Time)
53
+ raise ArgumentError.new("time_of_failure can't be negative") if time_of_failure < 0
54
+ raise ArgumentError.new("number of attempts can't be negative") if (attempts.values.find {|x| x < 0})
55
+ result = @exponential_retry_initial_retry_interval * (@exponential_retry_backoff_coefficient ** (attempts.values.reduce(0, :+) - 2))
56
+ result = @exponential_retry_maximum_retry_interval_seconds if @exponential_retry_maximum_retry_interval_seconds != INFINITY && result > @exponential_retry_maximum_retry_interval_seconds
57
+ seconds_since_first_attempt = time_of_failure.zero? ? 0 : -(first - time_of_failure).to_i
58
+ result = -1 if @exponential_retry_retry_expiration_seconds != INFINITY && (result + seconds_since_first_attempt) >= @exponential_retry_retry_expiration_seconds
59
+ return result.to_i
60
+ end
61
+
62
+ @jitter_function = lambda do |seed, max_value|
63
+ random = Random.new(seed.to_i)
64
+ random.rand(max_value)
65
+ end
66
+
67
+ @default_data_converter = YAMLDataConverter.new
68
+ end
69
+ end
70
+ end