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
@@ -24,9 +24,9 @@ module Igniter
24
24
 
25
25
  def explain_output(output_name)
26
26
  output = @execution.compiled_graph.fetch_output(output_name)
27
- source = @execution.compiled_graph.fetch_node(output.source)
27
+ source = @execution.compiled_graph.fetch_node(output.source_root)
28
28
 
29
- {
29
+ explanation = {
30
30
  output_id: output.id,
31
31
  output: output.name,
32
32
  path: output.path,
@@ -35,6 +35,13 @@ module Igniter
35
35
  source_path: source.path,
36
36
  dependencies: dependency_tree(source)
37
37
  }
38
+
39
+ if output.composition_output?
40
+ explanation[:child_output] = output.child_output_name
41
+ explanation[:child_output_path] = output.source.to_s
42
+ end
43
+
44
+ explanation
38
45
  end
39
46
 
40
47
  private
@@ -52,7 +59,7 @@ module Igniter
52
59
  value: serialize_value(state&.value),
53
60
  error: state&.error&.message,
54
61
  dependencies: node.dependencies.map do |dependency_name|
55
- dependency_tree(@execution.compiled_graph.fetch_node(dependency_name))
62
+ dependency_tree(@execution.compiled_graph.fetch_dependency(dependency_name))
56
63
  end
57
64
  }
58
65
  end
@@ -84,6 +91,14 @@ module Igniter
84
91
 
85
92
  def serialize_value(value)
86
93
  case value
94
+ when Igniter::Runtime::DeferredResult
95
+ {
96
+ type: :deferred,
97
+ token: value.token,
98
+ payload: value.payload,
99
+ source_node: value.source_node,
100
+ waiting_on: value.waiting_on
101
+ }
87
102
  when Igniter::Runtime::Result
88
103
  {
89
104
  type: :result,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "introspection/graph_formatter"
4
+ require_relative "introspection/plan_formatter"
4
5
  require_relative "introspection/runtime_formatter"
5
6
 
6
7
  module Igniter
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module Igniter
4
6
  module Extensions
5
7
  module Reactive
@@ -11,16 +13,21 @@ module Igniter
11
13
  @contract = contract
12
14
  @reactions = reactions
13
15
  @errors = []
16
+ @fired_reactions = Set.new
14
17
  end
15
18
 
16
19
  def call(event)
17
20
  reactions.each do |reaction|
18
21
  next unless Matcher.new(reaction, event).match?
22
+ next if already_fired?(reaction, event)
19
23
 
20
- reaction.action.call(
24
+ mark_fired(reaction, event)
25
+ call_action(
26
+ reaction.action,
21
27
  event: event,
22
28
  contract: contract,
23
- execution: execution
29
+ execution: execution,
30
+ value: value_for(event)
24
31
  )
25
32
  rescue StandardError => e
26
33
  @errors << {
@@ -30,6 +37,46 @@ module Igniter
30
37
  }
31
38
  end
32
39
  end
40
+
41
+ private
42
+
43
+ def call_action(action, **kwargs)
44
+ parameters = action.parameters
45
+ accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
46
+
47
+ if accepts_any_keywords
48
+ action.call(**kwargs)
49
+ return
50
+ end
51
+
52
+ accepted_keywords = parameters.select { |kind, _name| %i[key keyreq].include?(kind) }.map(&:last)
53
+ filtered_kwargs = kwargs.slice(*accepted_keywords)
54
+ action.call(**filtered_kwargs)
55
+ end
56
+
57
+ def value_for(event)
58
+ return nil unless %i[node_succeeded node_resumed node_pending].include?(event.type)
59
+ return nil unless event.node_name
60
+
61
+ state = execution.cache.fetch(event.node_name)
62
+ state&.value
63
+ end
64
+
65
+ def already_fired?(reaction, event)
66
+ return false unless reaction.once_per_execution
67
+
68
+ @fired_reactions.include?(reaction_key(reaction, event))
69
+ end
70
+
71
+ def mark_fired(reaction, event)
72
+ return unless reaction.once_per_execution
73
+
74
+ @fired_reactions << reaction_key(reaction, event)
75
+ end
76
+
77
+ def reaction_key(reaction, event)
78
+ [reaction.object_id, event.execution_id]
79
+ end
33
80
  end
34
81
  end
35
82
  end
@@ -4,12 +4,13 @@ module Igniter
4
4
  module Extensions
5
5
  module Reactive
6
6
  class Reaction
7
- attr_reader :event_type, :path, :action
7
+ attr_reader :event_type, :path, :action, :once_per_execution
8
8
 
9
- def initialize(event_type:, path: nil, action:)
9
+ def initialize(event_type:, path: nil, action:, once_per_execution: false)
10
10
  @event_type = event_type.to_sym
11
11
  @path = path&.to_s
12
12
  @action = action
13
+ @once_per_execution = once_per_execution
13
14
  end
14
15
  end
15
16
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class BranchNode < Node
6
+ attr_reader :selector_dependency, :cases, :default_contract, :input_mapping
7
+
8
+ def initialize(id:, name:, selector_dependency:, cases:, default_contract:, input_mapping:, path: nil, metadata: {})
9
+ dependencies = ([selector_dependency] + input_mapping.values).uniq
10
+
11
+ super(
12
+ id: id,
13
+ kind: :branch,
14
+ name: name,
15
+ path: (path || name),
16
+ dependencies: dependencies,
17
+ metadata: metadata
18
+ )
19
+
20
+ @selector_dependency = selector_dependency.to_sym
21
+ @cases = cases.map { |entry| normalize_case(entry) }.freeze
22
+ @default_contract = default_contract
23
+ @input_mapping = input_mapping.transform_keys(&:to_sym).transform_values(&:to_sym).freeze
24
+ end
25
+
26
+ def possible_contracts
27
+ (cases.map { |entry| entry[:contract] } + [default_contract]).uniq
28
+ end
29
+
30
+ private
31
+
32
+ def normalize_case(entry)
33
+ {
34
+ match: entry.fetch(:match),
35
+ contract: entry.fetch(:contract)
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Model
5
+ class CollectionNode < Node
6
+ attr_reader :source_dependency, :contract_class, :key_name, :mode
7
+
8
+ def initialize(id:, name:, source_dependency:, contract_class:, key_name:, mode:, path: nil, metadata: {})
9
+ super(
10
+ id: id,
11
+ kind: :collection,
12
+ name: name,
13
+ path: (path || name),
14
+ dependencies: [source_dependency],
15
+ metadata: metadata
16
+ )
17
+
18
+ @source_dependency = source_dependency.to_sym
19
+ @contract_class = contract_class
20
+ @key_name = key_name.to_sym
21
+ @mode = mode.to_sym
22
+ end
23
+ end
24
+ end
25
+ end
@@ -5,12 +5,12 @@ module Igniter
5
5
  class CompositionNode < Node
6
6
  attr_reader :contract_class, :input_mapping
7
7
 
8
- def initialize(id:, name:, contract_class:, input_mapping:, metadata: {})
8
+ def initialize(id:, name:, contract_class:, input_mapping:, path: nil, metadata: {})
9
9
  super(
10
10
  id: id,
11
11
  kind: :composition,
12
12
  name: name,
13
- path: name,
13
+ path: (path || name),
14
14
  dependencies: input_mapping.values,
15
15
  metadata: metadata
16
16
  )
@@ -5,17 +5,73 @@ module Igniter
5
5
  class ComputeNode < Node
6
6
  attr_reader :callable
7
7
 
8
- def initialize(id:, name:, dependencies:, callable:, metadata: {})
8
+ def initialize(id:, name:, dependencies:, callable:, path: nil, metadata: {})
9
9
  super(
10
10
  id: id,
11
11
  kind: :compute,
12
12
  name: name,
13
- path: name,
13
+ path: (path || name),
14
14
  dependencies: dependencies,
15
15
  metadata: metadata
16
16
  )
17
17
  @callable = callable
18
18
  end
19
+
20
+ def callable_name
21
+ return "const" if const?
22
+ return "guard" if guard?
23
+
24
+ case callable
25
+ when Proc
26
+ "proc"
27
+ when Symbol, String
28
+ callable.to_s
29
+ when Class
30
+ callable.name || "AnonymousClass"
31
+ else
32
+ callable.class.name || "AnonymousCallable"
33
+ end
34
+ end
35
+
36
+ def executor_key
37
+ metadata[:executor_key] || executor_metadata[:key]
38
+ end
39
+
40
+ def executor_label
41
+ metadata[:label] || executor_metadata[:label]
42
+ end
43
+
44
+ def executor_category
45
+ metadata[:category] || executor_metadata[:category]
46
+ end
47
+
48
+ def executor_tags
49
+ Array(metadata[:tags] || executor_metadata[:tags]).freeze
50
+ end
51
+
52
+ def executor_summary
53
+ metadata[:summary] || executor_metadata[:summary]
54
+ end
55
+
56
+ def type
57
+ metadata[:type] || executor_metadata[:type]
58
+ end
59
+
60
+ def const?
61
+ metadata[:kind] == :const
62
+ end
63
+
64
+ def guard?
65
+ metadata[:guard] == true || metadata[:kind] == :guard
66
+ end
67
+
68
+ private
69
+
70
+ def executor_metadata
71
+ return {} unless callable.is_a?(Class) && callable <= Igniter::Executor
72
+
73
+ callable.executor_metadata
74
+ end
19
75
  end
20
76
  end
21
77
  end
@@ -3,8 +3,8 @@
3
3
  module Igniter
4
4
  module Model
5
5
  class InputNode < Node
6
- def initialize(id:, name:, metadata: {})
7
- super(id: id, kind: :input, name: name, path: name, metadata: metadata)
6
+ def initialize(id:, name:, path: nil, metadata: {})
7
+ super(id: id, kind: :input, name: name, path: (path || name), metadata: metadata)
8
8
  end
9
9
 
10
10
  def type
@@ -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: [source],
15
+ path: (path || "output.#{name}"),
16
+ dependencies: [normalized_source.split(".").first],
15
17
  metadata: metadata
16
18
  )
17
- @source = source.to_sym
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
- current = fetch(state.node.name)
16
- version = state.version || next_version(current)
17
- @states[state.node.name] = NodeState.new(
18
- node: state.node,
19
- status: state.status,
20
- value: state.value,
21
- error: state.error,
22
- version: version,
23
- resolved_at: state.resolved_at,
24
- invalidated_by: state.invalidated_by
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
- current = fetch(node.name)
30
- return unless current
58
+ @mutex.synchronize do
59
+ current = @states[node.name]
60
+ return unless current
31
61
 
32
- @states[node.name] = NodeState.new(
33
- node: node,
34
- status: :stale,
35
- value: current.value,
36
- error: current.error,
37
- version: current.version + 1,
38
- resolved_at: current.resolved_at,
39
- invalidated_by: invalidated_by
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