floe 0.11.2 → 0.12.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.
@@ -4,114 +4,101 @@ module Floe
4
4
  class Workflow
5
5
  class State
6
6
  include Logging
7
+ include ValidationMixin
7
8
 
8
9
  class << self
9
10
  def build!(workflow, name, payload)
10
11
  state_type = payload["Type"]
11
- raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
12
+ missing_field_error!(name, "Type") if payload["Type"].nil?
13
+ invalid_field_error!(name[0..-2], "Name", name.last, "must be less than or equal to 80 characters") if name.last.length > 80
12
14
 
13
15
  begin
14
16
  klass = Floe::Workflow::States.const_get(state_type)
15
17
  rescue NameError
16
- raise Floe::InvalidWorkflowError, "Invalid state type: [#{state_type}]"
18
+ invalid_field_error!(name, "Type", state_type, "is not valid")
17
19
  end
18
20
 
19
21
  klass.new(workflow, name, payload)
20
22
  end
21
23
  end
22
24
 
23
- attr_reader :workflow, :comment, :name, :type, :payload
25
+ attr_reader :comment, :name, :type, :payload
24
26
 
25
- def initialize(workflow, name, payload)
26
- @workflow = workflow
27
+ def initialize(_workflow, name, payload)
27
28
  @name = name
28
29
  @payload = payload
29
30
  @type = payload["Type"]
30
31
  @comment = payload["Comment"]
31
-
32
- raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
33
- raise Floe::InvalidWorkflowError, "State name [#{name}] must be less than or equal to 80 characters" if name.length > 80
34
32
  end
35
33
 
36
- def wait(timeout: nil)
34
+ def wait(context, timeout: nil)
37
35
  start = Time.now.utc
38
36
 
39
37
  loop do
40
- return 0 if ready?
38
+ return 0 if ready?(context)
41
39
  return Errno::EAGAIN if timeout && (timeout.zero? || Time.now.utc - start > timeout)
42
40
 
43
41
  sleep(1)
44
42
  end
45
43
  end
46
44
 
47
- def run_nonblock!
48
- start(context.input) unless started?
49
- return Errno::EAGAIN unless ready?
45
+ # @return for incomplete Errno::EAGAIN, for completed 0
46
+ def run_nonblock!(context)
47
+ start(context) unless context.state_started?
48
+ return Errno::EAGAIN unless ready?(context)
50
49
 
51
- finish
50
+ finish(context)
52
51
  end
53
52
 
54
- def start(_input)
55
- start_time = Time.now.utc.iso8601
56
-
57
- context.execution["StartTime"] ||= start_time
58
- context.state["Guid"] = SecureRandom.uuid
59
- context.state["EnteredTime"] = start_time
53
+ def start(context)
54
+ context.state["EnteredTime"] = Time.now.utc.iso8601
60
55
 
61
- logger.info("Running state: [#{long_name}] with input [#{context.input}]...")
56
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
62
57
  end
63
58
 
64
- def finish
59
+ def finish(context)
65
60
  finished_time = Time.now.utc
66
- finished_time_iso = finished_time.iso8601
67
61
  entered_time = Time.parse(context.state["EnteredTime"])
68
62
 
69
- context.state["FinishedTime"] ||= finished_time_iso
63
+ context.state["FinishedTime"] ||= finished_time.iso8601
70
64
  context.state["Duration"] = finished_time - entered_time
71
- context.execution["EndTime"] = finished_time_iso if context.next_state.nil?
72
-
73
- level = context.output&.[]("Error") ? :error : :info
74
- logger.public_send(level, "Running state: [#{long_name}] with input [#{context.input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.output}]")
75
65
 
76
- context.state_history << context.state
66
+ level = context.failed? ? :error : :info
67
+ logger.public_send(level, "Running state: [#{long_name}] with input [#{context.json_input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.json_output}]")
77
68
 
78
69
  0
79
70
  end
80
71
 
81
- def context
82
- workflow.context
83
- end
84
-
85
- def started?
86
- context.state.key?("EnteredTime")
72
+ def ready?(context)
73
+ !context.state_started? || !running?(context)
87
74
  end
88
75
 
89
- def ready?
90
- !started? || !running?
76
+ def running?(context)
77
+ raise NotImplementedError, "Must be implemented in a subclass"
91
78
  end
92
79
 
93
- def finished?
94
- context.state.key?("FinishedTime")
95
- end
96
-
97
- def waiting?
80
+ def waiting?(context)
98
81
  context.state["WaitUntil"] && Time.now.utc <= Time.parse(context.state["WaitUntil"])
99
82
  end
100
83
 
101
- def wait_until
84
+ def wait_until(context)
102
85
  context.state["WaitUntil"] && Time.parse(context.state["WaitUntil"])
103
86
  end
104
87
 
88
+ def short_name
89
+ name.last
90
+ end
91
+
105
92
  def long_name
106
- "#{self.class.name.split("::").last}:#{name}"
93
+ "#{type}:#{short_name}"
107
94
  end
108
95
 
109
96
  private
110
97
 
111
- def wait_until!(seconds: nil, time: nil)
98
+ def wait_until!(context, seconds: nil, time: nil)
112
99
  context.state["WaitUntil"] =
113
100
  if seconds
114
- (Time.parse(context.state["EnteredTime"]) + seconds).iso8601
101
+ (Time.now + seconds).iso8601
115
102
  elsif time.kind_of?(String)
116
103
  time
117
104
  else
@@ -9,17 +9,18 @@ module Floe
9
9
  def initialize(workflow, name, payload)
10
10
  super
11
11
 
12
- validate_state!
12
+ validate_state!(workflow)
13
13
 
14
- @choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
14
+ @choices = payload["Choices"].map.with_index { |choice, i| ChoiceRule.build(workflow, name + ["Choices", i.to_s], choice) }
15
15
  @default = payload["Default"]
16
16
 
17
17
  @input_path = Path.new(payload.fetch("InputPath", "$"))
18
18
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
19
19
  end
20
20
 
21
- def finish
22
- output = output_path.value(context, context.input)
21
+ def finish(context)
22
+ input = input_path.value(context, context.input)
23
+ output = output_path.value(context, input)
23
24
  next_state = choices.detect { |choice| choice.true?(context, output) }&.next || default
24
25
 
25
26
  context.next_state = next_state
@@ -27,7 +28,7 @@ module Floe
27
28
  super
28
29
  end
29
30
 
30
- def running?
31
+ def running?(_)
31
32
  false
32
33
  end
33
34
 
@@ -37,18 +38,18 @@ module Floe
37
38
 
38
39
  private
39
40
 
40
- def validate_state!
41
+ def validate_state!(workflow)
41
42
  validate_state_choices!
42
- validate_state_default!
43
+ validate_state_default!(workflow)
43
44
  end
44
45
 
45
46
  def validate_state_choices!
46
- raise Floe::InvalidWorkflowError, "Choice state must have \"Choices\"" unless payload.key?("Choices")
47
- raise Floe::InvalidWorkflowError, "\"Choices\" must be a non-empty array" unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
47
+ missing_field_error!("Choices") unless payload.key?("Choices")
48
+ invalid_field_error!("Choices", nil, "must be a non-empty array") unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
48
49
  end
49
50
 
50
- def validate_state_default!
51
- raise Floe::InvalidWorkflowError, "\"Default\" not in \"States\"" unless workflow.payload["States"].include?(payload["Default"])
51
+ def validate_state_default!(workflow)
52
+ invalid_field_error!("Default", payload["Default"], "is not found in \"States\"") if payload["Default"] && !workflow_state?(payload["Default"], workflow)
52
53
  end
53
54
  end
54
55
  end
@@ -4,7 +4,7 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  class Fail < Floe::Workflow::State
7
- attr_reader :cause, :error
7
+ attr_reader :cause, :error, :cause_path, :error_path
8
8
 
9
9
  def initialize(workflow, name, payload)
10
10
  super
@@ -15,7 +15,7 @@ module Floe
15
15
  @error_path = Path.new(payload["ErrorPath"]) if payload["ErrorPath"]
16
16
  end
17
17
 
18
- def finish
18
+ def finish(context)
19
19
  context.next_state = nil
20
20
  # TODO: support intrinsic functions here
21
21
  # see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html
@@ -27,7 +27,7 @@ module Floe
27
27
  super
28
28
  end
29
29
 
30
- def running?
30
+ def running?(_)
31
31
  false
32
32
  end
33
33
 
@@ -4,23 +4,23 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  module InputOutputMixin
7
- def process_input(input)
8
- input = input_path.value(context, input)
7
+ def process_input(context)
8
+ input = input_path.value(context, context.input)
9
9
  input = parameters.value(context, input) if parameters
10
10
  input
11
11
  end
12
12
 
13
- def process_output(input, results)
14
- return input if results.nil?
13
+ def process_output(context, results)
14
+ return context.input.dup if results.nil?
15
15
  return if output_path.nil?
16
16
 
17
17
  results = result_selector.value(context, results) if @result_selector
18
18
  if result_path.payload.start_with?("$.Credentials")
19
- credentials = result_path.set(workflow.credentials, results)["Credentials"]
20
- workflow.credentials.merge!(credentials)
21
- output = input
19
+ credentials = result_path.set(context.credentials, results)["Credentials"]
20
+ context.credentials.merge!(credentials)
21
+ output = context.input.dup
22
22
  else
23
- output = result_path.set(input, results)
23
+ output = result_path.set(context.input.dup, results)
24
24
  end
25
25
 
26
26
  output_path.value(context, output)
@@ -4,16 +4,16 @@ module Floe
4
4
  class Workflow
5
5
  module States
6
6
  module NonTerminalMixin
7
- def finish
8
- # If this state is failed or the End state set next_state to nil
9
- context.next_state = end? || context.failed? ? nil : @next
7
+ def finish(context)
8
+ # If this state is failed or this is an end state, next_state to nil
9
+ context.next_state ||= end? || context.failed? ? nil : @next
10
10
 
11
11
  super
12
12
  end
13
13
 
14
- def validate_state_next!
15
- raise Floe::InvalidWorkflowError, "Missing \"Next\" field in state [#{name}]" if @next.nil? && !@end
16
- raise Floe::InvalidWorkflowError, "\"Next\" [#{@next}] not in \"States\" for state [#{name}]" if @next && !workflow.payload["States"].key?(@next)
14
+ def validate_state_next!(workflow)
15
+ missing_field_error!("Next") if @next.nil? && !@end
16
+ invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow_state?(@next, workflow)
17
17
  end
18
18
  end
19
19
  end
@@ -21,15 +21,16 @@ module Floe
21
21
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
22
22
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
23
23
 
24
- validate_state!
24
+ validate_state!(workflow)
25
25
  end
26
26
 
27
- def finish
28
- context.output = process_output(context.input.dup, result)
27
+ def finish(context)
28
+ input = result.nil? ? process_input(context) : result
29
+ context.output = process_output(context, input)
29
30
  super
30
31
  end
31
32
 
32
- def running?
33
+ def running?(_)
33
34
  false
34
35
  end
35
36
 
@@ -39,8 +40,8 @@ module Floe
39
40
 
40
41
  private
41
42
 
42
- def validate_state!
43
- validate_state_next!
43
+ def validate_state!(workflow)
44
+ validate_state_next!(workflow)
44
45
  end
45
46
  end
46
47
  end
@@ -6,13 +6,22 @@ module Floe
6
6
  class Succeed < Floe::Workflow::State
7
7
  attr_reader :input_path, :output_path
8
8
 
9
- def finish
9
+ def initialize(workflow, name, payload)
10
+ super
11
+
12
+ @input_path = Path.new(payload.fetch("InputPath", "$"))
13
+ @output_path = Path.new(payload.fetch("OutputPath", "$"))
14
+ end
15
+
16
+ def finish(context)
17
+ input = input_path.value(context, context.input)
18
+ context.output = output_path.value(context, input)
10
19
  context.next_state = nil
11
- context.output = context.input
20
+
12
21
  super
13
22
  end
14
23
 
15
- def running?
24
+ def running?(_)
16
25
  false
17
26
  end
18
27
 
@@ -18,10 +18,13 @@ module Floe
18
18
  @next = payload["Next"]
19
19
  @end = !!payload["End"]
20
20
  @resource = payload["Resource"]
21
- @runner = Floe::Runner.for_resource(@resource)
21
+
22
+ missing_field_error!("Resource") unless @resource.kind_of?(String)
23
+ @runner = wrap_parser_error("Resource", @resource) { Floe::Runner.for_resource(@resource) }
24
+
22
25
  @timeout_seconds = payload["TimeoutSeconds"]
23
- @retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
24
- @catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
26
+ @retry = payload["Retry"].to_a.map.with_index { |retrier, i| Retrier.new(workflow, name + ["Retry", i.to_s], retrier) }
27
+ @catch = payload["Catch"].to_a.map.with_index { |catcher, i| Catcher.new(workflow, name + ["Catch", i.to_s], catcher) }
25
28
  @input_path = Path.new(payload.fetch("InputPath", "$"))
26
29
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
27
30
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
@@ -29,36 +32,35 @@ module Floe
29
32
  @result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
30
33
  @credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
31
34
 
32
- validate_state!
35
+ validate_state!(workflow)
33
36
  end
34
37
 
35
- def start(input)
38
+ def start(context)
36
39
  super
37
40
 
38
- input = process_input(input)
39
- runner_context = runner.run_async!(resource, input, credentials&.value({}, workflow.credentials), context)
41
+ input = process_input(context)
42
+ runner_context = runner.run_async!(resource, input, credentials&.value({}, context.credentials), context)
40
43
 
41
44
  context.state["RunnerContext"] = runner_context
42
45
  end
43
46
 
44
- def finish
47
+ def finish(context)
45
48
  output = runner.output(context.state["RunnerContext"])
46
49
 
47
- if success?
50
+ if success?(context)
48
51
  output = parse_output(output)
49
- context.output = process_output(context.input.dup, output)
50
- super
52
+ context.output = process_output(context, output)
51
53
  else
52
- context.output = error = parse_error(output)
53
- super
54
- retry_state!(error) || catch_error!(error) || fail_workflow!(error)
54
+ error = parse_error(output)
55
+ retry_state!(context, error) || catch_error!(context, error) || fail_workflow!(context, error)
55
56
  end
57
+ super
56
58
  ensure
57
59
  runner.cleanup(context.state["RunnerContext"])
58
60
  end
59
61
 
60
- def running?
61
- return true if waiting?
62
+ def running?(context)
63
+ return true if waiting?(context)
62
64
 
63
65
  runner.status!(context.state["RunnerContext"])
64
66
  runner.running?(context.state["RunnerContext"])
@@ -72,23 +74,23 @@ module Floe
72
74
 
73
75
  attr_reader :runner
74
76
 
75
- def validate_state!
76
- validate_state_next!
77
+ def validate_state!(workflow)
78
+ validate_state_next!(workflow)
77
79
  end
78
80
 
79
- def success?
81
+ def success?(context)
80
82
  runner.success?(context.state["RunnerContext"])
81
83
  end
82
84
 
83
85
  def find_retrier(error)
84
- self.retry.detect { |r| (r.error_equals & [error, "States.ALL"]).any? }
86
+ self.retry.detect { |r| r.match_error?(error) }
85
87
  end
86
88
 
87
89
  def find_catcher(error)
88
- self.catch.detect { |c| (c.error_equals & [error, "States.ALL"]).any? }
90
+ self.catch.detect { |c| c.match_error?(error) }
89
91
  end
90
92
 
91
- def retry_state!(error)
93
+ def retry_state!(context, error)
92
94
  retrier = find_retrier(error["Error"]) if error
93
95
  return if retrier.nil?
94
96
 
@@ -102,27 +104,30 @@ module Floe
102
104
 
103
105
  return if context["State"]["RetryCount"] > retrier.max_attempts
104
106
 
105
- wait_until!(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
107
+ wait_until!(context, :seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
106
108
  context.next_state = context.state_name
107
- logger.info("Running state: [#{long_name}] with input [#{context.input}]...Retry - delay: #{wait_until}")
109
+ context.output = error
110
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}] got error[#{context.json_output}]...Retry - delay: #{wait_until(context)}")
108
111
  true
109
112
  end
110
113
 
111
- def catch_error!(error)
114
+ def catch_error!(context, error)
112
115
  catcher = find_catcher(error["Error"]) if error
113
116
  return if catcher.nil?
114
117
 
115
118
  context.next_state = catcher.next
116
119
  context.output = catcher.result_path.set(context.input, error)
117
- logger.info("Running state: [#{long_name}] with input [#{context.input}]...CatchError - next state: [#{context.next_state}] output: [#{context.output}]")
120
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...CatchError - next state: [#{context.next_state}] output: [#{context.json_output}]")
118
121
 
119
122
  true
120
123
  end
121
124
 
122
- def fail_workflow!(error)
123
- context.next_state = nil
124
- context.output = {"Error" => error["Error"], "Cause" => error["Cause"]}.compact
125
- logger.error("Running state: [#{long_name}] with input [#{context.input}]...Complete workflow - output: [#{context.output}]")
125
+ def fail_workflow!(context, error)
126
+ # next_state is nil, and will be set to nil again in super
127
+ # keeping in here for completeness
128
+ context.next_state = nil
129
+ context.output = error
130
+ logger.error("Running state: [#{long_name}] with input [#{context.json_input}]...Complete workflow - output: [#{context.json_output}]")
126
131
  end
127
132
 
128
133
  def parse_error(output)
@@ -23,28 +23,29 @@ module Floe
23
23
  @input_path = Path.new(payload.fetch("InputPath", "$"))
24
24
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
25
25
 
26
- validate_state!
26
+ validate_state!(workflow)
27
27
  end
28
28
 
29
- def start(input)
29
+ def start(context)
30
30
  super
31
31
 
32
32
  input = input_path.value(context, context.input)
33
33
 
34
34
  wait_until!(
35
+ context,
35
36
  :seconds => seconds_path ? seconds_path.value(context, input).to_i : seconds,
36
37
  :time => timestamp_path ? timestamp_path.value(context, input) : timestamp
37
38
  )
38
39
  end
39
40
 
40
- def finish
41
+ def finish(context)
41
42
  input = input_path.value(context, context.input)
42
43
  context.output = output_path.value(context, input)
43
44
  super
44
45
  end
45
46
 
46
- def running?
47
- waiting?
47
+ def running?(context)
48
+ waiting?(context)
48
49
  end
49
50
 
50
51
  def end?
@@ -53,8 +54,8 @@ module Floe
53
54
 
54
55
  private
55
56
 
56
- def validate_state!
57
- validate_state_next!
57
+ def validate_state!(workflow)
58
+ validate_state_next!(workflow)
58
59
  end
59
60
  end
60
61
  end