-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Map state #184
Implement Map state #184
Changes from all commits
6318b74
44deddd
0207fc1
79c2759
80df0f0
80d9468
9d785f0
ed854ce
8bf874c
9e3d2d6
1ca1251
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"Comment": "Using Map state in Inline mode", | ||
"StartAt": "Pass", | ||
"States": { | ||
"Pass": { | ||
"Type": "Pass", | ||
"Next": "Map demo", | ||
"Result": { | ||
"foo": "bar", | ||
"colors": [ | ||
"red", | ||
"green", | ||
"blue", | ||
"yellow", | ||
"white" | ||
] | ||
} | ||
}, | ||
"Map demo": { | ||
"Type": "Map", | ||
"ItemsPath": "$.colors", | ||
"ItemProcessor": { | ||
"ProcessorConfig": { | ||
"Mode": "INLINE" | ||
}, | ||
"StartAt": "Generate UUID", | ||
"States": { | ||
"Generate UUID": { | ||
"Type": "Pass", | ||
"End": true, | ||
"Parameters": { | ||
"uuid.$": "States.UUID()" | ||
}, | ||
"OutputPath": "$.uuid" | ||
} | ||
} | ||
}, | ||
"End": true | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# frozen_string_literal: true | ||
|
||
module Floe | ||
class Workflow | ||
class ItemProcessor < Floe::WorkflowBase | ||
attr_reader :processor_config | ||
|
||
def initialize(payload, name = nil) | ||
super | ||
@processor_config = payload.fetch("ProcessorConfig", "INLINE") | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,116 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "input_output_mixin" | ||
require_relative "non_terminal_mixin" | ||
|
||
module Floe | ||
class Workflow | ||
module States | ||
class Map < Floe::Workflow::State | ||
def initialize(*) | ||
include InputOutputMixin | ||
include NonTerminalMixin | ||
|
||
attr_reader :end, :next, :parameters, :input_path, :output_path, :result_path, | ||
:result_selector, :retry, :catch, :item_processor, :items_path, | ||
:item_reader, :item_selector, :item_batcher, :result_writer, | ||
:max_concurrency, :tolerated_failure_percentage, :tolerated_failure_count | ||
|
||
def initialize(workflow, name, payload) | ||
super | ||
raise NotImplementedError | ||
|
||
missing_field_error!("InputProcessor") if payload["ItemProcessor"].nil? | ||
|
||
@next = payload["Next"] | ||
@end = !!payload["End"] | ||
@parameters = PayloadTemplate.new(payload["Parameters"]) if payload["Parameters"] | ||
@input_path = Path.new(payload.fetch("InputPath", "$")) | ||
@output_path = Path.new(payload.fetch("OutputPath", "$")) | ||
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$")) | ||
@result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"] | ||
@retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) } | ||
@catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) } | ||
@item_processor = ItemProcessor.new(payload["ItemProcessor"], name) | ||
@items_path = ReferencePath.new(payload.fetch("ItemsPath", "$")) | ||
@item_reader = payload["ItemReader"] | ||
@item_selector = payload["ItemSelector"] | ||
@item_batcher = payload["ItemBatcher"] | ||
@result_writer = payload["ResultWriter"] | ||
@max_concurrency = payload["MaxConcurrency"]&.to_i | ||
@tolerated_failure_percentage = payload["ToleratedFailurePercentage"] | ||
@tolerated_failure_count = payload["ToleratedFailureCount"] | ||
|
||
validate_state!(workflow) | ||
end | ||
|
||
def process_input(context) | ||
input = super | ||
items_path.value(context, input) | ||
end | ||
|
||
def start(context) | ||
super | ||
|
||
input = process_input(context) | ||
|
||
context.state["Iteration"] = 0 | ||
context.state["MaxIterations"] = input.count | ||
context.state["ItemProcessorContext"] = input.map { |item| Context.new({"Execution" => {"Id" => context.execution["Id"]}}, :input => item.to_json).to_h } | ||
end | ||
|
||
def finish(context) | ||
result = context.state["ItemProcessorContext"].map { |ctx| Context.new(ctx).output } | ||
context.output = process_output(context, result) | ||
super | ||
end | ||
|
||
def run_nonblock!(context) | ||
start(context) unless context.state_started? | ||
|
||
loop while step_nonblock!(context) == 0 && running?(context) | ||
return Errno::EAGAIN unless ready?(context) | ||
|
||
finish(context) if ended?(context) | ||
end | ||
|
||
def end? | ||
@end | ||
end | ||
|
||
def ready?(context) | ||
!context.state_started? || context.state["ItemProcessorContext"].any? { |ctx| item_processor.step_nonblock_ready?(Context.new(ctx)) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No only that the current Map state has started There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had deleted context.state_started? from So you can probably drop it? |
||
end | ||
|
||
def wait_until(context) | ||
context.state["ItemProcessorContext"].filter_map { |ctx| item_processor.wait_until(Context.new(ctx)) }.min | ||
end | ||
|
||
def waiting?(context) | ||
context.state["ItemProcessorContext"].any? { |ctx| item_processor.waiting?(Context.new(ctx)) } | ||
end | ||
|
||
def running?(context) | ||
!ended?(context) | ||
end | ||
|
||
def ended?(context) | ||
context.state["ItemProcessorContext"].all? { |ctx| Context.new(ctx).ended? } | ||
end | ||
|
||
private | ||
|
||
def step_nonblock!(context) | ||
item_processor_context = Context.new(context.state["ItemProcessorContext"][context.state["Iteration"]]) | ||
item_processor.run_nonblock(item_processor_context) if item_processor.step_nonblock_ready?(item_processor_context) | ||
if item_processor_context.ended? | ||
context.state["Iteration"] += 1 | ||
0 | ||
else | ||
Errno::EAGAIN | ||
end | ||
Comment on lines
+102
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is the loop thing again
|
||
end | ||
|
||
def validate_state!(workflow) | ||
validate_state_next!(workflow) | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
RSpec.describe Floe::Workflow::ItemProcessor do | ||
it "raises an exception for missing States field" do | ||
payload = {"StartAt" => "Missing"} | ||
expect { described_class.new(payload, ["Map"]) } | ||
.to raise_error(Floe::InvalidWorkflowError, "Map does not have required field \"States\"") | ||
end | ||
|
||
it "raises an exception for missing StartAt field" do | ||
payload = {"States" => {"First" => {"Type" => "Succeed"}}} | ||
expect { described_class.new(payload, ["Map"]) } | ||
.to raise_error(Floe::InvalidWorkflowError, "Map does not have required field \"StartAt\"") | ||
end | ||
|
||
it "raises an exception if StartAt isn't in States" do | ||
payload = {"StartAt" => "First", "States" => {"Second" => {"Type" => "Succeed"}}} | ||
expect { described_class.new(payload, ["Map"]) } | ||
.to raise_error(Floe::InvalidWorkflowError, "Map field \"StartAt\" value \"First\" is not found in \"States\"") | ||
end | ||
|
||
it "raises an exception if a Next state isn't in States" do | ||
payload = {"StartAt" => "First", "States" => {"First" => {"Type" => "Pass", "Next" => "Last"}}} | ||
expect { described_class.new(payload, ["Map"]) } | ||
.to raise_error(Floe::InvalidWorkflowError, "States.First field \"Next\" value \"Last\" is not found in \"States\"") | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool 👍... Was wondering how you were going to implement it, since it's like a sub workflow