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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +238 -1
  5. data/docs/BACKLOG.md +166 -0
  6. data/docs/BRANCHES_V1.md +213 -0
  7. data/docs/COLLECTIONS_V1.md +303 -0
  8. data/docs/EXECUTION_MODEL_V2.md +79 -0
  9. data/docs/PATTERNS.md +222 -0
  10. data/docs/STORE_ADAPTERS.md +126 -0
  11. data/examples/README.md +124 -0
  12. data/examples/async_store.rb +47 -0
  13. data/examples/collection.rb +43 -0
  14. data/examples/collection_partial_failure.rb +50 -0
  15. data/examples/marketing_ergonomics.rb +57 -0
  16. data/examples/ringcentral_routing.rb +278 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +82 -0
  18. data/lib/igniter/compiler/graph_compiler.rb +12 -2
  19. data/lib/igniter/compiler/type_resolver.rb +54 -0
  20. data/lib/igniter/compiler/validation_context.rb +61 -0
  21. data/lib/igniter/compiler/validation_pipeline.rb +30 -0
  22. data/lib/igniter/compiler/validator.rb +1 -187
  23. data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
  24. data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
  25. data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
  26. data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
  27. data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
  28. data/lib/igniter/compiler.rb +8 -0
  29. data/lib/igniter/contract.rb +136 -4
  30. data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
  31. data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
  32. data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
  33. data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
  34. data/lib/igniter/diagnostics/report.rb +84 -8
  35. data/lib/igniter/dsl/contract_builder.rb +208 -5
  36. data/lib/igniter/dsl/schema_builder.rb +73 -0
  37. data/lib/igniter/dsl.rb +1 -0
  38. data/lib/igniter/errors.rb +11 -0
  39. data/lib/igniter/events/bus.rb +5 -0
  40. data/lib/igniter/events/event.rb +29 -0
  41. data/lib/igniter/executor.rb +74 -0
  42. data/lib/igniter/executor_registry.rb +44 -0
  43. data/lib/igniter/extensions/auditing/timeline.rb +4 -0
  44. data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
  45. data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
  46. data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
  47. data/lib/igniter/extensions/introspection.rb +1 -0
  48. data/lib/igniter/extensions/reactive/engine.rb +49 -2
  49. data/lib/igniter/extensions/reactive/reaction.rb +3 -2
  50. data/lib/igniter/model/branch_node.rb +40 -0
  51. data/lib/igniter/model/collection_node.rb +25 -0
  52. data/lib/igniter/model/composition_node.rb +2 -2
  53. data/lib/igniter/model/compute_node.rb +58 -2
  54. data/lib/igniter/model/input_node.rb +2 -2
  55. data/lib/igniter/model/output_node.rb +24 -4
  56. data/lib/igniter/model.rb +2 -0
  57. data/lib/igniter/runtime/cache.rb +64 -25
  58. data/lib/igniter/runtime/collection_result.rb +111 -0
  59. data/lib/igniter/runtime/deferred_result.rb +40 -0
  60. data/lib/igniter/runtime/execution.rb +261 -11
  61. data/lib/igniter/runtime/input_validator.rb +2 -24
  62. data/lib/igniter/runtime/invalidator.rb +1 -1
  63. data/lib/igniter/runtime/job_worker.rb +18 -0
  64. data/lib/igniter/runtime/node_state.rb +20 -0
  65. data/lib/igniter/runtime/planner.rb +126 -0
  66. data/lib/igniter/runtime/resolver.rb +269 -15
  67. data/lib/igniter/runtime/result.rb +14 -2
  68. data/lib/igniter/runtime/runner_factory.rb +20 -0
  69. data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
  70. data/lib/igniter/runtime/runners/store_runner.rb +29 -0
  71. data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
  72. data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
  73. data/lib/igniter/runtime/stores/file_store.rb +43 -0
  74. data/lib/igniter/runtime/stores/memory_store.rb +40 -0
  75. data/lib/igniter/runtime/stores/redis_store.rb +44 -0
  76. data/lib/igniter/runtime.rb +12 -0
  77. data/lib/igniter/type_system.rb +44 -0
  78. data/lib/igniter/version.rb +1 -1
  79. data/lib/igniter.rb +23 -0
  80. 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.fetch(node.name)
13
- return cached if cached && !cached.stale?
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
- @execution.events.emit(
30
- state.failed? ? :node_failed : :node_succeeded,
33
+ emit_resolution_event(node, state)
34
+ state
35
+ rescue PendingDependencyError => e
36
+ state = NodeState.new(
31
37
  node: node,
32
- status: state.status,
33
- payload: success_payload(node, state)
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
- dependency_state = resolve(dependency_name)
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
- dependency_state = resolve(dependency_name)
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 == :composition
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
@@ -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"