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
|
@@ -5,16 +5,36 @@ module Igniter
|
|
|
5
5
|
class OutputNode < Node
|
|
6
6
|
attr_reader :source
|
|
7
7
|
|
|
8
|
-
def initialize(id:, name:, source:, metadata: {})
|
|
8
|
+
def initialize(id:, name:, source:, path: nil, metadata: {})
|
|
9
|
+
normalized_source = source.to_s
|
|
10
|
+
|
|
9
11
|
super(
|
|
10
12
|
id: id,
|
|
11
13
|
kind: :output,
|
|
12
14
|
name: name,
|
|
13
|
-
path: "output.#{name}",
|
|
14
|
-
dependencies: [
|
|
15
|
+
path: (path || "output.#{name}"),
|
|
16
|
+
dependencies: [normalized_source.split(".").first],
|
|
15
17
|
metadata: metadata
|
|
16
18
|
)
|
|
17
|
-
@source =
|
|
19
|
+
@source = normalized_source.include?(".") ? normalized_source : normalized_source.to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def source_root
|
|
23
|
+
source.to_s.split(".").first.to_sym
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def composition_output?
|
|
27
|
+
source.to_s.include?(".")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def type
|
|
31
|
+
metadata[:type]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def child_output_name
|
|
35
|
+
return unless composition_output?
|
|
36
|
+
|
|
37
|
+
source.to_s.split(".", 2).last.to_sym
|
|
18
38
|
end
|
|
19
39
|
end
|
|
20
40
|
end
|
data/lib/igniter/model.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "model/graph"
|
|
|
5
5
|
require_relative "model/input_node"
|
|
6
6
|
require_relative "model/compute_node"
|
|
7
7
|
require_relative "model/composition_node"
|
|
8
|
+
require_relative "model/branch_node"
|
|
9
|
+
require_relative "model/collection_node"
|
|
8
10
|
require_relative "model/output_node"
|
|
9
11
|
|
|
10
12
|
module Igniter
|
|
@@ -5,47 +5,86 @@ module Igniter
|
|
|
5
5
|
class Cache
|
|
6
6
|
def initialize
|
|
7
7
|
@states = {}
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
@condition = ConditionVariable.new
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def fetch(node_name)
|
|
11
|
-
@states[node_name.to_sym]
|
|
13
|
+
@mutex.synchronize { @states[node_name.to_sym] }
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def write(state)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
node
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
current = @states[state.node.name]
|
|
19
|
+
version = state.version || (current&.running? ? current.version : next_version(current))
|
|
20
|
+
@states[state.node.name] = NodeState.new(
|
|
21
|
+
node: state.node,
|
|
22
|
+
status: state.status,
|
|
23
|
+
value: state.value,
|
|
24
|
+
error: state.error,
|
|
25
|
+
version: version,
|
|
26
|
+
resolved_at: state.resolved_at,
|
|
27
|
+
invalidated_by: state.invalidated_by
|
|
28
|
+
)
|
|
29
|
+
@condition.broadcast
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def begin_resolution(node)
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
loop do
|
|
36
|
+
current = @states[node.name]
|
|
37
|
+
return [:cached, current] if current && !current.stale? && !current.running?
|
|
38
|
+
|
|
39
|
+
unless current&.running?
|
|
40
|
+
@states[node.name] = NodeState.new(
|
|
41
|
+
node: node,
|
|
42
|
+
status: :running,
|
|
43
|
+
value: current&.value,
|
|
44
|
+
error: current&.error,
|
|
45
|
+
version: next_version(current),
|
|
46
|
+
resolved_at: current&.resolved_at || Time.now.utc,
|
|
47
|
+
invalidated_by: nil
|
|
48
|
+
)
|
|
49
|
+
return [:started, @states[node.name]]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@condition.wait(@mutex)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
26
55
|
end
|
|
27
56
|
|
|
28
57
|
def stale!(node, invalidated_by:)
|
|
29
|
-
|
|
30
|
-
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
current = @states[node.name]
|
|
60
|
+
return unless current
|
|
31
61
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
62
|
+
@states[node.name] = NodeState.new(
|
|
63
|
+
node: node,
|
|
64
|
+
status: :stale,
|
|
65
|
+
value: current.value,
|
|
66
|
+
error: current.error,
|
|
67
|
+
version: current.version + 1,
|
|
68
|
+
resolved_at: current.resolved_at,
|
|
69
|
+
invalidated_by: invalidated_by
|
|
70
|
+
)
|
|
71
|
+
@condition.broadcast
|
|
72
|
+
end
|
|
41
73
|
end
|
|
42
74
|
|
|
43
75
|
def values
|
|
44
|
-
@states.values
|
|
76
|
+
@mutex.synchronize { @states.values }
|
|
45
77
|
end
|
|
46
78
|
|
|
47
79
|
def to_h
|
|
48
|
-
@states.dup
|
|
80
|
+
@mutex.synchronize { @states.dup }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def restore!(states)
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
@states = states.transform_keys(&:to_sym)
|
|
86
|
+
@condition.broadcast
|
|
87
|
+
end
|
|
49
88
|
end
|
|
50
89
|
|
|
51
90
|
private
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
class CollectionResult
|
|
6
|
+
Item = Struct.new(:key, :status, :result, :error, keyword_init: true) do
|
|
7
|
+
def succeeded?
|
|
8
|
+
status == :succeeded
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def failed?
|
|
12
|
+
status == :failed
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
key: key,
|
|
18
|
+
status: status,
|
|
19
|
+
result: serialize_result(result),
|
|
20
|
+
error: serialize_error(error)
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def serialize_result(value)
|
|
27
|
+
case value
|
|
28
|
+
when Runtime::Result
|
|
29
|
+
value.to_h
|
|
30
|
+
else
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def serialize_error(value)
|
|
36
|
+
return nil unless value
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
type: value.class.name,
|
|
40
|
+
message: value.message,
|
|
41
|
+
context: value.respond_to?(:context) ? value.context : {}
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_reader :items, :mode
|
|
47
|
+
|
|
48
|
+
def initialize(items:, mode:)
|
|
49
|
+
@items = items.freeze
|
|
50
|
+
@mode = mode.to_sym
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def [](key)
|
|
54
|
+
items.fetch(key)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def keys
|
|
58
|
+
items.keys
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def successes
|
|
62
|
+
items.select { |_key, item| item.succeeded? }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def failures
|
|
66
|
+
items.select { |_key, item| item.failed? }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def items_summary
|
|
70
|
+
items.transform_values do |item|
|
|
71
|
+
{
|
|
72
|
+
status: item.status,
|
|
73
|
+
error: item.error&.message
|
|
74
|
+
}.compact
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def failed_items
|
|
79
|
+
failures.transform_values do |item|
|
|
80
|
+
{
|
|
81
|
+
type: item.error.class.name,
|
|
82
|
+
message: item.error.message,
|
|
83
|
+
context: item.error.respond_to?(:context) ? item.error.context : {}
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_h
|
|
89
|
+
items.transform_values(&:to_h)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def summary
|
|
93
|
+
{
|
|
94
|
+
mode: mode,
|
|
95
|
+
total: items.size,
|
|
96
|
+
succeeded: successes.size,
|
|
97
|
+
failed: failures.size,
|
|
98
|
+
status: failures.empty? ? :succeeded : :partial_failure
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def as_json(*)
|
|
103
|
+
{
|
|
104
|
+
mode: mode,
|
|
105
|
+
summary: summary,
|
|
106
|
+
items: to_h
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Runtime
|
|
7
|
+
class DeferredResult
|
|
8
|
+
attr_reader :token, :payload, :source_node, :waiting_on
|
|
9
|
+
|
|
10
|
+
def initialize(token:, payload: {}, source_node: nil, waiting_on: nil)
|
|
11
|
+
@token = token
|
|
12
|
+
@payload = payload.freeze
|
|
13
|
+
@source_node = source_node&.to_sym
|
|
14
|
+
@waiting_on = waiting_on&.to_sym
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.build(token: nil, payload: {}, source_node: nil, waiting_on: nil)
|
|
18
|
+
new(
|
|
19
|
+
token: token || SecureRandom.uuid,
|
|
20
|
+
payload: payload,
|
|
21
|
+
source_node: source_node,
|
|
22
|
+
waiting_on: waiting_on
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
token: token,
|
|
29
|
+
payload: payload,
|
|
30
|
+
source_node: source_node,
|
|
31
|
+
waiting_on: waiting_on
|
|
32
|
+
}.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def as_json(*)
|
|
36
|
+
to_h
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
module Igniter
|
|
4
4
|
module Runtime
|
|
5
5
|
class Execution
|
|
6
|
-
attr_reader :compiled_graph, :contract_instance, :inputs, :cache, :events, :audit
|
|
6
|
+
attr_reader :compiled_graph, :contract_instance, :inputs, :cache, :events, :audit, :runner_strategy, :max_workers, :store
|
|
7
7
|
|
|
8
|
-
def initialize(compiled_graph:, contract_instance:, inputs:)
|
|
8
|
+
def initialize(compiled_graph:, contract_instance:, inputs:, runner: :inline, max_workers: nil, store: nil)
|
|
9
9
|
@compiled_graph = compiled_graph
|
|
10
10
|
@contract_instance = contract_instance
|
|
11
|
+
@runner_strategy = runner
|
|
12
|
+
@max_workers = max_workers
|
|
13
|
+
@store = store
|
|
11
14
|
@input_validator = InputValidator.new(compiled_graph)
|
|
12
15
|
@inputs = @input_validator.normalize_initial_inputs(inputs)
|
|
13
16
|
@cache = Cache.new
|
|
@@ -15,16 +18,16 @@ module Igniter
|
|
|
15
18
|
@audit = Extensions::Auditing::Timeline.new(self)
|
|
16
19
|
@events.subscribe(@audit)
|
|
17
20
|
@resolver = Resolver.new(self)
|
|
21
|
+
@planner = Planner.new(self)
|
|
22
|
+
@runner = RunnerFactory.build(@runner_strategy, self, resolver: @resolver, max_workers: @max_workers, store: @store)
|
|
18
23
|
@invalidator = Invalidator.new(self)
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def resolve_output(name)
|
|
22
27
|
output = compiled_graph.fetch_output(name)
|
|
23
|
-
with_execution_lifecycle([output.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
state.value
|
|
28
|
+
with_execution_lifecycle([output.source_root]) do
|
|
29
|
+
run_targets([output.source_root])
|
|
30
|
+
resolve_exported_output(output)
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -33,10 +36,11 @@ module Igniter
|
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def resolve_all
|
|
36
|
-
output_sources =
|
|
39
|
+
output_sources = @planner.targets_for_outputs
|
|
37
40
|
|
|
38
41
|
with_execution_lifecycle(output_sources) do
|
|
39
|
-
|
|
42
|
+
run_targets(output_sources)
|
|
43
|
+
compiled_graph.outputs.each { |output_node| resolve_output_value(output_node) }
|
|
40
44
|
self
|
|
41
45
|
end
|
|
42
46
|
end
|
|
@@ -55,9 +59,28 @@ module Igniter
|
|
|
55
59
|
self
|
|
56
60
|
end
|
|
57
61
|
|
|
62
|
+
def resume(node_name, value:)
|
|
63
|
+
node = compiled_graph.fetch_node(node_name)
|
|
64
|
+
current = cache.fetch(node.name)
|
|
65
|
+
raise ResolutionError, "Node '#{node_name}' is not pending" unless current&.pending?
|
|
66
|
+
|
|
67
|
+
cache.write(NodeState.new(node: node, status: :succeeded, value: value))
|
|
68
|
+
@events.emit(:node_resumed, node: node, status: :succeeded, payload: { resumed: true })
|
|
69
|
+
@invalidator.invalidate_from(node.name)
|
|
70
|
+
persist_runtime_state!
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def resume_by_token(token, value:)
|
|
75
|
+
node_name = pending_node_name_for_token(token)
|
|
76
|
+
raise ResolutionError, "No pending node found for token '#{token}'" unless node_name
|
|
77
|
+
|
|
78
|
+
resume(node_name, value: value)
|
|
79
|
+
end
|
|
80
|
+
|
|
58
81
|
def success?
|
|
59
82
|
resolve_all
|
|
60
|
-
!
|
|
83
|
+
!failed? && !pending?
|
|
61
84
|
end
|
|
62
85
|
|
|
63
86
|
def failed?
|
|
@@ -65,6 +88,11 @@ module Igniter
|
|
|
65
88
|
cache.values.any?(&:failed?)
|
|
66
89
|
end
|
|
67
90
|
|
|
91
|
+
def pending?
|
|
92
|
+
resolve_all
|
|
93
|
+
cache.values.any?(&:pending?)
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
def states
|
|
69
97
|
Extensions::Introspection::RuntimeFormatter.states(self)
|
|
70
98
|
end
|
|
@@ -77,13 +105,25 @@ module Igniter
|
|
|
77
105
|
Diagnostics::Report.new(self)
|
|
78
106
|
end
|
|
79
107
|
|
|
108
|
+
def plan(output_names = nil)
|
|
109
|
+
@planner.plan(output_names)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def explain_plan(output_names = nil)
|
|
113
|
+
Extensions::Introspection::PlanFormatter.to_text(self, output_names)
|
|
114
|
+
end
|
|
115
|
+
|
|
80
116
|
def to_h
|
|
81
117
|
{
|
|
82
118
|
graph: compiled_graph.name,
|
|
83
119
|
execution_id: events.execution_id,
|
|
84
120
|
inputs: inputs.dup,
|
|
85
|
-
|
|
121
|
+
runner: runner_strategy,
|
|
122
|
+
max_workers: max_workers,
|
|
123
|
+
success: success?,
|
|
86
124
|
failed: cache.values.any?(&:failed?),
|
|
125
|
+
pending: cache.values.any?(&:pending?),
|
|
126
|
+
plan: plan,
|
|
87
127
|
states: states,
|
|
88
128
|
event_count: events.events.size
|
|
89
129
|
}
|
|
@@ -95,6 +135,28 @@ module Igniter
|
|
|
95
135
|
)
|
|
96
136
|
end
|
|
97
137
|
|
|
138
|
+
def snapshot(include_resolution: true)
|
|
139
|
+
resolve_pending_safe if include_resolution
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
graph: compiled_graph.name,
|
|
143
|
+
execution_id: events.execution_id,
|
|
144
|
+
runner: runner_strategy,
|
|
145
|
+
max_workers: max_workers,
|
|
146
|
+
inputs: inputs.dup,
|
|
147
|
+
states: serialize_states,
|
|
148
|
+
events: events.events.map(&:as_json)
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def restore!(snapshot)
|
|
153
|
+
@inputs.replace(symbolize_keys(value_from(snapshot, :inputs) || {}))
|
|
154
|
+
cache.restore!(deserialize_states(value_from(snapshot, :states) || {}))
|
|
155
|
+
events.restore!(events: value_from(snapshot, :events) || [], execution_id: value_from(snapshot, :execution_id))
|
|
156
|
+
audit.restore!(events.events)
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
98
160
|
private
|
|
99
161
|
|
|
100
162
|
def with_execution_lifecycle(node_names)
|
|
@@ -102,6 +164,7 @@ module Igniter
|
|
|
102
164
|
@events.emit(:execution_started, payload: { graph: compiled_graph.name, targets: node_names.map(&:to_sym) })
|
|
103
165
|
begin
|
|
104
166
|
result = yield
|
|
167
|
+
persist_runtime_state!
|
|
105
168
|
@events.emit(:execution_finished, payload: { graph: compiled_graph.name, targets: node_names.map(&:to_sym) })
|
|
106
169
|
result
|
|
107
170
|
rescue StandardError => e
|
|
@@ -114,6 +177,7 @@ module Igniter
|
|
|
114
177
|
error: e.message
|
|
115
178
|
}
|
|
116
179
|
)
|
|
180
|
+
persist_runtime_state!
|
|
117
181
|
raise
|
|
118
182
|
end
|
|
119
183
|
else
|
|
@@ -137,6 +201,192 @@ module Igniter
|
|
|
137
201
|
def fetch_input!(name)
|
|
138
202
|
@input_validator.fetch_value!(name, @inputs)
|
|
139
203
|
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
def resolve_exported_output(output)
|
|
208
|
+
state = @resolver.resolve(output.source_root)
|
|
209
|
+
raise state.error if state.failed?
|
|
210
|
+
return state.value if state.pending?
|
|
211
|
+
|
|
212
|
+
return state.value unless output.composition_output?
|
|
213
|
+
|
|
214
|
+
state.value.public_send(output.child_output_name)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def run_targets(node_names)
|
|
218
|
+
@runner.run(node_names)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def persist_runtime_state!
|
|
222
|
+
return unless @runner.respond_to?(:persist!)
|
|
223
|
+
|
|
224
|
+
@runner.persist!
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def pending_node_name_for_token(token)
|
|
228
|
+
source_match = cache.values.find do |state|
|
|
229
|
+
state.pending? &&
|
|
230
|
+
state.value.is_a?(Runtime::DeferredResult) &&
|
|
231
|
+
state.value.token == token &&
|
|
232
|
+
state.value.source_node == state.node.name
|
|
233
|
+
end
|
|
234
|
+
return source_match.node.name if source_match
|
|
235
|
+
|
|
236
|
+
cache.values.find do |state|
|
|
237
|
+
state.pending? &&
|
|
238
|
+
state.value.is_a?(Runtime::DeferredResult) &&
|
|
239
|
+
state.value.token == token
|
|
240
|
+
end&.node&.name
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def resolve_pending_safe
|
|
244
|
+
resolve_all
|
|
245
|
+
rescue Igniter::Error
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def serialize_states
|
|
250
|
+
cache.to_h.each_with_object({}) do |(node_name, state), memo|
|
|
251
|
+
memo[node_name] = {
|
|
252
|
+
status: state.status,
|
|
253
|
+
version: state.version,
|
|
254
|
+
resolved_at: state.resolved_at&.iso8601,
|
|
255
|
+
invalidated_by: state.invalidated_by,
|
|
256
|
+
value: serialize_state_value(state.value),
|
|
257
|
+
error: serialize_state_error(state.error)
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def deserialize_states(snapshot_states)
|
|
263
|
+
snapshot_states.each_with_object({}) do |(node_name, state_data), memo|
|
|
264
|
+
node = compiled_graph.fetch_node(node_name)
|
|
265
|
+
memo[node.name] = NodeState.new(
|
|
266
|
+
node: node,
|
|
267
|
+
status: (state_data[:status] || state_data["status"]).to_sym,
|
|
268
|
+
value: deserialize_state_value(node, state_data[:value] || state_data["value"]),
|
|
269
|
+
error: deserialize_state_error(state_data[:error] || state_data["error"]),
|
|
270
|
+
version: state_data[:version] || state_data["version"],
|
|
271
|
+
resolved_at: deserialize_time(state_data[:resolved_at] || state_data["resolved_at"]),
|
|
272
|
+
invalidated_by: (state_data[:invalidated_by] || state_data["invalidated_by"])&.to_sym
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def serialize_state_value(value)
|
|
278
|
+
case value
|
|
279
|
+
when Runtime::DeferredResult
|
|
280
|
+
{ type: :deferred, data: value.as_json }
|
|
281
|
+
when Runtime::Result
|
|
282
|
+
{
|
|
283
|
+
type: :result_snapshot,
|
|
284
|
+
snapshot: value.execution.snapshot(include_resolution: false)
|
|
285
|
+
}
|
|
286
|
+
when Runtime::CollectionResult
|
|
287
|
+
{
|
|
288
|
+
type: :collection_result,
|
|
289
|
+
mode: value.mode,
|
|
290
|
+
items: value.items.transform_values do |item|
|
|
291
|
+
{
|
|
292
|
+
key: item.key,
|
|
293
|
+
status: item.status,
|
|
294
|
+
result: serialize_state_value(item.result),
|
|
295
|
+
error: serialize_state_error(item.error)
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
}
|
|
299
|
+
else
|
|
300
|
+
value
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def deserialize_state_value(node, value)
|
|
305
|
+
if value.is_a?(Hash) && (value[:type] || value["type"])&.to_sym == :deferred
|
|
306
|
+
data = value[:data] || value["data"] || {}
|
|
307
|
+
return Runtime::DeferredResult.build(
|
|
308
|
+
token: data[:token] || data["token"],
|
|
309
|
+
payload: data[:payload] || data["payload"] || {},
|
|
310
|
+
source_node: data[:source_node] || data["source_node"],
|
|
311
|
+
waiting_on: data[:waiting_on] || data["waiting_on"]
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
if value.is_a?(Hash) && (value[:type] || value["type"])&.to_sym == :result_snapshot
|
|
316
|
+
snapshot = value[:snapshot] || value["snapshot"] || {}
|
|
317
|
+
if node.kind == :composition
|
|
318
|
+
child_contract = node.contract_class.restore(snapshot)
|
|
319
|
+
return child_contract.result
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
if node.kind == :branch
|
|
323
|
+
snapshot_graph = snapshot[:graph] || snapshot["graph"]
|
|
324
|
+
contract_class = node.possible_contracts.find { |candidate| candidate.compiled_graph.name == snapshot_graph }
|
|
325
|
+
return value unless contract_class
|
|
326
|
+
|
|
327
|
+
child_contract = contract_class.restore(snapshot)
|
|
328
|
+
return child_contract.result
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if node.kind == :collection
|
|
332
|
+
child_contract = node.contract_class.restore(snapshot)
|
|
333
|
+
return child_contract.result
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
if value.is_a?(Hash) && (value[:type] || value["type"])&.to_sym == :collection_result
|
|
338
|
+
items = (value[:items] || value["items"] || {}).each_with_object({}) do |(key, item), memo|
|
|
339
|
+
memo[key.is_a?(String) && key.match?(/\A\d+\z/) ? key.to_i : key] = Runtime::CollectionResult::Item.new(
|
|
340
|
+
key: item[:key] || item["key"] || key,
|
|
341
|
+
status: (item[:status] || item["status"]).to_sym,
|
|
342
|
+
result: deserialize_state_value(node, item[:result] || item["result"]),
|
|
343
|
+
error: deserialize_state_error(item[:error] || item["error"])
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
return Runtime::CollectionResult.new(
|
|
347
|
+
items: items,
|
|
348
|
+
mode: (value[:mode] || value["mode"] || :collect).to_sym
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
value
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def serialize_state_error(error)
|
|
356
|
+
return nil unless error
|
|
357
|
+
|
|
358
|
+
{
|
|
359
|
+
type: error.class.name,
|
|
360
|
+
message: error.message,
|
|
361
|
+
context: error.respond_to?(:context) ? error.context : {}
|
|
362
|
+
}
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def deserialize_state_error(error_data)
|
|
366
|
+
return nil unless error_data
|
|
367
|
+
|
|
368
|
+
ResolutionError.new(
|
|
369
|
+
error_data[:message] || error_data["message"],
|
|
370
|
+
context: error_data[:context] || error_data["context"] || {}
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def deserialize_time(value)
|
|
375
|
+
case value
|
|
376
|
+
when Time
|
|
377
|
+
value
|
|
378
|
+
when String
|
|
379
|
+
Time.iso8601(value)
|
|
380
|
+
else
|
|
381
|
+
value || Time.now.utc
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def value_from(data, key)
|
|
386
|
+
data[key] || data[key.to_s]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
alias_method :resolve_output_value, :resolve_exported_output
|
|
140
390
|
end
|
|
141
391
|
end
|
|
142
392
|
end
|