igniter 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +224 -1
- data/docs/API_V2.md +296 -1
- data/docs/BACKLOG.md +166 -0
- data/docs/BRANCHES_V1.md +213 -0
- data/docs/COLLECTIONS_V1.md +303 -0
- data/docs/EXECUTION_MODEL_V2.md +79 -0
- data/docs/PATTERNS.md +222 -0
- data/docs/STORE_ADAPTERS.md +126 -0
- data/examples/README.md +127 -0
- data/examples/async_store.rb +47 -0
- data/examples/collection.rb +43 -0
- data/examples/collection_partial_failure.rb +50 -0
- data/examples/marketing_ergonomics.rb +57 -0
- data/examples/ringcentral_routing.rb +269 -0
- data/lib/igniter/compiler/compiled_graph.rb +90 -0
- data/lib/igniter/compiler/graph_compiler.rb +12 -2
- data/lib/igniter/compiler/type_resolver.rb +54 -0
- data/lib/igniter/compiler/validation_context.rb +61 -0
- data/lib/igniter/compiler/validation_pipeline.rb +30 -0
- data/lib/igniter/compiler/validator.rb +1 -187
- data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +153 -0
- data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
- data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
- data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
- data/lib/igniter/compiler.rb +8 -0
- data/lib/igniter/contract.rb +152 -4
- data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
- data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
- data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
- data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
- data/lib/igniter/diagnostics/report.rb +186 -11
- data/lib/igniter/dsl/contract_builder.rb +271 -5
- data/lib/igniter/dsl/schema_builder.rb +73 -0
- data/lib/igniter/dsl.rb +1 -0
- data/lib/igniter/errors.rb +11 -0
- data/lib/igniter/events/bus.rb +5 -0
- data/lib/igniter/events/event.rb +29 -0
- data/lib/igniter/executor.rb +74 -0
- data/lib/igniter/executor_registry.rb +44 -0
- data/lib/igniter/extensions/auditing/timeline.rb +4 -0
- data/lib/igniter/extensions/introspection/graph_formatter.rb +33 -3
- data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
- data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
- data/lib/igniter/extensions/introspection.rb +1 -0
- data/lib/igniter/extensions/reactive/engine.rb +49 -2
- data/lib/igniter/extensions/reactive/reaction.rb +3 -2
- data/lib/igniter/model/branch_node.rb +46 -0
- data/lib/igniter/model/collection_node.rb +31 -0
- data/lib/igniter/model/composition_node.rb +2 -2
- data/lib/igniter/model/compute_node.rb +58 -2
- data/lib/igniter/model/input_node.rb +2 -2
- data/lib/igniter/model/output_node.rb +24 -4
- data/lib/igniter/model.rb +2 -0
- data/lib/igniter/runtime/cache.rb +64 -25
- data/lib/igniter/runtime/collection_result.rb +111 -0
- data/lib/igniter/runtime/deferred_result.rb +40 -0
- data/lib/igniter/runtime/execution.rb +261 -11
- data/lib/igniter/runtime/input_validator.rb +2 -24
- data/lib/igniter/runtime/invalidator.rb +1 -1
- data/lib/igniter/runtime/job_worker.rb +18 -0
- data/lib/igniter/runtime/node_state.rb +20 -0
- data/lib/igniter/runtime/planner.rb +126 -0
- data/lib/igniter/runtime/resolver.rb +310 -15
- data/lib/igniter/runtime/result.rb +14 -2
- data/lib/igniter/runtime/runner_factory.rb +20 -0
- data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
- data/lib/igniter/runtime/runners/store_runner.rb +29 -0
- data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
- data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
- data/lib/igniter/runtime/stores/file_store.rb +43 -0
- data/lib/igniter/runtime/stores/memory_store.rb +40 -0
- data/lib/igniter/runtime/stores/redis_store.rb +44 -0
- data/lib/igniter/runtime.rb +12 -0
- data/lib/igniter/type_system.rb +44 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +23 -0
- metadata +43 -2
|
@@ -3,17 +3,6 @@
|
|
|
3
3
|
module Igniter
|
|
4
4
|
module Runtime
|
|
5
5
|
class InputValidator
|
|
6
|
-
SUPPORTED_TYPES = {
|
|
7
|
-
integer: Integer,
|
|
8
|
-
float: Float,
|
|
9
|
-
numeric: Numeric,
|
|
10
|
-
string: String,
|
|
11
|
-
boolean: :boolean,
|
|
12
|
-
array: Array,
|
|
13
|
-
hash: Hash,
|
|
14
|
-
symbol: Symbol
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
6
|
def initialize(compiled_graph)
|
|
18
7
|
@compiled_graph = compiled_graph
|
|
19
8
|
end
|
|
@@ -104,26 +93,15 @@ module Igniter
|
|
|
104
93
|
return if value.nil?
|
|
105
94
|
return unless input_node.type
|
|
106
95
|
|
|
107
|
-
unless
|
|
96
|
+
unless TypeSystem.supported?(input_node.type)
|
|
108
97
|
raise input_error(input_node, "Unsupported input type '#{input_node.type}' for '#{input_node.name}'")
|
|
109
98
|
end
|
|
110
99
|
|
|
111
|
-
return if
|
|
100
|
+
return if TypeSystem.match?(input_node.type, value)
|
|
112
101
|
|
|
113
102
|
raise input_error(input_node, "Input '#{input_node.name}' must be of type #{input_node.type}, got #{value.class}")
|
|
114
103
|
end
|
|
115
104
|
|
|
116
|
-
def supported_type?(type)
|
|
117
|
-
SUPPORTED_TYPES.key?(type.to_sym)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def type_match?(type, value)
|
|
121
|
-
matcher = SUPPORTED_TYPES.fetch(type.to_sym)
|
|
122
|
-
return value == true || value == false if matcher == :boolean
|
|
123
|
-
|
|
124
|
-
value.is_a?(matcher)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
105
|
def symbolize_keys(hash)
|
|
128
106
|
hash.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = value }
|
|
129
107
|
end
|
|
@@ -37,7 +37,7 @@ module Igniter
|
|
|
37
37
|
|
|
38
38
|
def emit_output_invalidations_for(source_name, cause_name)
|
|
39
39
|
@execution.compiled_graph.outputs.each do |output_node|
|
|
40
|
-
next unless output_node.
|
|
40
|
+
next unless output_node.source_root == source_name.to_sym
|
|
41
41
|
|
|
42
42
|
@execution.events.emit(
|
|
43
43
|
:node_invalidated,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
class JobWorker
|
|
6
|
+
def initialize(contract_class, store: Igniter.execution_store)
|
|
7
|
+
@contract_class = contract_class
|
|
8
|
+
@store = store
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def resume(execution_id:, token:, value:)
|
|
12
|
+
contract = @contract_class.restore_from_store(execution_id, store: @store)
|
|
13
|
+
contract.execution.resume_by_token(token, value: value)
|
|
14
|
+
contract
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -19,6 +19,14 @@ module Igniter
|
|
|
19
19
|
status == :stale
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def pending?
|
|
23
|
+
status == :pending
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def running?
|
|
27
|
+
status == :running
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
def succeeded?
|
|
23
31
|
status == :succeeded
|
|
24
32
|
end
|
|
@@ -26,6 +34,18 @@ module Igniter
|
|
|
26
34
|
def failed?
|
|
27
35
|
status == :failed
|
|
28
36
|
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
node_name: node.name,
|
|
41
|
+
status: status,
|
|
42
|
+
version: version,
|
|
43
|
+
resolved_at: resolved_at,
|
|
44
|
+
invalidated_by: invalidated_by,
|
|
45
|
+
value: value,
|
|
46
|
+
error: error
|
|
47
|
+
}
|
|
48
|
+
end
|
|
29
49
|
end
|
|
30
50
|
end
|
|
31
51
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
class Planner
|
|
6
|
+
def initialize(execution)
|
|
7
|
+
@execution = execution
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def targets_for_outputs(output_names = nil)
|
|
11
|
+
selected_outputs = if output_names
|
|
12
|
+
Array(output_names).map { |name| @execution.compiled_graph.fetch_output(name) }
|
|
13
|
+
else
|
|
14
|
+
@execution.compiled_graph.outputs
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
selected_outputs
|
|
18
|
+
.map(&:source_root)
|
|
19
|
+
.uniq
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def plan(output_names = nil)
|
|
23
|
+
targets = targets_for_outputs(output_names)
|
|
24
|
+
nodes = relevant_nodes_for(targets)
|
|
25
|
+
|
|
26
|
+
node_entries = nodes.each_with_object({}) do |node, memo|
|
|
27
|
+
memo[node.name] = plan_entry(node)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
targets: targets,
|
|
32
|
+
ready: node_entries.values.select { |entry| entry[:ready] }.map { |entry| entry[:name] },
|
|
33
|
+
blocked: node_entries.values.select { |entry| entry[:blocked] }.map { |entry| entry[:name] },
|
|
34
|
+
nodes: node_entries
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def relevant_nodes_for(targets)
|
|
41
|
+
seen = {}
|
|
42
|
+
ordered = []
|
|
43
|
+
|
|
44
|
+
targets.each do |target_name|
|
|
45
|
+
visit(@execution.compiled_graph.fetch_node(target_name), seen, ordered)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ordered
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def visit(node, seen, ordered)
|
|
52
|
+
return if seen[node.name]
|
|
53
|
+
|
|
54
|
+
seen[node.name] = true
|
|
55
|
+
node.dependencies.each do |dependency_name|
|
|
56
|
+
dependency = dependency_node_for(dependency_name)
|
|
57
|
+
visit(dependency, seen, ordered)
|
|
58
|
+
end
|
|
59
|
+
ordered << node
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dependency_node_for(dependency_name)
|
|
63
|
+
dependency = @execution.compiled_graph.fetch_dependency(dependency_name)
|
|
64
|
+
return dependency if dependency.kind != :output
|
|
65
|
+
|
|
66
|
+
@execution.compiled_graph.fetch_node(dependency.source_root)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def plan_entry(node)
|
|
70
|
+
state = @execution.cache.fetch(node.name)
|
|
71
|
+
dependency_entries = node.dependencies.map { |dependency_name| dependency_entry(dependency_name) }
|
|
72
|
+
blocked_dependencies = dependency_entries.reject { |entry| entry[:satisfied] }.map { |entry| entry[:name] }
|
|
73
|
+
ready = resolution_required?(state) && blocked_dependencies.empty?
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
id: node.id,
|
|
77
|
+
name: node.name,
|
|
78
|
+
path: node.path,
|
|
79
|
+
kind: node.kind,
|
|
80
|
+
status: state&.status || :pending,
|
|
81
|
+
ready: ready,
|
|
82
|
+
blocked: !ready && resolution_required?(state),
|
|
83
|
+
dependencies: dependency_entries,
|
|
84
|
+
waiting_on: blocked_dependencies
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def dependency_entry(dependency_name)
|
|
89
|
+
dependency = @execution.compiled_graph.fetch_dependency(dependency_name)
|
|
90
|
+
source_node = dependency.kind == :output ? @execution.compiled_graph.fetch_node(dependency.source_root) : dependency
|
|
91
|
+
state = @execution.cache.fetch(source_node.name)
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
name: dependency_name.to_sym,
|
|
95
|
+
source: source_node.name,
|
|
96
|
+
kind: dependency.kind,
|
|
97
|
+
status: state&.status || inferred_status(source_node),
|
|
98
|
+
satisfied: dependency_satisfied?(source_node, state)
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def dependency_satisfied?(node, state)
|
|
103
|
+
case node.kind
|
|
104
|
+
when :input
|
|
105
|
+
input_available?(node)
|
|
106
|
+
else
|
|
107
|
+
state&.succeeded?
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def inferred_status(node)
|
|
112
|
+
return :ready if node.kind == :input && input_available?(node)
|
|
113
|
+
|
|
114
|
+
:pending
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def input_available?(node)
|
|
118
|
+
@execution.inputs.key?(node.name) || node.default?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def resolution_required?(state)
|
|
122
|
+
state.nil? || state.stale? || state.pending? || state.running?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -9,8 +9,8 @@ module Igniter
|
|
|
9
9
|
|
|
10
10
|
def resolve(node_name)
|
|
11
11
|
node = @execution.compiled_graph.fetch_node(node_name)
|
|
12
|
-
cached = @execution.cache.
|
|
13
|
-
return cached if
|
|
12
|
+
resolution_status, cached = @execution.cache.begin_resolution(node)
|
|
13
|
+
return cached if resolution_status == :cached
|
|
14
14
|
|
|
15
15
|
@execution.events.emit(:node_started, node: node, status: :running)
|
|
16
16
|
|
|
@@ -21,17 +21,30 @@ module Igniter
|
|
|
21
21
|
resolve_compute(node)
|
|
22
22
|
when :composition
|
|
23
23
|
resolve_composition(node)
|
|
24
|
+
when :branch
|
|
25
|
+
resolve_branch(node)
|
|
26
|
+
when :collection
|
|
27
|
+
resolve_collection(node)
|
|
24
28
|
else
|
|
25
29
|
raise ResolutionError, "Unsupported node kind: #{node.kind}"
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
@execution.cache.write(state)
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
emit_resolution_event(node, state)
|
|
34
|
+
state
|
|
35
|
+
rescue PendingDependencyError => e
|
|
36
|
+
state = NodeState.new(
|
|
31
37
|
node: node,
|
|
32
|
-
status:
|
|
33
|
-
|
|
38
|
+
status: :pending,
|
|
39
|
+
value: Runtime::DeferredResult.build(
|
|
40
|
+
token: e.deferred_result.token,
|
|
41
|
+
payload: e.deferred_result.payload,
|
|
42
|
+
source_node: e.deferred_result.source_node,
|
|
43
|
+
waiting_on: e.deferred_result.waiting_on || node.name
|
|
44
|
+
)
|
|
34
45
|
)
|
|
46
|
+
@execution.cache.write(state)
|
|
47
|
+
@execution.events.emit(:node_pending, node: node, status: :pending, payload: pending_payload(state))
|
|
35
48
|
state
|
|
36
49
|
rescue StandardError => e
|
|
37
50
|
state = NodeState.new(node: node, status: :failed, error: normalize_error(e, node))
|
|
@@ -48,13 +61,13 @@ module Igniter
|
|
|
48
61
|
|
|
49
62
|
def resolve_compute(node)
|
|
50
63
|
dependencies = node.dependencies.each_with_object({}) do |dependency_name, memo|
|
|
51
|
-
|
|
52
|
-
raise dependency_state.error if dependency_state.failed?
|
|
53
|
-
|
|
54
|
-
memo[dependency_name] = dependency_state.value
|
|
64
|
+
memo[dependency_name] = resolve_dependency_value(dependency_name)
|
|
55
65
|
end
|
|
56
66
|
|
|
57
67
|
value = call_compute(node.callable, dependencies)
|
|
68
|
+
return NodeState.new(node: node, status: :pending, value: normalize_deferred_result(value, node)) if deferred_result?(value)
|
|
69
|
+
value = normalize_guard_value(node, value)
|
|
70
|
+
|
|
58
71
|
NodeState.new(node: node, status: :succeeded, value: value)
|
|
59
72
|
end
|
|
60
73
|
|
|
@@ -62,8 +75,28 @@ module Igniter
|
|
|
62
75
|
case callable
|
|
63
76
|
when Proc
|
|
64
77
|
callable.call(**dependencies)
|
|
78
|
+
when Class
|
|
79
|
+
call_compute_class(callable, dependencies)
|
|
65
80
|
when Symbol, String
|
|
66
81
|
@execution.contract_instance.public_send(callable.to_sym, **dependencies)
|
|
82
|
+
else
|
|
83
|
+
call_compute_object(callable, dependencies)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def call_compute_class(callable, dependencies)
|
|
88
|
+
if callable <= Igniter::Executor
|
|
89
|
+
callable.new(execution: @execution, contract: @execution.contract_instance).call(**dependencies)
|
|
90
|
+
elsif callable.respond_to?(:call)
|
|
91
|
+
callable.call(**dependencies)
|
|
92
|
+
else
|
|
93
|
+
raise ResolutionError, "Unsupported callable: #{callable}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def call_compute_object(callable, dependencies)
|
|
98
|
+
if callable.respond_to?(:call)
|
|
99
|
+
callable.call(**dependencies)
|
|
67
100
|
else
|
|
68
101
|
raise ResolutionError, "Unsupported callable: #{callable.class}"
|
|
69
102
|
end
|
|
@@ -71,10 +104,7 @@ module Igniter
|
|
|
71
104
|
|
|
72
105
|
def resolve_composition(node)
|
|
73
106
|
child_inputs = node.input_mapping.each_with_object({}) do |(child_input_name, dependency_name), memo|
|
|
74
|
-
|
|
75
|
-
raise dependency_state.error if dependency_state.failed?
|
|
76
|
-
|
|
77
|
-
memo[child_input_name] = dependency_state.value
|
|
107
|
+
memo[child_input_name] = resolve_dependency_value(dependency_name)
|
|
78
108
|
end
|
|
79
109
|
|
|
80
110
|
child_contract = node.contract_class.new(child_inputs)
|
|
@@ -85,8 +115,183 @@ module Igniter
|
|
|
85
115
|
NodeState.new(node: node, status: :succeeded, value: child_contract.result)
|
|
86
116
|
end
|
|
87
117
|
|
|
118
|
+
def resolve_branch(node)
|
|
119
|
+
selector_value = resolve_dependency_value(node.selector_dependency)
|
|
120
|
+
selected_case = node.cases.find { |entry| entry[:match] == selector_value }
|
|
121
|
+
selected_contract = selected_case ? selected_case[:contract] : node.default_contract
|
|
122
|
+
matched_case = selected_case ? selected_case[:match] : :default
|
|
123
|
+
|
|
124
|
+
raise BranchSelectionError, "Branch '#{node.name}' has no matching case and no default" unless selected_contract
|
|
125
|
+
|
|
126
|
+
context_values = node.context_dependencies.each_with_object({}) do |dependency_name, memo|
|
|
127
|
+
memo[dependency_name] = resolve_dependency_value(dependency_name)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
child_inputs = if node.input_mapper?
|
|
131
|
+
map_branch_inputs(node, selector_value, context_values)
|
|
132
|
+
else
|
|
133
|
+
node.input_mapping.each_with_object({}) do |(child_input_name, dependency_name), memo|
|
|
134
|
+
memo[child_input_name] = resolve_dependency_value(dependency_name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@execution.events.emit(
|
|
139
|
+
:branch_selected,
|
|
140
|
+
node: node,
|
|
141
|
+
status: :succeeded,
|
|
142
|
+
payload: {
|
|
143
|
+
selector: node.selector_dependency,
|
|
144
|
+
selector_value: selector_value,
|
|
145
|
+
matched_case: matched_case,
|
|
146
|
+
selected_contract: selected_contract.name || "AnonymousContract"
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
child_contract = selected_contract.new(child_inputs)
|
|
151
|
+
child_contract.resolve_all
|
|
152
|
+
child_error = child_contract.result.errors.values.first
|
|
153
|
+
raise child_error if child_error
|
|
154
|
+
|
|
155
|
+
NodeState.new(node: node, status: :succeeded, value: child_contract.result)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def map_branch_inputs(node, selector_value, context_values)
|
|
159
|
+
mapper = node.input_mapper
|
|
160
|
+
|
|
161
|
+
if mapper.is_a?(Symbol) || mapper.is_a?(String)
|
|
162
|
+
return @execution.contract_instance.public_send(mapper, selector: selector_value, **context_values)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
mapper.call(selector: selector_value, **context_values)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def resolve_collection(node)
|
|
169
|
+
items = resolve_dependency_value(node.source_dependency)
|
|
170
|
+
context_values = node.context_dependencies.each_with_object({}) do |dependency_name, memo|
|
|
171
|
+
memo[dependency_name] = resolve_dependency_value(dependency_name)
|
|
172
|
+
end
|
|
173
|
+
normalized_items = normalize_collection_items(node, items, context_values)
|
|
174
|
+
collection_items = {}
|
|
175
|
+
|
|
176
|
+
normalized_items.each do |item_inputs|
|
|
177
|
+
item_key = extract_collection_key(node, item_inputs)
|
|
178
|
+
emit_collection_item_event(:collection_item_started, node, item_key, item_inputs: item_inputs)
|
|
179
|
+
child_contract = node.contract_class.new(item_inputs)
|
|
180
|
+
begin
|
|
181
|
+
child_contract.resolve_all
|
|
182
|
+
rescue Igniter::Error
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
child_error = child_contract.execution.cache.values.find(&:failed?)&.error
|
|
186
|
+
|
|
187
|
+
if child_error
|
|
188
|
+
collection_items[item_key] = Runtime::CollectionResult::Item.new(
|
|
189
|
+
key: item_key,
|
|
190
|
+
status: :failed,
|
|
191
|
+
error: child_error
|
|
192
|
+
)
|
|
193
|
+
emit_collection_item_event(
|
|
194
|
+
:collection_item_failed,
|
|
195
|
+
node,
|
|
196
|
+
item_key,
|
|
197
|
+
error: child_error.message,
|
|
198
|
+
error_type: child_error.class.name,
|
|
199
|
+
child_execution_id: child_contract.execution.events.execution_id
|
|
200
|
+
)
|
|
201
|
+
raise child_error if node.mode == :fail_fast
|
|
202
|
+
else
|
|
203
|
+
collection_items[item_key] = Runtime::CollectionResult::Item.new(
|
|
204
|
+
key: item_key,
|
|
205
|
+
status: :succeeded,
|
|
206
|
+
result: child_contract.result
|
|
207
|
+
)
|
|
208
|
+
emit_collection_item_event(
|
|
209
|
+
:collection_item_succeeded,
|
|
210
|
+
node,
|
|
211
|
+
item_key,
|
|
212
|
+
child_execution_id: child_contract.execution.events.execution_id
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
NodeState.new(
|
|
218
|
+
node: node,
|
|
219
|
+
status: :succeeded,
|
|
220
|
+
value: Runtime::CollectionResult.new(items: collection_items, mode: node.mode)
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolve_dependency_value(dependency_name)
|
|
225
|
+
if @execution.compiled_graph.node?(dependency_name)
|
|
226
|
+
dependency_state = resolve(dependency_name)
|
|
227
|
+
raise dependency_state.error if dependency_state.failed?
|
|
228
|
+
raise PendingDependencyError.new(dependency_state.value, context: pending_context(dependency_state.node)) if dependency_state.pending?
|
|
229
|
+
|
|
230
|
+
dependency_state.value
|
|
231
|
+
elsif @execution.compiled_graph.outputs_by_name.key?(dependency_name.to_sym)
|
|
232
|
+
output = @execution.compiled_graph.fetch_output(dependency_name)
|
|
233
|
+
value = @execution.send(:resolve_exported_output, output)
|
|
234
|
+
raise PendingDependencyError.new(value) if deferred_result?(value)
|
|
235
|
+
|
|
236
|
+
value
|
|
237
|
+
else
|
|
238
|
+
raise ResolutionError, "Unknown dependency: #{dependency_name}"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def deferred_result?(value)
|
|
243
|
+
value.is_a?(Runtime::DeferredResult)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def normalize_deferred_result(value, node)
|
|
247
|
+
Runtime::DeferredResult.build(
|
|
248
|
+
token: value.token,
|
|
249
|
+
payload: value.payload,
|
|
250
|
+
source_node: value.source_node || node.name,
|
|
251
|
+
waiting_on: value.waiting_on
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def emit_resolution_event(node, state)
|
|
256
|
+
event_type =
|
|
257
|
+
if state.failed?
|
|
258
|
+
:node_failed
|
|
259
|
+
elsif state.pending?
|
|
260
|
+
:node_pending
|
|
261
|
+
else
|
|
262
|
+
:node_succeeded
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
payload = state.pending? ? pending_payload(state) : success_payload(node, state)
|
|
266
|
+
@execution.events.emit(event_type, node: node, status: state.status, payload: payload)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def emit_collection_item_event(type, node, item_key, payload = {})
|
|
270
|
+
@execution.events.emit(
|
|
271
|
+
type,
|
|
272
|
+
node: node,
|
|
273
|
+
payload: payload.merge(item_key: item_key)
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def pending_payload(state)
|
|
278
|
+
return {} unless state.value.is_a?(Runtime::DeferredResult)
|
|
279
|
+
|
|
280
|
+
state.value.to_h
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def pending_context(node)
|
|
284
|
+
{
|
|
285
|
+
graph: @execution.compiled_graph.name,
|
|
286
|
+
node_id: node.id,
|
|
287
|
+
node_name: node.name,
|
|
288
|
+
node_path: node.path,
|
|
289
|
+
source_location: node.source_location
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
88
293
|
def success_payload(node, state)
|
|
89
|
-
return {} unless node.kind
|
|
294
|
+
return {} unless %i[composition branch].include?(node.kind)
|
|
90
295
|
return {} unless state.value.is_a?(Igniter::Runtime::Result)
|
|
91
296
|
|
|
92
297
|
{
|
|
@@ -109,6 +314,96 @@ module Igniter
|
|
|
109
314
|
}
|
|
110
315
|
)
|
|
111
316
|
end
|
|
317
|
+
|
|
318
|
+
def normalize_guard_value(node, value)
|
|
319
|
+
return value unless node.respond_to?(:guard?) && node.guard?
|
|
320
|
+
return true if value
|
|
321
|
+
|
|
322
|
+
raise ResolutionError.new(
|
|
323
|
+
node.metadata[:guard_message] || "Guard '#{node.name}' failed",
|
|
324
|
+
context: {
|
|
325
|
+
graph: @execution.compiled_graph.name,
|
|
326
|
+
node_id: node.id,
|
|
327
|
+
node_name: node.name,
|
|
328
|
+
node_path: node.path,
|
|
329
|
+
source_location: node.source_location
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def normalize_collection_items(node, items, context_values = {})
|
|
335
|
+
if node.input_mapper? && items.is_a?(Hash)
|
|
336
|
+
items = items.to_a
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
unless items.is_a?(Array)
|
|
340
|
+
raise CollectionInputError.new(
|
|
341
|
+
"Collection '#{node.name}' expects an array, got #{items.class}",
|
|
342
|
+
context: collection_context(node)
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
mapped_items = if node.input_mapper?
|
|
347
|
+
items.map { |item| map_collection_item_inputs(node, item, context_values) }
|
|
348
|
+
else
|
|
349
|
+
items
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
mapped_items.each do |item|
|
|
353
|
+
next if item.is_a?(Hash)
|
|
354
|
+
|
|
355
|
+
raise CollectionInputError.new(
|
|
356
|
+
"Collection '#{node.name}' expects item hashes, got #{item.class}",
|
|
357
|
+
context: collection_context(node)
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
ensure_unique_collection_keys!(node, mapped_items)
|
|
362
|
+
mapped_items.map { |item| item.transform_keys(&:to_sym) }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def map_collection_item_inputs(node, item, context_values)
|
|
366
|
+
mapper = node.input_mapper
|
|
367
|
+
|
|
368
|
+
if mapper.is_a?(Symbol) || mapper.is_a?(String)
|
|
369
|
+
return @execution.contract_instance.public_send(mapper, item: item, **context_values)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
mapper.call(item: item, **context_values)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def extract_collection_key(node, item_inputs)
|
|
376
|
+
item_inputs.fetch(node.key_name)
|
|
377
|
+
rescue KeyError
|
|
378
|
+
raise CollectionKeyError.new(
|
|
379
|
+
"Collection '#{node.name}' item is missing key '#{node.key_name}'",
|
|
380
|
+
context: collection_context(node)
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def ensure_unique_collection_keys!(node, items)
|
|
385
|
+
keys = items.map do |item|
|
|
386
|
+
item.fetch(node.key_name) { raise CollectionKeyError.new("Collection '#{node.name}' item is missing key '#{node.key_name}'", context: collection_context(node)) }
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
duplicates = keys.group_by(&:itself).select { |_key, entries| entries.size > 1 }.keys
|
|
390
|
+
return if duplicates.empty?
|
|
391
|
+
|
|
392
|
+
raise CollectionKeyError.new(
|
|
393
|
+
"Collection '#{node.name}' has duplicate keys: #{duplicates.join(', ')}",
|
|
394
|
+
context: collection_context(node)
|
|
395
|
+
)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def collection_context(node)
|
|
399
|
+
{
|
|
400
|
+
graph: @execution.compiled_graph.name,
|
|
401
|
+
node_id: node.id,
|
|
402
|
+
node_name: node.name,
|
|
403
|
+
node_path: node.path,
|
|
404
|
+
source_location: node.source_location
|
|
405
|
+
}
|
|
406
|
+
end
|
|
112
407
|
end
|
|
113
408
|
end
|
|
114
409
|
end
|
|
@@ -18,7 +18,7 @@ module Igniter
|
|
|
18
18
|
|
|
19
19
|
def success?
|
|
20
20
|
@execution.resolve_all
|
|
21
|
-
!failed?
|
|
21
|
+
!failed? && !pending?
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def failed?
|
|
@@ -26,6 +26,11 @@ module Igniter
|
|
|
26
26
|
@execution.cache.values.any?(&:failed?)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def pending?
|
|
30
|
+
@execution.resolve_all
|
|
31
|
+
@execution.cache.values.any?(&:pending?)
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def errors
|
|
30
35
|
@execution.resolve_all
|
|
31
36
|
@execution.cache.values.each_with_object({}) do |state, memo|
|
|
@@ -52,8 +57,9 @@ module Igniter
|
|
|
52
57
|
graph: @execution.compiled_graph.name,
|
|
53
58
|
execution_id: @execution.events.execution_id,
|
|
54
59
|
outputs: to_h,
|
|
55
|
-
success: !failed?,
|
|
60
|
+
success: !failed? && !pending?,
|
|
56
61
|
failed: failed?,
|
|
62
|
+
pending: pending?,
|
|
57
63
|
errors: serialize_errors(errors),
|
|
58
64
|
states: states
|
|
59
65
|
}
|
|
@@ -73,6 +79,8 @@ module Igniter
|
|
|
73
79
|
case value
|
|
74
80
|
when Result
|
|
75
81
|
value.as_json
|
|
82
|
+
when CollectionResult
|
|
83
|
+
value.as_json
|
|
76
84
|
when Array
|
|
77
85
|
value.map { |item| serialize_value(item) }
|
|
78
86
|
else
|
|
@@ -82,8 +90,12 @@ module Igniter
|
|
|
82
90
|
|
|
83
91
|
def serialize_output_value(value)
|
|
84
92
|
case value
|
|
93
|
+
when DeferredResult
|
|
94
|
+
value.as_json
|
|
85
95
|
when Result
|
|
86
96
|
value.to_h
|
|
97
|
+
when CollectionResult
|
|
98
|
+
value.to_h
|
|
87
99
|
when Array
|
|
88
100
|
value.map { |item| serialize_output_value(item) }
|
|
89
101
|
else
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
class RunnerFactory
|
|
6
|
+
def self.build(strategy, execution, resolver:, max_workers: nil, store: nil)
|
|
7
|
+
case strategy.to_sym
|
|
8
|
+
when :inline
|
|
9
|
+
Runners::InlineRunner.new(execution, resolver: resolver, max_workers: max_workers)
|
|
10
|
+
when :store
|
|
11
|
+
Runners::StoreRunner.new(execution, resolver: resolver, store: store, max_workers: max_workers)
|
|
12
|
+
when :thread_pool
|
|
13
|
+
Runners::ThreadPoolRunner.new(execution, resolver: resolver, max_workers: max_workers)
|
|
14
|
+
else
|
|
15
|
+
raise CompileError, "Unknown execution runner strategy: #{strategy}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|