igniter 0.4.0 → 0.4.5
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 +25 -0
- data/README.md +238 -218
- data/docs/LLM_V1.md +335 -0
- data/docs/PATTERNS.md +189 -0
- data/docs/SERVER_V1.md +313 -0
- data/examples/README.md +129 -0
- data/examples/agents.rb +150 -0
- data/examples/differential.rb +161 -0
- data/examples/distributed_server.rb +94 -0
- data/examples/effects.rb +184 -0
- data/examples/incremental.rb +142 -0
- data/examples/invariants.rb +179 -0
- data/examples/order_pipeline.rb +163 -0
- data/examples/provenance.rb +122 -0
- data/examples/saga.rb +110 -0
- data/lib/igniter/agent/mailbox.rb +96 -0
- data/lib/igniter/agent/message.rb +21 -0
- data/lib/igniter/agent/ref.rb +86 -0
- data/lib/igniter/agent/runner.rb +129 -0
- data/lib/igniter/agent/state_holder.rb +23 -0
- data/lib/igniter/agent.rb +155 -0
- data/lib/igniter/compiler/validators/callable_validator.rb +21 -3
- data/lib/igniter/differential/divergence.rb +29 -0
- data/lib/igniter/differential/formatter.rb +96 -0
- data/lib/igniter/differential/report.rb +86 -0
- data/lib/igniter/differential/runner.rb +130 -0
- data/lib/igniter/differential.rb +51 -0
- data/lib/igniter/dsl/contract_builder.rb +32 -0
- data/lib/igniter/effect.rb +91 -0
- data/lib/igniter/effect_registry.rb +78 -0
- data/lib/igniter/errors.rb +11 -1
- data/lib/igniter/execution_report/builder.rb +54 -0
- data/lib/igniter/execution_report/formatter.rb +50 -0
- data/lib/igniter/execution_report/node_entry.rb +24 -0
- data/lib/igniter/execution_report/report.rb +65 -0
- data/lib/igniter/execution_report.rb +32 -0
- data/lib/igniter/extensions/differential.rb +114 -0
- data/lib/igniter/extensions/execution_report.rb +27 -0
- data/lib/igniter/extensions/incremental.rb +50 -0
- data/lib/igniter/extensions/invariants.rb +116 -0
- data/lib/igniter/extensions/provenance.rb +45 -0
- data/lib/igniter/extensions/saga.rb +74 -0
- data/lib/igniter/incremental/formatter.rb +81 -0
- data/lib/igniter/incremental/result.rb +69 -0
- data/lib/igniter/incremental/tracker.rb +108 -0
- data/lib/igniter/incremental.rb +50 -0
- data/lib/igniter/integrations/agents.rb +18 -0
- data/lib/igniter/invariant.rb +50 -0
- data/lib/igniter/model/effect_node.rb +37 -0
- data/lib/igniter/model.rb +1 -0
- data/lib/igniter/property_testing/formatter.rb +66 -0
- data/lib/igniter/property_testing/generators.rb +115 -0
- data/lib/igniter/property_testing/result.rb +45 -0
- data/lib/igniter/property_testing/run.rb +43 -0
- data/lib/igniter/property_testing/runner.rb +47 -0
- data/lib/igniter/property_testing.rb +64 -0
- data/lib/igniter/provenance/builder.rb +97 -0
- data/lib/igniter/provenance/lineage.rb +82 -0
- data/lib/igniter/provenance/node_trace.rb +65 -0
- data/lib/igniter/provenance/text_formatter.rb +70 -0
- data/lib/igniter/provenance.rb +29 -0
- data/lib/igniter/registry.rb +67 -0
- data/lib/igniter/runtime/cache.rb +35 -6
- data/lib/igniter/runtime/execution.rb +8 -2
- data/lib/igniter/runtime/node_state.rb +7 -2
- data/lib/igniter/runtime/resolver.rb +84 -15
- data/lib/igniter/saga/compensation.rb +31 -0
- data/lib/igniter/saga/compensation_record.rb +20 -0
- data/lib/igniter/saga/executor.rb +85 -0
- data/lib/igniter/saga/formatter.rb +49 -0
- data/lib/igniter/saga/result.rb +47 -0
- data/lib/igniter/saga.rb +56 -0
- data/lib/igniter/stream_loop.rb +80 -0
- data/lib/igniter/supervisor.rb +167 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +10 -0
- metadata +63 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Saga
|
|
5
|
+
# Immutable record of a single compensation that was attempted.
|
|
6
|
+
class CompensationRecord
|
|
7
|
+
attr_reader :node_name, :error
|
|
8
|
+
|
|
9
|
+
def initialize(node_name:, success:, error: nil)
|
|
10
|
+
@node_name = node_name
|
|
11
|
+
@success = success
|
|
12
|
+
@error = error
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success? = @success
|
|
17
|
+
def failed? = !@success
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Saga
|
|
5
|
+
# Runs declared compensations for all successfully completed nodes,
|
|
6
|
+
# in reverse topological order.
|
|
7
|
+
#
|
|
8
|
+
# A node is eligible for compensation if:
|
|
9
|
+
# 1. Its state in the cache is `succeeded?`
|
|
10
|
+
# 2. A compensation is declared for it, via one of:
|
|
11
|
+
# a. Contract-level `compensate :node_name do ... end` (takes precedence)
|
|
12
|
+
# b. Built-in compensation defined on the Igniter::Effect adapter class
|
|
13
|
+
#
|
|
14
|
+
# Compensation failures are captured as failed CompensationRecords and
|
|
15
|
+
# do NOT halt the rollback of other nodes.
|
|
16
|
+
class Executor
|
|
17
|
+
def initialize(contract)
|
|
18
|
+
@contract = contract
|
|
19
|
+
@graph = contract.execution.compiled_graph
|
|
20
|
+
@cache = contract.execution.cache
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Array<CompensationRecord>]
|
|
24
|
+
def run_compensations
|
|
25
|
+
eligible_nodes_reversed.filter_map do |node|
|
|
26
|
+
compensation = find_compensation(node)
|
|
27
|
+
next unless compensation
|
|
28
|
+
|
|
29
|
+
attempt(compensation, node)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Find the first node whose state is :failed in the cache.
|
|
34
|
+
# @return [Symbol, nil]
|
|
35
|
+
def failed_node_name
|
|
36
|
+
@cache.to_h.find { |_name, state| state.failed? }&.first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Resolve the compensation to use for a node, if any.
|
|
42
|
+
#
|
|
43
|
+
# Contract-level `compensate :node_name` takes precedence over built-in
|
|
44
|
+
# compensation declared on an Igniter::Effect subclass.
|
|
45
|
+
#
|
|
46
|
+
# @return [Igniter::Saga::Compensation, nil]
|
|
47
|
+
def find_compensation(node)
|
|
48
|
+
declared = @contract.class.compensations
|
|
49
|
+
return declared[node.name] if declared.key?(node.name)
|
|
50
|
+
|
|
51
|
+
return nil unless node.kind == :effect
|
|
52
|
+
|
|
53
|
+
built_in = node.adapter_class.built_in_compensation
|
|
54
|
+
return nil unless built_in
|
|
55
|
+
|
|
56
|
+
Compensation.new(node.name, &built_in)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Nodes that succeeded, in reverse resolution order.
|
|
60
|
+
def eligible_nodes_reversed
|
|
61
|
+
@graph.resolution_order
|
|
62
|
+
.select { |node| @cache.fetch(node.name)&.succeeded? }
|
|
63
|
+
.reverse
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def attempt(compensation, node)
|
|
67
|
+
inputs = extract_inputs(node)
|
|
68
|
+
value = @cache.fetch(node.name)&.value
|
|
69
|
+
|
|
70
|
+
compensation.run(inputs: inputs, value: value)
|
|
71
|
+
CompensationRecord.new(node_name: compensation.node_name, success: true)
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
CompensationRecord.new(node_name: compensation.node_name, success: false, error: e)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Gather the resolved values of the node's direct dependencies.
|
|
77
|
+
def extract_inputs(node)
|
|
78
|
+
node.dependencies.each_with_object({}) do |dep_name, acc|
|
|
79
|
+
state = @cache.fetch(dep_name)
|
|
80
|
+
acc[dep_name.to_sym] = state&.value
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Saga
|
|
5
|
+
# Formats a SagaResult as a human-readable text block.
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
#
|
|
9
|
+
# Contract: OrderWorkflow
|
|
10
|
+
# Status: FAILED
|
|
11
|
+
# Error: Insufficient funds
|
|
12
|
+
# At node: :charge_card
|
|
13
|
+
#
|
|
14
|
+
# COMPENSATIONS (1):
|
|
15
|
+
# [ok] :reserve_stock
|
|
16
|
+
#
|
|
17
|
+
module Formatter
|
|
18
|
+
class << self
|
|
19
|
+
def format(result)
|
|
20
|
+
lines = []
|
|
21
|
+
lines << "Contract: #{result.contract.class.name}"
|
|
22
|
+
lines << "Status: #{result.success? ? "SUCCESS" : "FAILED"}"
|
|
23
|
+
|
|
24
|
+
if result.failed?
|
|
25
|
+
lines << "Error: #{result.error&.message}"
|
|
26
|
+
lines << "At node: :#{result.failed_node}" if result.failed_node
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
append_compensations(result, lines)
|
|
30
|
+
lines.join("\n")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def append_compensations(result, lines)
|
|
36
|
+
return if result.compensations.empty?
|
|
37
|
+
|
|
38
|
+
lines << ""
|
|
39
|
+
lines << "COMPENSATIONS (#{result.compensations.size}):"
|
|
40
|
+
result.compensations.each do |rec|
|
|
41
|
+
tag = rec.success? ? "[ok] " : "[fail] "
|
|
42
|
+
lines << " #{tag} :#{rec.node_name}"
|
|
43
|
+
lines << " error: #{rec.error.message}" if rec.failed?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Saga
|
|
5
|
+
# Immutable result of a saga execution (resolve_saga call).
|
|
6
|
+
#
|
|
7
|
+
# Attributes:
|
|
8
|
+
# contract — the contract instance that was executed
|
|
9
|
+
# error — Igniter::Error that caused failure (nil on success)
|
|
10
|
+
# failed_node — Symbol name of the first node that failed (nil on success)
|
|
11
|
+
# compensations — Array<CompensationRecord> for all attempted compensations
|
|
12
|
+
class Result
|
|
13
|
+
attr_reader :contract, :error, :failed_node, :compensations
|
|
14
|
+
|
|
15
|
+
def initialize(success:, contract:, error: nil, failed_node: nil, compensations: [])
|
|
16
|
+
@success = success
|
|
17
|
+
@contract = contract
|
|
18
|
+
@error = error
|
|
19
|
+
@failed_node = failed_node
|
|
20
|
+
@compensations = compensations.freeze
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def success? = @success
|
|
25
|
+
def failed? = !@success
|
|
26
|
+
|
|
27
|
+
# Human-readable saga report.
|
|
28
|
+
def explain
|
|
29
|
+
Formatter.format(self)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias to_s explain
|
|
33
|
+
|
|
34
|
+
# Structured (serialisable) representation.
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
success: success?,
|
|
38
|
+
failed_node: failed_node,
|
|
39
|
+
error: error&.message,
|
|
40
|
+
compensations: compensations.map do |rec|
|
|
41
|
+
{ node: rec.node_name, success: rec.success?, error: rec.error&.message }
|
|
42
|
+
end
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/igniter/saga.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "saga/compensation"
|
|
5
|
+
require_relative "saga/compensation_record"
|
|
6
|
+
require_relative "saga/formatter"
|
|
7
|
+
require_relative "saga/result"
|
|
8
|
+
require_relative "saga/executor"
|
|
9
|
+
|
|
10
|
+
module Igniter
|
|
11
|
+
# Saga pattern — compensating transactions for Igniter contracts.
|
|
12
|
+
#
|
|
13
|
+
# When a contract execution fails partway through, the saga system
|
|
14
|
+
# automatically runs the compensating actions for all previously
|
|
15
|
+
# SUCCEEDED nodes, in reverse topological order.
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
#
|
|
19
|
+
# require "igniter/extensions/saga"
|
|
20
|
+
#
|
|
21
|
+
# class OrderWorkflow < Igniter::Contract
|
|
22
|
+
# define do
|
|
23
|
+
# input :order_id
|
|
24
|
+
# input :amount
|
|
25
|
+
#
|
|
26
|
+
# compute :reserve_stock, depends_on: :order_id do |order_id:|
|
|
27
|
+
# InventoryService.reserve(order_id)
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# compute :charge_card, depends_on: %i[order_id amount reserve_stock] do |amount:, **|
|
|
31
|
+
# raise "Declined" if amount > 1000
|
|
32
|
+
# PaymentService.charge(amount)
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# output :charge_card
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# compensate :charge_card do |inputs:, value:|
|
|
39
|
+
# PaymentService.refund(value[:charge_id])
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# compensate :reserve_stock do |inputs:, value:|
|
|
43
|
+
# InventoryService.release(value[:reservation_id])
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# result = OrderWorkflow.new(order_id: "x1", amount: 1500).resolve_saga
|
|
48
|
+
# result.success? # => false
|
|
49
|
+
# result.failed_node # => :charge_card
|
|
50
|
+
# result.compensations.map(&:node_name) # => [:reserve_stock]
|
|
51
|
+
# puts result.explain
|
|
52
|
+
#
|
|
53
|
+
module Saga
|
|
54
|
+
class SagaError < Igniter::Error; end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
# Runs an Igniter contract in a continuous tick-loop.
|
|
5
|
+
#
|
|
6
|
+
# Each tick resolves the contract with the current inputs and delivers the
|
|
7
|
+
# result to the on_result callback. Useful for sensor polling, feed
|
|
8
|
+
# processing, or any recurring computation.
|
|
9
|
+
#
|
|
10
|
+
# stream = Igniter::StreamLoop.new(
|
|
11
|
+
# contract: SensorContract,
|
|
12
|
+
# tick_interval: 0.1,
|
|
13
|
+
# inputs: { sensor_id: "temp-1", threshold: 25.0 },
|
|
14
|
+
# on_result: ->(result) { puts result.status },
|
|
15
|
+
# on_error: ->(err) { warn err.message }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# stream.start
|
|
19
|
+
# stream.update_inputs(threshold: 30.0) # hot-swap inputs between ticks
|
|
20
|
+
# stream.stop
|
|
21
|
+
#
|
|
22
|
+
class StreamLoop
|
|
23
|
+
def initialize(contract:, tick_interval: 1.0, inputs: {}, on_result: nil, on_error: nil)
|
|
24
|
+
@contract_class = contract
|
|
25
|
+
@tick_interval = tick_interval.to_f
|
|
26
|
+
@on_result = on_result
|
|
27
|
+
@on_error = on_error
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@current_inputs = inputs.dup
|
|
30
|
+
@running = false
|
|
31
|
+
@thread = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Start the loop in a background thread. Returns self.
|
|
35
|
+
def start
|
|
36
|
+
@running = true
|
|
37
|
+
@thread = Thread.new { loop_body }
|
|
38
|
+
@thread.abort_on_exception = false
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Stop the loop and wait for the current tick to finish.
|
|
43
|
+
def stop(timeout: 5)
|
|
44
|
+
@running = false
|
|
45
|
+
@thread&.join(timeout)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Merge +new_inputs+ into the current input set. Takes effect on the next tick.
|
|
50
|
+
def update_inputs(new_inputs)
|
|
51
|
+
@mutex.synchronize { @current_inputs.merge!(new_inputs) }
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def alive?
|
|
56
|
+
@thread&.alive? || false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def loop_body
|
|
62
|
+
while @running
|
|
63
|
+
tick_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
64
|
+
run_tick
|
|
65
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - tick_start
|
|
66
|
+
sleep_for = [@tick_interval - elapsed, 0].max
|
|
67
|
+
sleep(sleep_for) if sleep_for.positive? && @running
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run_tick
|
|
72
|
+
inputs = @mutex.synchronize { @current_inputs.dup }
|
|
73
|
+
contract = @contract_class.new(**inputs)
|
|
74
|
+
contract.resolve_all
|
|
75
|
+
@on_result&.call(contract.result)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
@on_error&.call(e)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
# Supervises a group of agents and restarts them when they crash.
|
|
5
|
+
#
|
|
6
|
+
# Subclass Supervisor and declare children with the class-level DSL:
|
|
7
|
+
#
|
|
8
|
+
# class AppSupervisor < Igniter::Supervisor
|
|
9
|
+
# strategy :one_for_one # default
|
|
10
|
+
# max_restarts 5, within: 60 # default
|
|
11
|
+
#
|
|
12
|
+
# children do |c|
|
|
13
|
+
# c.worker :counter, CounterAgent
|
|
14
|
+
# c.worker :logger, LoggerAgent, initial_state: { level: :info }
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# sup = AppSupervisor.start
|
|
19
|
+
# sup.child(:counter).send(:increment, by: 1)
|
|
20
|
+
# sup.stop
|
|
21
|
+
#
|
|
22
|
+
# Restart strategies:
|
|
23
|
+
# :one_for_one — restart only the crashed agent (default)
|
|
24
|
+
# :one_for_all — stop all agents and restart them all when any one crashes
|
|
25
|
+
#
|
|
26
|
+
# Restart budget: if more than +max_restarts+ crashes happen within +within+
|
|
27
|
+
# seconds, the supervisor logs the failure and stops trying to restart.
|
|
28
|
+
#
|
|
29
|
+
class Supervisor
|
|
30
|
+
class RestartBudgetExceeded < Igniter::Error; end
|
|
31
|
+
|
|
32
|
+
# ── ChildSpec ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
ChildSpec = Struct.new(:name, :agent_class, :init_opts, keyword_init: true)
|
|
35
|
+
|
|
36
|
+
class ChildSpecBuilder
|
|
37
|
+
attr_reader :specs
|
|
38
|
+
|
|
39
|
+
def initialize
|
|
40
|
+
@specs = []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def worker(name, agent_class, **opts)
|
|
44
|
+
@specs << ChildSpec.new(name: name.to_sym, agent_class: agent_class, init_opts: opts)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ── Class-level defaults ─────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
@strategy = :one_for_one
|
|
51
|
+
@max_restarts = 5
|
|
52
|
+
@restart_window = 60
|
|
53
|
+
@spec_builder = ChildSpecBuilder.new
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def inherited(subclass)
|
|
57
|
+
super
|
|
58
|
+
subclass.instance_variable_set(:@strategy, :one_for_one)
|
|
59
|
+
subclass.instance_variable_set(:@max_restarts, 5)
|
|
60
|
+
subclass.instance_variable_set(:@restart_window, 60)
|
|
61
|
+
subclass.instance_variable_set(:@spec_builder, ChildSpecBuilder.new)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def strategy(sym)
|
|
65
|
+
@strategy = sym
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def max_restarts(count, within:)
|
|
69
|
+
@max_restarts = count
|
|
70
|
+
@restart_window = within
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def children(&block)
|
|
74
|
+
block.call(@spec_builder)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def child_specs
|
|
78
|
+
@spec_builder.specs
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start
|
|
82
|
+
new.tap(&:start_all)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ── Instance ─────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def initialize
|
|
89
|
+
@refs = {}
|
|
90
|
+
@specs_by_name = {}
|
|
91
|
+
@restart_log = []
|
|
92
|
+
@mutex = Mutex.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def start_all
|
|
96
|
+
self.class.child_specs.each do |spec|
|
|
97
|
+
@specs_by_name[spec.name] = spec
|
|
98
|
+
start_child(spec)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Return the Ref for a named child. Returns nil if not found.
|
|
103
|
+
def child(name)
|
|
104
|
+
@mutex.synchronize { @refs[name.to_sym] }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Stop all children gracefully.
|
|
108
|
+
def stop
|
|
109
|
+
refs = @mutex.synchronize { @refs.values.dup }
|
|
110
|
+
refs.each do |ref|
|
|
111
|
+
ref.stop
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
self
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def start_child(spec)
|
|
121
|
+
opts = spec.init_opts.dup
|
|
122
|
+
opts[:on_crash] = ->(error) { handle_crash(spec, error) }
|
|
123
|
+
ref = spec.agent_class.start(**opts)
|
|
124
|
+
@mutex.synchronize { @refs[spec.name] = ref }
|
|
125
|
+
ref
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handle_crash(spec, _error)
|
|
129
|
+
check_restart_budget!
|
|
130
|
+
|
|
131
|
+
case self.class.instance_variable_get(:@strategy)
|
|
132
|
+
when :one_for_one
|
|
133
|
+
start_child(spec)
|
|
134
|
+
when :one_for_all
|
|
135
|
+
stop_all_children
|
|
136
|
+
self.class.child_specs.each { |s| start_child(s) }
|
|
137
|
+
end
|
|
138
|
+
rescue RestartBudgetExceeded => e
|
|
139
|
+
warn "Igniter::Supervisor #{self.class.name}: #{e.message}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def check_restart_budget! # rubocop:disable Metrics/MethodLength
|
|
143
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
|
+
window = self.class.instance_variable_get(:@restart_window).to_f
|
|
145
|
+
max = self.class.instance_variable_get(:@max_restarts)
|
|
146
|
+
|
|
147
|
+
@mutex.synchronize do
|
|
148
|
+
@restart_log.reject! { |t| now - t > window }
|
|
149
|
+
@restart_log << now
|
|
150
|
+
|
|
151
|
+
if @restart_log.size > max
|
|
152
|
+
raise RestartBudgetExceeded,
|
|
153
|
+
"#{@restart_log.size} crashes in #{window}s (max=#{max})"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def stop_all_children
|
|
159
|
+
refs = @mutex.synchronize { @refs.values.dup }
|
|
160
|
+
refs.each do |ref|
|
|
161
|
+
ref.stop(timeout: 2)
|
|
162
|
+
rescue StandardError
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/igniter/version.rb
CHANGED
data/lib/igniter.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "igniter/errors"
|
|
|
5
5
|
require_relative "igniter/type_system"
|
|
6
6
|
require_relative "igniter/executor"
|
|
7
7
|
require_relative "igniter/executor_registry"
|
|
8
|
+
require_relative "igniter/effect"
|
|
9
|
+
require_relative "igniter/effect_registry"
|
|
8
10
|
require_relative "igniter/model"
|
|
9
11
|
require_relative "igniter/compiler"
|
|
10
12
|
require_relative "igniter/events"
|
|
@@ -32,6 +34,14 @@ module Igniter
|
|
|
32
34
|
executor_registry.register(key, executor_class, **metadata)
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
def effect_registry
|
|
38
|
+
@effect_registry ||= EffectRegistry.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def register_effect(key, adapter_class, **metadata)
|
|
42
|
+
effect_registry.register(key, adapter_class, **metadata)
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
def compile(&block)
|
|
36
46
|
DSL::ContractBuilder.compile(&block)
|
|
37
47
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: igniter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander
|
|
@@ -57,19 +57,36 @@ files:
|
|
|
57
57
|
- docs/DISTRIBUTED_CONTRACTS_V1.md
|
|
58
58
|
- docs/EXECUTION_MODEL_V2.md
|
|
59
59
|
- docs/IGNITER_CONCEPTS.md
|
|
60
|
+
- docs/LLM_V1.md
|
|
60
61
|
- docs/PATTERNS.md
|
|
62
|
+
- docs/SERVER_V1.md
|
|
61
63
|
- docs/STORE_ADAPTERS.md
|
|
62
64
|
- examples/README.md
|
|
65
|
+
- examples/agents.rb
|
|
63
66
|
- examples/async_store.rb
|
|
64
67
|
- examples/basic_pricing.rb
|
|
65
68
|
- examples/collection.rb
|
|
66
69
|
- examples/collection_partial_failure.rb
|
|
67
70
|
- examples/composition.rb
|
|
68
71
|
- examples/diagnostics.rb
|
|
72
|
+
- examples/differential.rb
|
|
73
|
+
- examples/distributed_server.rb
|
|
69
74
|
- examples/distributed_workflow.rb
|
|
75
|
+
- examples/effects.rb
|
|
76
|
+
- examples/incremental.rb
|
|
77
|
+
- examples/invariants.rb
|
|
70
78
|
- examples/marketing_ergonomics.rb
|
|
79
|
+
- examples/order_pipeline.rb
|
|
80
|
+
- examples/provenance.rb
|
|
71
81
|
- examples/ringcentral_routing.rb
|
|
82
|
+
- examples/saga.rb
|
|
72
83
|
- lib/igniter.rb
|
|
84
|
+
- lib/igniter/agent.rb
|
|
85
|
+
- lib/igniter/agent/mailbox.rb
|
|
86
|
+
- lib/igniter/agent/message.rb
|
|
87
|
+
- lib/igniter/agent/ref.rb
|
|
88
|
+
- lib/igniter/agent/runner.rb
|
|
89
|
+
- lib/igniter/agent/state_holder.rb
|
|
73
90
|
- lib/igniter/compiler.rb
|
|
74
91
|
- lib/igniter/compiler/compiled_graph.rb
|
|
75
92
|
- lib/igniter/compiler/graph_compiler.rb
|
|
@@ -91,26 +108,49 @@ files:
|
|
|
91
108
|
- lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb
|
|
92
109
|
- lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb
|
|
93
110
|
- lib/igniter/diagnostics/report.rb
|
|
111
|
+
- lib/igniter/differential.rb
|
|
112
|
+
- lib/igniter/differential/divergence.rb
|
|
113
|
+
- lib/igniter/differential/formatter.rb
|
|
114
|
+
- lib/igniter/differential/report.rb
|
|
115
|
+
- lib/igniter/differential/runner.rb
|
|
94
116
|
- lib/igniter/dsl.rb
|
|
95
117
|
- lib/igniter/dsl/contract_builder.rb
|
|
96
118
|
- lib/igniter/dsl/schema_builder.rb
|
|
119
|
+
- lib/igniter/effect.rb
|
|
120
|
+
- lib/igniter/effect_registry.rb
|
|
97
121
|
- lib/igniter/errors.rb
|
|
98
122
|
- lib/igniter/events.rb
|
|
99
123
|
- lib/igniter/events/bus.rb
|
|
100
124
|
- lib/igniter/events/event.rb
|
|
125
|
+
- lib/igniter/execution_report.rb
|
|
126
|
+
- lib/igniter/execution_report/builder.rb
|
|
127
|
+
- lib/igniter/execution_report/formatter.rb
|
|
128
|
+
- lib/igniter/execution_report/node_entry.rb
|
|
129
|
+
- lib/igniter/execution_report/report.rb
|
|
101
130
|
- lib/igniter/executor.rb
|
|
102
131
|
- lib/igniter/executor_registry.rb
|
|
103
132
|
- lib/igniter/extensions.rb
|
|
104
133
|
- lib/igniter/extensions/auditing.rb
|
|
105
134
|
- lib/igniter/extensions/auditing/timeline.rb
|
|
135
|
+
- lib/igniter/extensions/differential.rb
|
|
136
|
+
- lib/igniter/extensions/execution_report.rb
|
|
137
|
+
- lib/igniter/extensions/incremental.rb
|
|
106
138
|
- lib/igniter/extensions/introspection.rb
|
|
107
139
|
- lib/igniter/extensions/introspection/graph_formatter.rb
|
|
108
140
|
- lib/igniter/extensions/introspection/plan_formatter.rb
|
|
109
141
|
- lib/igniter/extensions/introspection/runtime_formatter.rb
|
|
142
|
+
- lib/igniter/extensions/invariants.rb
|
|
143
|
+
- lib/igniter/extensions/provenance.rb
|
|
110
144
|
- lib/igniter/extensions/reactive.rb
|
|
111
145
|
- lib/igniter/extensions/reactive/engine.rb
|
|
112
146
|
- lib/igniter/extensions/reactive/matcher.rb
|
|
113
147
|
- lib/igniter/extensions/reactive/reaction.rb
|
|
148
|
+
- lib/igniter/extensions/saga.rb
|
|
149
|
+
- lib/igniter/incremental.rb
|
|
150
|
+
- lib/igniter/incremental/formatter.rb
|
|
151
|
+
- lib/igniter/incremental/result.rb
|
|
152
|
+
- lib/igniter/incremental/tracker.rb
|
|
153
|
+
- lib/igniter/integrations/agents.rb
|
|
114
154
|
- lib/igniter/integrations/llm.rb
|
|
115
155
|
- lib/igniter/integrations/llm/config.rb
|
|
116
156
|
- lib/igniter/integrations/llm/context.rb
|
|
@@ -126,17 +166,31 @@ files:
|
|
|
126
166
|
- lib/igniter/integrations/rails/generators/install/install_generator.rb
|
|
127
167
|
- lib/igniter/integrations/rails/railtie.rb
|
|
128
168
|
- lib/igniter/integrations/rails/webhook_concern.rb
|
|
169
|
+
- lib/igniter/invariant.rb
|
|
129
170
|
- lib/igniter/model.rb
|
|
130
171
|
- lib/igniter/model/await_node.rb
|
|
131
172
|
- lib/igniter/model/branch_node.rb
|
|
132
173
|
- lib/igniter/model/collection_node.rb
|
|
133
174
|
- lib/igniter/model/composition_node.rb
|
|
134
175
|
- lib/igniter/model/compute_node.rb
|
|
176
|
+
- lib/igniter/model/effect_node.rb
|
|
135
177
|
- lib/igniter/model/graph.rb
|
|
136
178
|
- lib/igniter/model/input_node.rb
|
|
137
179
|
- lib/igniter/model/node.rb
|
|
138
180
|
- lib/igniter/model/output_node.rb
|
|
139
181
|
- lib/igniter/model/remote_node.rb
|
|
182
|
+
- lib/igniter/property_testing.rb
|
|
183
|
+
- lib/igniter/property_testing/formatter.rb
|
|
184
|
+
- lib/igniter/property_testing/generators.rb
|
|
185
|
+
- lib/igniter/property_testing/result.rb
|
|
186
|
+
- lib/igniter/property_testing/run.rb
|
|
187
|
+
- lib/igniter/property_testing/runner.rb
|
|
188
|
+
- lib/igniter/provenance.rb
|
|
189
|
+
- lib/igniter/provenance/builder.rb
|
|
190
|
+
- lib/igniter/provenance/lineage.rb
|
|
191
|
+
- lib/igniter/provenance/node_trace.rb
|
|
192
|
+
- lib/igniter/provenance/text_formatter.rb
|
|
193
|
+
- lib/igniter/registry.rb
|
|
140
194
|
- lib/igniter/runtime.rb
|
|
141
195
|
- lib/igniter/runtime/cache.rb
|
|
142
196
|
- lib/igniter/runtime/collection_result.rb
|
|
@@ -157,6 +211,12 @@ files:
|
|
|
157
211
|
- lib/igniter/runtime/stores/file_store.rb
|
|
158
212
|
- lib/igniter/runtime/stores/memory_store.rb
|
|
159
213
|
- lib/igniter/runtime/stores/redis_store.rb
|
|
214
|
+
- lib/igniter/saga.rb
|
|
215
|
+
- lib/igniter/saga/compensation.rb
|
|
216
|
+
- lib/igniter/saga/compensation_record.rb
|
|
217
|
+
- lib/igniter/saga/executor.rb
|
|
218
|
+
- lib/igniter/saga/formatter.rb
|
|
219
|
+
- lib/igniter/saga/result.rb
|
|
160
220
|
- lib/igniter/server.rb
|
|
161
221
|
- lib/igniter/server/client.rb
|
|
162
222
|
- lib/igniter/server/config.rb
|
|
@@ -170,6 +230,8 @@ files:
|
|
|
170
230
|
- lib/igniter/server/rack_app.rb
|
|
171
231
|
- lib/igniter/server/registry.rb
|
|
172
232
|
- lib/igniter/server/router.rb
|
|
233
|
+
- lib/igniter/stream_loop.rb
|
|
234
|
+
- lib/igniter/supervisor.rb
|
|
173
235
|
- lib/igniter/type_system.rb
|
|
174
236
|
- lib/igniter/version.rb
|
|
175
237
|
- sig/igniter.rbs
|