igniter 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +224 -1
- data/docs/API_V2.md +238 -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 +124 -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 +278 -0
- data/lib/igniter/compiler/compiled_graph.rb +82 -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 +151 -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 +136 -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 +84 -8
- data/lib/igniter/dsl/contract_builder.rb +208 -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 +29 -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 +40 -0
- data/lib/igniter/model/collection_node.rb +25 -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 +269 -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
data/lib/igniter/contract.rb
CHANGED
|
@@ -7,16 +7,102 @@ module Igniter
|
|
|
7
7
|
@compiled_graph = DSL::ContractBuilder.compile(name: contract_name, &block)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def run_with(runner:, max_workers: nil)
|
|
11
|
+
@execution_options = { runner: runner, max_workers: max_workers }.compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def restore_from_store(execution_id, store: nil)
|
|
15
|
+
snapshot = (store || Igniter.execution_store).fetch(execution_id)
|
|
16
|
+
restore(snapshot)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resume_from_store(execution_id, token:, value:, store: nil)
|
|
20
|
+
Runtime::JobWorker.new(self, store: store || Igniter.execution_store).resume(
|
|
21
|
+
execution_id: execution_id,
|
|
22
|
+
token: token,
|
|
23
|
+
value: value
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def define_schema(schema)
|
|
28
|
+
@compiled_graph = DSL::SchemaBuilder.compile(schema, name: contract_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def restore(snapshot)
|
|
32
|
+
instance = new(
|
|
33
|
+
snapshot[:inputs] || snapshot["inputs"] || {},
|
|
34
|
+
runner: snapshot[:runner] || snapshot["runner"],
|
|
35
|
+
max_workers: snapshot[:max_workers] || snapshot["max_workers"]
|
|
36
|
+
)
|
|
37
|
+
instance.restore_execution(snapshot)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def react_to(event_type, path: nil, once_per_execution: false, &block)
|
|
11
41
|
raise CompileError, "react_to requires a block" unless block
|
|
12
42
|
|
|
13
43
|
reactions << Extensions::Reactive::Reaction.new(
|
|
14
44
|
event_type: event_type,
|
|
15
45
|
path: path,
|
|
16
|
-
action: block
|
|
46
|
+
action: block,
|
|
47
|
+
once_per_execution: once_per_execution
|
|
17
48
|
)
|
|
18
49
|
end
|
|
19
50
|
|
|
51
|
+
def effect(path = nil, event_type: :node_succeeded, &block)
|
|
52
|
+
react_to(event_type, path: path, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def on_success(target, &block)
|
|
56
|
+
graph = compiled_graph
|
|
57
|
+
if graph&.output?(target)
|
|
58
|
+
react_to(:execution_finished, once_per_execution: true) do |event:, contract:, execution:|
|
|
59
|
+
next if execution.cache.values.any?(&:failed?)
|
|
60
|
+
next if execution.cache.values.any?(&:pending?)
|
|
61
|
+
|
|
62
|
+
block.call(
|
|
63
|
+
event: event,
|
|
64
|
+
contract: contract,
|
|
65
|
+
execution: execution,
|
|
66
|
+
value: contract.result.public_send(target)
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
effect(target.to_s, event_type: :node_succeeded, &block)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def on_failure(&block)
|
|
75
|
+
react_to(:execution_failed, once_per_execution: true) do |event:, contract:, execution:|
|
|
76
|
+
block.call(
|
|
77
|
+
event: event,
|
|
78
|
+
contract: contract,
|
|
79
|
+
execution: execution,
|
|
80
|
+
status: :failed,
|
|
81
|
+
errors: terminal_errors(execution),
|
|
82
|
+
error: terminal_errors(execution).values.first
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def on_exit(&block)
|
|
88
|
+
terminal_hook = proc do |event:, contract:, execution:|
|
|
89
|
+
status = event.type == :execution_failed ? :failed : :succeeded
|
|
90
|
+
errors = status == :failed ? terminal_errors(execution) : {}
|
|
91
|
+
|
|
92
|
+
block.call(
|
|
93
|
+
event: event,
|
|
94
|
+
contract: contract,
|
|
95
|
+
execution: execution,
|
|
96
|
+
status: status,
|
|
97
|
+
errors: errors,
|
|
98
|
+
error: errors.values.first
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
react_to(:execution_finished, once_per_execution: true, &terminal_hook)
|
|
103
|
+
react_to(:execution_failed, once_per_execution: true, &terminal_hook)
|
|
104
|
+
end
|
|
105
|
+
|
|
20
106
|
def compiled_graph
|
|
21
107
|
@compiled_graph || superclass_compiled_graph
|
|
22
108
|
end
|
|
@@ -26,6 +112,10 @@ module Igniter
|
|
|
26
112
|
@reactions ||= []
|
|
27
113
|
end
|
|
28
114
|
|
|
115
|
+
def execution_options
|
|
116
|
+
@execution_options || superclass_execution_options || {}
|
|
117
|
+
end
|
|
118
|
+
|
|
29
119
|
private
|
|
30
120
|
|
|
31
121
|
def contract_name
|
|
@@ -37,18 +127,47 @@ module Igniter
|
|
|
37
127
|
|
|
38
128
|
superclass.compiled_graph
|
|
39
129
|
end
|
|
130
|
+
|
|
131
|
+
def superclass_execution_options
|
|
132
|
+
return unless superclass.respond_to?(:execution_options)
|
|
133
|
+
|
|
134
|
+
superclass.execution_options
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def terminal_errors(execution)
|
|
138
|
+
execution.cache.values.each_with_object({}) do |state, memo|
|
|
139
|
+
next unless state.failed?
|
|
140
|
+
|
|
141
|
+
memo[state.node.name] = state.error
|
|
142
|
+
end
|
|
143
|
+
end
|
|
40
144
|
end
|
|
41
145
|
|
|
42
146
|
attr_reader :execution, :result
|
|
43
147
|
|
|
44
|
-
def initialize(inputs =
|
|
148
|
+
def initialize(inputs = nil, runner: nil, max_workers: nil, **keyword_inputs)
|
|
45
149
|
graph = self.class.compiled_graph
|
|
46
150
|
raise CompileError, "#{self.class.name} has no compiled graph. Use `define`." unless graph
|
|
47
151
|
|
|
152
|
+
normalized_inputs =
|
|
153
|
+
if inputs.nil?
|
|
154
|
+
keyword_inputs
|
|
155
|
+
elsif keyword_inputs.empty?
|
|
156
|
+
inputs
|
|
157
|
+
else
|
|
158
|
+
inputs.to_h.merge(keyword_inputs)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
execution_options = self.class.execution_options.merge(
|
|
162
|
+
{ runner: runner, max_workers: max_workers }.compact
|
|
163
|
+
)
|
|
164
|
+
execution_options[:store] ||= Igniter.execution_store if execution_options[:runner]&.to_sym == :store
|
|
165
|
+
|
|
48
166
|
@execution = Runtime::Execution.new(
|
|
49
167
|
compiled_graph: graph,
|
|
50
168
|
contract_instance: self,
|
|
51
|
-
inputs:
|
|
169
|
+
inputs: normalized_inputs,
|
|
170
|
+
**execution_options
|
|
52
171
|
)
|
|
53
172
|
@reactive = Extensions::Reactive::Engine.new(
|
|
54
173
|
execution: @execution,
|
|
@@ -98,6 +217,15 @@ module Igniter
|
|
|
98
217
|
Diagnostics::Report.new(execution)
|
|
99
218
|
end
|
|
100
219
|
|
|
220
|
+
def snapshot
|
|
221
|
+
execution.snapshot
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def restore_execution(snapshot)
|
|
225
|
+
execution.restore!(snapshot)
|
|
226
|
+
self
|
|
227
|
+
end
|
|
228
|
+
|
|
101
229
|
def diagnostics_text
|
|
102
230
|
diagnostics.to_text
|
|
103
231
|
end
|
|
@@ -106,6 +234,10 @@ module Igniter
|
|
|
106
234
|
diagnostics.to_markdown
|
|
107
235
|
end
|
|
108
236
|
|
|
237
|
+
def explain_plan(output_names = nil)
|
|
238
|
+
execution.explain_plan(output_names)
|
|
239
|
+
end
|
|
240
|
+
|
|
109
241
|
def success?
|
|
110
242
|
execution.success?
|
|
111
243
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Diagnostics
|
|
7
|
+
module Auditing
|
|
8
|
+
module Report
|
|
9
|
+
# Generates a human-readable text tree of an audition step.
|
|
10
|
+
class ConsoleFormatter
|
|
11
|
+
def self.call(player)
|
|
12
|
+
new(player).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(player)
|
|
16
|
+
@player = player
|
|
17
|
+
@buffer = StringIO.new
|
|
18
|
+
@definition_graph = player.definition_graph
|
|
19
|
+
@audited_graph = player.current_graph
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
print_header
|
|
24
|
+
|
|
25
|
+
@definition_graph.root_nodes.each do |node_def|
|
|
26
|
+
build_node_tree(node_def, prefix: "")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@buffer.string
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def print_header
|
|
35
|
+
trigger = @audited_graph.triggering_event
|
|
36
|
+
@buffer.puts "--- Audition for #{@player.record.contract_class_name} ---"
|
|
37
|
+
@buffer.puts "Step: #{@audited_graph.version} / #{@player.versions_count}"
|
|
38
|
+
@buffer.puts "Trigger: #{trigger[:type]} on '#{trigger[:path]}'" if trigger
|
|
39
|
+
@buffer.puts "-------------------------------------------------"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_node_tree(node_def, prefix:)
|
|
43
|
+
children_defs = @definition_graph.children_of(node_def)
|
|
44
|
+
is_last = (node_def.parent ? @definition_graph.children_of(node_def.parent) : @definition_graph.root_nodes).last == node_def
|
|
45
|
+
|
|
46
|
+
@buffer << prefix
|
|
47
|
+
@buffer << (is_last ? "└── " : "├── ")
|
|
48
|
+
@buffer << "#{node_def.name} [#{node_def.class.name.split('::').last}]"
|
|
49
|
+
|
|
50
|
+
append_node_details(node_def)
|
|
51
|
+
|
|
52
|
+
@buffer << "\n"
|
|
53
|
+
|
|
54
|
+
new_prefix = prefix + (is_last ? " " : "│ ")
|
|
55
|
+
children_defs.each do |child_def|
|
|
56
|
+
build_node_tree(child_def, prefix: new_prefix)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def append_node_details(node_def)
|
|
61
|
+
audited_node = @audited_graph.find(node_def.path)
|
|
62
|
+
if audited_node
|
|
63
|
+
status_icon = case audited_node.status.to_sym
|
|
64
|
+
when :success then "✓"
|
|
65
|
+
when :failure then "✗"
|
|
66
|
+
when :invalidated then "~"
|
|
67
|
+
else "?"
|
|
68
|
+
end
|
|
69
|
+
@buffer << " [#{status_icon}]"
|
|
70
|
+
@buffer << " => <#{audited_node.value_class}> #{audited_node.value.to_s.truncate(100)}"
|
|
71
|
+
@buffer << " (Errors: #{audited_node.errors.join(', ')})" if audited_node.status.to_sym == :failure
|
|
72
|
+
else
|
|
73
|
+
@buffer << " [Not Present in History]"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Diagnostics
|
|
5
|
+
module Auditing
|
|
6
|
+
module Report
|
|
7
|
+
class MarkdownFormatter
|
|
8
|
+
def self.call(player)
|
|
9
|
+
new(player).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(player)
|
|
13
|
+
@player = player
|
|
14
|
+
@definition_graph = player.definition_graph
|
|
15
|
+
@audited_graph = player.current_graph
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Diagnostics
|
|
5
|
+
module Introspection
|
|
6
|
+
module Formatters
|
|
7
|
+
class MermaidFormatter
|
|
8
|
+
def self.call(structure)
|
|
9
|
+
new(structure).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(structure)
|
|
13
|
+
@structure = structure
|
|
14
|
+
@output = StringIO.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
# Устанавливаем направление диаграммы слева направо
|
|
19
|
+
@output.puts "graph LR"
|
|
20
|
+
@output.puts "classDef default fill:#fff,stroke:#333,stroke-width:2px;"
|
|
21
|
+
|
|
22
|
+
# Запускаем рекурсивную отрисовку
|
|
23
|
+
draw_nodes_and_links(@structure)
|
|
24
|
+
|
|
25
|
+
@output.string
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def draw_nodes_and_links(nodes)
|
|
31
|
+
nodes.each do |node|
|
|
32
|
+
# 1. Если узел - это Namespace, создаем для него subgraph
|
|
33
|
+
if node[:type] == "Namespace"
|
|
34
|
+
@output.puts " subgraph #{node[:name]}"
|
|
35
|
+
# Рекурсивно отрисовываем всех детей внутри подграфа
|
|
36
|
+
draw_nodes_and_links(node[:children])
|
|
37
|
+
@output.puts " end"
|
|
38
|
+
else
|
|
39
|
+
# 2. Если это обычный узел, просто объявляем его
|
|
40
|
+
node_text = "\"#{node[:name]}<br>[#{node[:details]}]\""
|
|
41
|
+
@output.puts " #{node[:path]}(#{node_text})"
|
|
42
|
+
|
|
43
|
+
# У обычного узла тоже могут быть дети (например, проекции у композиции),
|
|
44
|
+
# их тоже нужно отрисовать.
|
|
45
|
+
draw_nodes_and_links(node[:children])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 3. После объявления узла (или целого подграфа) рисуем его зависимости
|
|
49
|
+
node[:dependencies].each do |dep_path|
|
|
50
|
+
@output.puts " #{dep_path} --> #{node[:path]}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Diagnostics
|
|
5
|
+
module Introspection
|
|
6
|
+
module Formatters
|
|
7
|
+
class TextTreeFormatter
|
|
8
|
+
def self.call(structure)
|
|
9
|
+
new(structure).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(structure)
|
|
13
|
+
@structure = structure
|
|
14
|
+
@output = StringIO.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
@structure.each_with_index do |node, index|
|
|
19
|
+
is_last = index == @structure.size - 1
|
|
20
|
+
print_node(node, "", is_last)
|
|
21
|
+
end
|
|
22
|
+
@output.string
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def print_node(node, prefix, is_last)
|
|
28
|
+
connector = is_last ? "└── " : "├── "
|
|
29
|
+
@output << "#{prefix}#{connector}#{node[:name]} [#{node[:type]}] #{node[:details]}\n"
|
|
30
|
+
|
|
31
|
+
children_prefix = prefix + (is_last ? " " : "│ ")
|
|
32
|
+
if node[:dependencies].any?
|
|
33
|
+
@output << "#{children_prefix} └─ depends_on: [#{node[:dependencies].join(', ')}]\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
node[:children].each_with_index do |child, index|
|
|
37
|
+
print_node(child, children_prefix, index == node[:children].size - 1)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -17,6 +17,7 @@ module Igniter
|
|
|
17
17
|
outputs: serialize_outputs,
|
|
18
18
|
errors: serialize_errors,
|
|
19
19
|
nodes: summarize_nodes,
|
|
20
|
+
collection_nodes: summarize_collection_nodes,
|
|
20
21
|
events: summarize_events
|
|
21
22
|
}
|
|
22
23
|
end
|
|
@@ -33,6 +34,7 @@ module Igniter
|
|
|
33
34
|
lines << "Status: #{report[:status]}"
|
|
34
35
|
lines << format_outputs(report[:outputs])
|
|
35
36
|
lines << format_nodes(report[:nodes])
|
|
37
|
+
lines << format_collection_nodes(report[:collection_nodes])
|
|
36
38
|
lines << format_errors(report[:errors])
|
|
37
39
|
lines << format_events(report[:events])
|
|
38
40
|
lines.compact.join("\n")
|
|
@@ -47,6 +49,9 @@ module Igniter
|
|
|
47
49
|
lines << "- Status: `#{report[:status]}`"
|
|
48
50
|
lines << "- Outputs: #{inline_hash(report[:outputs])}"
|
|
49
51
|
lines << "- Nodes: total=#{report[:nodes][:total]}, succeeded=#{report[:nodes][:succeeded]}, failed=#{report[:nodes][:failed]}, stale=#{report[:nodes][:stale]}"
|
|
52
|
+
unless report[:collection_nodes].empty?
|
|
53
|
+
lines << "- Collections: #{report[:collection_nodes].map { |node| "#{node[:node_name]} total=#{node[:total]} succeeded=#{node[:succeeded]} failed=#{node[:failed]} status=#{node[:status]}" }.join('; ')}"
|
|
54
|
+
end
|
|
50
55
|
lines << "- Events: total=#{report[:events][:total]}, latest=#{report[:events][:latest_type] || 'none'}"
|
|
51
56
|
|
|
52
57
|
unless report[:errors].empty?
|
|
@@ -57,6 +62,17 @@ module Igniter
|
|
|
57
62
|
end
|
|
58
63
|
end
|
|
59
64
|
|
|
65
|
+
unless report[:collection_nodes].empty?
|
|
66
|
+
lines << ""
|
|
67
|
+
lines << "## Collections"
|
|
68
|
+
report[:collection_nodes].each do |node|
|
|
69
|
+
lines << "- `#{node[:node_name]}`: total=#{node[:total]}, succeeded=#{node[:succeeded]}, failed=#{node[:failed]}, status=#{node[:status]}"
|
|
70
|
+
node[:failed_items].each do |item|
|
|
71
|
+
lines << "- `#{node[:node_name]}[#{item[:key]}]` failed: #{item[:message]}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
lines.join("\n")
|
|
61
77
|
end
|
|
62
78
|
|
|
@@ -76,36 +92,52 @@ module Igniter
|
|
|
76
92
|
|
|
77
93
|
def status
|
|
78
94
|
return :failed if execution.cache.values.any?(&:failed?)
|
|
95
|
+
return :pending if execution.cache.values.any?(&:pending?)
|
|
79
96
|
return :stale if execution.cache.values.any?(&:stale?)
|
|
80
97
|
|
|
81
98
|
:succeeded
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
def serialize_errors
|
|
85
|
-
|
|
102
|
+
execution.cache.values.filter_map do |state|
|
|
103
|
+
next unless state.failed?
|
|
104
|
+
|
|
86
105
|
{
|
|
87
|
-
node_name:
|
|
88
|
-
type: error.class.name,
|
|
89
|
-
message: error.message,
|
|
90
|
-
context: error.respond_to?(:context) ? error.context : {}
|
|
106
|
+
node_name: state.node.name,
|
|
107
|
+
type: state.error.class.name,
|
|
108
|
+
message: state.error.message,
|
|
109
|
+
context: state.error.respond_to?(:context) ? state.error.context : {}
|
|
91
110
|
}
|
|
92
111
|
end
|
|
93
112
|
end
|
|
94
113
|
|
|
95
114
|
def serialize_outputs
|
|
96
115
|
execution.compiled_graph.outputs.each_with_object({}) do |output_node, memo|
|
|
97
|
-
state = execution.cache.fetch(output_node.
|
|
98
|
-
memo[output_node.name] =
|
|
116
|
+
state = execution.cache.fetch(output_node.source_root)
|
|
117
|
+
memo[output_node.name] = serialize_output_value(output_node, state)
|
|
99
118
|
end
|
|
100
119
|
end
|
|
101
120
|
|
|
102
|
-
def
|
|
121
|
+
def serialize_output_value(output_node, state)
|
|
103
122
|
return nil unless state
|
|
104
123
|
return { error: state.error.message, status: state.status } if state.failed?
|
|
105
124
|
|
|
125
|
+
if output_node.composition_output?
|
|
126
|
+
return serialize_output_from_child(output_node, state.value)
|
|
127
|
+
end
|
|
128
|
+
|
|
106
129
|
serialize_value(state.value)
|
|
107
130
|
end
|
|
108
131
|
|
|
132
|
+
def serialize_output_from_child(output_node, child_result)
|
|
133
|
+
return nil unless child_result.is_a?(Runtime::Result)
|
|
134
|
+
|
|
135
|
+
child_errors = child_result.execution.cache.values.select(&:failed?)
|
|
136
|
+
return { error: child_errors.first.error.message, status: :failed } unless child_errors.empty?
|
|
137
|
+
|
|
138
|
+
child_result.public_send(output_node.child_output_name)
|
|
139
|
+
end
|
|
140
|
+
|
|
109
141
|
def summarize_nodes
|
|
110
142
|
states = execution.states
|
|
111
143
|
|
|
@@ -113,6 +145,7 @@ module Igniter
|
|
|
113
145
|
total: states.size,
|
|
114
146
|
succeeded: states.values.count { |state| state[:status] == :succeeded },
|
|
115
147
|
failed: states.values.count { |state| state[:status] == :failed },
|
|
148
|
+
pending: states.values.count { |state| state[:status] == :pending },
|
|
116
149
|
stale: states.values.count { |state| state[:status] == :stale },
|
|
117
150
|
failed_nodes: states.filter_map do |node_name, state|
|
|
118
151
|
next unless state[:status] == :failed
|
|
@@ -139,6 +172,7 @@ module Igniter
|
|
|
139
172
|
|
|
140
173
|
def format_nodes(nodes)
|
|
141
174
|
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, stale=#{nodes[:stale]}"
|
|
175
|
+
line = "Nodes: total=#{nodes[:total]}, succeeded=#{nodes[:succeeded]}, failed=#{nodes[:failed]}, pending=#{nodes[:pending]}, stale=#{nodes[:stale]}"
|
|
142
176
|
return line if nodes[:failed_nodes].empty?
|
|
143
177
|
|
|
144
178
|
failures = nodes[:failed_nodes].map { |node| "#{node[:node_name]}(#{node[:error]})" }.join(", ")
|
|
@@ -151,6 +185,19 @@ module Igniter
|
|
|
151
185
|
"Errors: #{errors.map { |error| "#{error[:node_name]}=#{error[:type]}" }.join(', ')}"
|
|
152
186
|
end
|
|
153
187
|
|
|
188
|
+
def format_collection_nodes(collection_nodes)
|
|
189
|
+
return nil if collection_nodes.empty?
|
|
190
|
+
|
|
191
|
+
summaries = collection_nodes.map do |node|
|
|
192
|
+
summary = "#{node[:node_name]} total=#{node[:total]} succeeded=#{node[:succeeded]} failed=#{node[:failed]} status=#{node[:status]}"
|
|
193
|
+
next summary if node[:failed_items].empty?
|
|
194
|
+
|
|
195
|
+
"#{summary} failed_items=#{node[:failed_items].map { |item| "#{item[:key]}(#{item[:message]})" }.join(', ')}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
"Collections: #{summaries.join('; ')}"
|
|
199
|
+
end
|
|
200
|
+
|
|
154
201
|
def format_events(events)
|
|
155
202
|
"Events: total=#{events[:total]}, latest=#{events[:latest_type] || 'none'}"
|
|
156
203
|
end
|
|
@@ -161,14 +208,43 @@ module Igniter
|
|
|
161
208
|
|
|
162
209
|
def serialize_value(value)
|
|
163
210
|
case value
|
|
211
|
+
when Runtime::DeferredResult
|
|
212
|
+
value.as_json
|
|
164
213
|
when Runtime::Result
|
|
165
214
|
value.to_h
|
|
215
|
+
when Runtime::CollectionResult
|
|
216
|
+
value.as_json
|
|
166
217
|
when Array
|
|
167
218
|
value.map { |item| serialize_value(item) }
|
|
168
219
|
else
|
|
169
220
|
value
|
|
170
221
|
end
|
|
171
222
|
end
|
|
223
|
+
|
|
224
|
+
def summarize_collection_nodes
|
|
225
|
+
execution.cache.values.filter_map do |state|
|
|
226
|
+
next unless state.value.is_a?(Runtime::CollectionResult)
|
|
227
|
+
|
|
228
|
+
result = state.value
|
|
229
|
+
{
|
|
230
|
+
node_name: state.node.name,
|
|
231
|
+
path: state.node.path,
|
|
232
|
+
mode: result.mode,
|
|
233
|
+
total: result.items.size,
|
|
234
|
+
succeeded: result.successes.size,
|
|
235
|
+
failed: result.failures.size,
|
|
236
|
+
status: result.failures.empty? ? :succeeded : :partial_failure,
|
|
237
|
+
failed_items: result.failures.values.map do |item|
|
|
238
|
+
{
|
|
239
|
+
key: item.key,
|
|
240
|
+
type: item.error.class.name,
|
|
241
|
+
message: item.error.message,
|
|
242
|
+
context: item.error.respond_to?(:context) ? item.error.context : {}
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
end
|
|
172
248
|
end
|
|
173
249
|
end
|
|
174
250
|
end
|