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
|
@@ -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,162 @@ 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
|
+
child_inputs = node.input_mapping.each_with_object({}) do |(child_input_name, dependency_name), memo|
|
|
127
|
+
memo[child_input_name] = resolve_dependency_value(dependency_name)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
@execution.events.emit(
|
|
131
|
+
:branch_selected,
|
|
132
|
+
node: node,
|
|
133
|
+
status: :succeeded,
|
|
134
|
+
payload: {
|
|
135
|
+
selector: node.selector_dependency,
|
|
136
|
+
selector_value: selector_value,
|
|
137
|
+
matched_case: matched_case,
|
|
138
|
+
selected_contract: selected_contract.name || "AnonymousContract"
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
child_contract = selected_contract.new(child_inputs)
|
|
143
|
+
child_contract.resolve_all
|
|
144
|
+
child_error = child_contract.result.errors.values.first
|
|
145
|
+
raise child_error if child_error
|
|
146
|
+
|
|
147
|
+
NodeState.new(node: node, status: :succeeded, value: child_contract.result)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def resolve_collection(node)
|
|
151
|
+
items = resolve_dependency_value(node.source_dependency)
|
|
152
|
+
normalized_items = normalize_collection_items(node, items)
|
|
153
|
+
collection_items = {}
|
|
154
|
+
|
|
155
|
+
normalized_items.each do |item_inputs|
|
|
156
|
+
item_key = extract_collection_key(node, item_inputs)
|
|
157
|
+
emit_collection_item_event(:collection_item_started, node, item_key, item_inputs: item_inputs)
|
|
158
|
+
child_contract = node.contract_class.new(item_inputs)
|
|
159
|
+
begin
|
|
160
|
+
child_contract.resolve_all
|
|
161
|
+
rescue Igniter::Error
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
child_error = child_contract.execution.cache.values.find(&:failed?)&.error
|
|
165
|
+
|
|
166
|
+
if child_error
|
|
167
|
+
collection_items[item_key] = Runtime::CollectionResult::Item.new(
|
|
168
|
+
key: item_key,
|
|
169
|
+
status: :failed,
|
|
170
|
+
error: child_error
|
|
171
|
+
)
|
|
172
|
+
emit_collection_item_event(
|
|
173
|
+
:collection_item_failed,
|
|
174
|
+
node,
|
|
175
|
+
item_key,
|
|
176
|
+
error: child_error.message,
|
|
177
|
+
error_type: child_error.class.name,
|
|
178
|
+
child_execution_id: child_contract.execution.events.execution_id
|
|
179
|
+
)
|
|
180
|
+
raise child_error if node.mode == :fail_fast
|
|
181
|
+
else
|
|
182
|
+
collection_items[item_key] = Runtime::CollectionResult::Item.new(
|
|
183
|
+
key: item_key,
|
|
184
|
+
status: :succeeded,
|
|
185
|
+
result: child_contract.result
|
|
186
|
+
)
|
|
187
|
+
emit_collection_item_event(
|
|
188
|
+
:collection_item_succeeded,
|
|
189
|
+
node,
|
|
190
|
+
item_key,
|
|
191
|
+
child_execution_id: child_contract.execution.events.execution_id
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
NodeState.new(
|
|
197
|
+
node: node,
|
|
198
|
+
status: :succeeded,
|
|
199
|
+
value: Runtime::CollectionResult.new(items: collection_items, mode: node.mode)
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def resolve_dependency_value(dependency_name)
|
|
204
|
+
if @execution.compiled_graph.node?(dependency_name)
|
|
205
|
+
dependency_state = resolve(dependency_name)
|
|
206
|
+
raise dependency_state.error if dependency_state.failed?
|
|
207
|
+
raise PendingDependencyError.new(dependency_state.value, context: pending_context(dependency_state.node)) if dependency_state.pending?
|
|
208
|
+
|
|
209
|
+
dependency_state.value
|
|
210
|
+
elsif @execution.compiled_graph.outputs_by_name.key?(dependency_name.to_sym)
|
|
211
|
+
output = @execution.compiled_graph.fetch_output(dependency_name)
|
|
212
|
+
value = @execution.send(:resolve_exported_output, output)
|
|
213
|
+
raise PendingDependencyError.new(value) if deferred_result?(value)
|
|
214
|
+
|
|
215
|
+
value
|
|
216
|
+
else
|
|
217
|
+
raise ResolutionError, "Unknown dependency: #{dependency_name}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def deferred_result?(value)
|
|
222
|
+
value.is_a?(Runtime::DeferredResult)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def normalize_deferred_result(value, node)
|
|
226
|
+
Runtime::DeferredResult.build(
|
|
227
|
+
token: value.token,
|
|
228
|
+
payload: value.payload,
|
|
229
|
+
source_node: value.source_node || node.name,
|
|
230
|
+
waiting_on: value.waiting_on
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def emit_resolution_event(node, state)
|
|
235
|
+
event_type =
|
|
236
|
+
if state.failed?
|
|
237
|
+
:node_failed
|
|
238
|
+
elsif state.pending?
|
|
239
|
+
:node_pending
|
|
240
|
+
else
|
|
241
|
+
:node_succeeded
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
payload = state.pending? ? pending_payload(state) : success_payload(node, state)
|
|
245
|
+
@execution.events.emit(event_type, node: node, status: state.status, payload: payload)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def emit_collection_item_event(type, node, item_key, payload = {})
|
|
249
|
+
@execution.events.emit(
|
|
250
|
+
type,
|
|
251
|
+
node: node,
|
|
252
|
+
payload: payload.merge(item_key: item_key)
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def pending_payload(state)
|
|
257
|
+
return {} unless state.value.is_a?(Runtime::DeferredResult)
|
|
258
|
+
|
|
259
|
+
state.value.to_h
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def pending_context(node)
|
|
263
|
+
{
|
|
264
|
+
graph: @execution.compiled_graph.name,
|
|
265
|
+
node_id: node.id,
|
|
266
|
+
node_name: node.name,
|
|
267
|
+
node_path: node.path,
|
|
268
|
+
source_location: node.source_location
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
88
272
|
def success_payload(node, state)
|
|
89
|
-
return {} unless node.kind
|
|
273
|
+
return {} unless %i[composition branch].include?(node.kind)
|
|
90
274
|
return {} unless state.value.is_a?(Igniter::Runtime::Result)
|
|
91
275
|
|
|
92
276
|
{
|
|
@@ -109,6 +293,76 @@ module Igniter
|
|
|
109
293
|
}
|
|
110
294
|
)
|
|
111
295
|
end
|
|
296
|
+
|
|
297
|
+
def normalize_guard_value(node, value)
|
|
298
|
+
return value unless node.respond_to?(:guard?) && node.guard?
|
|
299
|
+
return true if value
|
|
300
|
+
|
|
301
|
+
raise ResolutionError.new(
|
|
302
|
+
node.metadata[:guard_message] || "Guard '#{node.name}' failed",
|
|
303
|
+
context: {
|
|
304
|
+
graph: @execution.compiled_graph.name,
|
|
305
|
+
node_id: node.id,
|
|
306
|
+
node_name: node.name,
|
|
307
|
+
node_path: node.path,
|
|
308
|
+
source_location: node.source_location
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def normalize_collection_items(node, items)
|
|
314
|
+
unless items.is_a?(Array)
|
|
315
|
+
raise CollectionInputError.new(
|
|
316
|
+
"Collection '#{node.name}' expects an array, got #{items.class}",
|
|
317
|
+
context: collection_context(node)
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
items.each do |item|
|
|
322
|
+
next if item.is_a?(Hash)
|
|
323
|
+
|
|
324
|
+
raise CollectionInputError.new(
|
|
325
|
+
"Collection '#{node.name}' expects item hashes, got #{item.class}",
|
|
326
|
+
context: collection_context(node)
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
ensure_unique_collection_keys!(node, items)
|
|
331
|
+
items.map { |item| item.transform_keys(&:to_sym) }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def extract_collection_key(node, item_inputs)
|
|
335
|
+
item_inputs.fetch(node.key_name)
|
|
336
|
+
rescue KeyError
|
|
337
|
+
raise CollectionKeyError.new(
|
|
338
|
+
"Collection '#{node.name}' item is missing key '#{node.key_name}'",
|
|
339
|
+
context: collection_context(node)
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def ensure_unique_collection_keys!(node, items)
|
|
344
|
+
keys = items.map do |item|
|
|
345
|
+
item.fetch(node.key_name) { raise CollectionKeyError.new("Collection '#{node.name}' item is missing key '#{node.key_name}'", context: collection_context(node)) }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
duplicates = keys.group_by(&:itself).select { |_key, entries| entries.size > 1 }.keys
|
|
349
|
+
return if duplicates.empty?
|
|
350
|
+
|
|
351
|
+
raise CollectionKeyError.new(
|
|
352
|
+
"Collection '#{node.name}' has duplicate keys: #{duplicates.join(', ')}",
|
|
353
|
+
context: collection_context(node)
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def collection_context(node)
|
|
358
|
+
{
|
|
359
|
+
graph: @execution.compiled_graph.name,
|
|
360
|
+
node_id: node.id,
|
|
361
|
+
node_name: node.name,
|
|
362
|
+
node_path: node.path,
|
|
363
|
+
source_location: node.source_location
|
|
364
|
+
}
|
|
365
|
+
end
|
|
112
366
|
end
|
|
113
367
|
end
|
|
114
368
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
module Runners
|
|
6
|
+
class InlineRunner
|
|
7
|
+
def initialize(execution, resolver:, max_workers: nil)
|
|
8
|
+
@execution = execution
|
|
9
|
+
@resolver = resolver
|
|
10
|
+
@max_workers = max_workers
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(node_names)
|
|
14
|
+
Array(node_names).each do |node_name|
|
|
15
|
+
@resolver.resolve(node_name)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
module Runners
|
|
6
|
+
class StoreRunner
|
|
7
|
+
def initialize(execution, resolver:, store:, max_workers: nil)
|
|
8
|
+
@execution = execution
|
|
9
|
+
@delegate = InlineRunner.new(execution, resolver: resolver, max_workers: max_workers)
|
|
10
|
+
@store = store
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(node_names)
|
|
14
|
+
@delegate.run(node_names)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def persist!
|
|
18
|
+
return unless @store
|
|
19
|
+
|
|
20
|
+
if @execution.pending?
|
|
21
|
+
@store.save(@execution.snapshot(include_resolution: false))
|
|
22
|
+
else
|
|
23
|
+
@store.delete(@execution.events.execution_id)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Runtime
|
|
7
|
+
module Runners
|
|
8
|
+
class ThreadPoolRunner
|
|
9
|
+
def initialize(execution, resolver:, max_workers: nil)
|
|
10
|
+
@execution = execution
|
|
11
|
+
@resolver = resolver
|
|
12
|
+
@max_workers = [Integer(max_workers || 4), 1].max
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(node_names)
|
|
16
|
+
queue = Queue.new
|
|
17
|
+
Array(node_names).each { |node_name| queue << node_name }
|
|
18
|
+
worker_count = [@max_workers, queue.size].min
|
|
19
|
+
return if worker_count.zero?
|
|
20
|
+
|
|
21
|
+
threads = worker_count.times.map do
|
|
22
|
+
Thread.new do
|
|
23
|
+
loop do
|
|
24
|
+
node_name = queue.pop(true)
|
|
25
|
+
@resolver.resolve(node_name)
|
|
26
|
+
rescue ThreadError
|
|
27
|
+
break
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
threads.each(&:join)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Runtime
|
|
7
|
+
module Stores
|
|
8
|
+
class ActiveRecordStore
|
|
9
|
+
def initialize(record_class:, execution_id_column: :execution_id, snapshot_column: :snapshot_json)
|
|
10
|
+
@record_class = record_class
|
|
11
|
+
@execution_id_column = execution_id_column.to_sym
|
|
12
|
+
@snapshot_column = snapshot_column.to_sym
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def save(snapshot)
|
|
16
|
+
execution_id = snapshot[:execution_id] || snapshot["execution_id"]
|
|
17
|
+
record = @record_class.find_or_initialize_by(@execution_id_column => execution_id)
|
|
18
|
+
record.public_send(:"#{@snapshot_column}=", JSON.generate(snapshot))
|
|
19
|
+
record.save!
|
|
20
|
+
execution_id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch(execution_id)
|
|
24
|
+
record = @record_class.find_by(@execution_id_column => execution_id)
|
|
25
|
+
raise Igniter::ResolutionError, "No execution snapshot found for '#{execution_id}'" unless record
|
|
26
|
+
|
|
27
|
+
JSON.parse(record.public_send(@snapshot_column))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete(execution_id)
|
|
31
|
+
record = @record_class.find_by(@execution_id_column => execution_id)
|
|
32
|
+
record&.destroy!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def exist?(execution_id)
|
|
36
|
+
!!@record_class.find_by(@execution_id_column => execution_id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Igniter
|
|
7
|
+
module Runtime
|
|
8
|
+
module Stores
|
|
9
|
+
class FileStore
|
|
10
|
+
def initialize(root:)
|
|
11
|
+
@root = root
|
|
12
|
+
FileUtils.mkdir_p(@root)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def save(snapshot)
|
|
16
|
+
execution_id = snapshot[:execution_id] || snapshot["execution_id"]
|
|
17
|
+
File.write(path_for(execution_id), JSON.pretty_generate(snapshot))
|
|
18
|
+
execution_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(execution_id)
|
|
22
|
+
JSON.parse(File.read(path_for(execution_id)))
|
|
23
|
+
rescue Errno::ENOENT
|
|
24
|
+
raise Igniter::ResolutionError, "No execution snapshot found for '#{execution_id}'"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete(execution_id)
|
|
28
|
+
FileUtils.rm_f(path_for(execution_id))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def exist?(execution_id)
|
|
32
|
+
File.exist?(path_for(execution_id))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def path_for(execution_id)
|
|
38
|
+
File.join(@root, "#{execution_id}.json")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Runtime
|
|
5
|
+
module Stores
|
|
6
|
+
class MemoryStore
|
|
7
|
+
def initialize
|
|
8
|
+
@snapshots = {}
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def save(snapshot)
|
|
13
|
+
execution_id = snapshot[:execution_id] || snapshot["execution_id"]
|
|
14
|
+
@mutex.synchronize { @snapshots[execution_id] = deep_copy(snapshot) }
|
|
15
|
+
execution_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fetch(execution_id)
|
|
19
|
+
@mutex.synchronize { deep_copy(@snapshots.fetch(execution_id)) }
|
|
20
|
+
rescue KeyError
|
|
21
|
+
raise Igniter::ResolutionError, "No execution snapshot found for '#{execution_id}'"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(execution_id)
|
|
25
|
+
@mutex.synchronize { @snapshots.delete(execution_id) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exist?(execution_id)
|
|
29
|
+
@mutex.synchronize { @snapshots.key?(execution_id) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def deep_copy(value)
|
|
35
|
+
Marshal.load(Marshal.dump(value))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Runtime
|
|
7
|
+
module Stores
|
|
8
|
+
class RedisStore
|
|
9
|
+
def initialize(redis:, namespace: "igniter:executions")
|
|
10
|
+
@redis = redis
|
|
11
|
+
@namespace = namespace
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def save(snapshot)
|
|
15
|
+
execution_id = snapshot[:execution_id] || snapshot["execution_id"]
|
|
16
|
+
@redis.set(redis_key(execution_id), JSON.generate(snapshot))
|
|
17
|
+
execution_id
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch(execution_id)
|
|
21
|
+
payload = @redis.get(redis_key(execution_id))
|
|
22
|
+
raise Igniter::ResolutionError, "No execution snapshot found for '#{execution_id}'" unless payload
|
|
23
|
+
|
|
24
|
+
JSON.parse(payload)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete(execution_id)
|
|
28
|
+
@redis.del(redis_key(execution_id))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def exist?(execution_id)
|
|
32
|
+
result = @redis.exists?(redis_key(execution_id))
|
|
33
|
+
result == true || result.to_i.positive?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def redis_key(execution_id)
|
|
39
|
+
"#{@namespace}:#{execution_id}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/igniter/runtime.rb
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "runtime/node_state"
|
|
4
4
|
require_relative "runtime/cache"
|
|
5
|
+
require_relative "runtime/deferred_result"
|
|
6
|
+
require_relative "runtime/collection_result"
|
|
5
7
|
require_relative "runtime/input_validator"
|
|
8
|
+
require_relative "runtime/planner"
|
|
9
|
+
require_relative "runtime/job_worker"
|
|
10
|
+
require_relative "runtime/runners/inline_runner"
|
|
11
|
+
require_relative "runtime/runners/store_runner"
|
|
12
|
+
require_relative "runtime/runners/thread_pool_runner"
|
|
13
|
+
require_relative "runtime/stores/active_record_store"
|
|
14
|
+
require_relative "runtime/stores/file_store"
|
|
15
|
+
require_relative "runtime/stores/memory_store"
|
|
16
|
+
require_relative "runtime/stores/redis_store"
|
|
17
|
+
require_relative "runtime/runner_factory"
|
|
6
18
|
require_relative "runtime/resolver"
|
|
7
19
|
require_relative "runtime/invalidator"
|
|
8
20
|
require_relative "runtime/result"
|