floe 0.11.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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