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
|
@@ -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
|
|
@@ -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
|