igniter 0.4.3 → 0.5.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/README.md +217 -0
- data/docs/APPLICATION_V1.md +253 -0
- data/docs/CAPABILITIES_V1.md +207 -0
- data/docs/CONSENSUS_V1.md +477 -0
- data/docs/CONTENT_ADDRESSING_V1.md +221 -0
- data/docs/DATAFLOW_V1.md +274 -0
- data/docs/MESH_V1.md +732 -0
- data/docs/NODE_CACHE_V1.md +324 -0
- data/docs/PROACTIVE_AGENTS_V1.md +293 -0
- data/docs/SERVER_V1.md +200 -1
- data/docs/SKILLS_V1.md +213 -0
- data/docs/STORE_ADAPTERS.md +41 -13
- data/docs/TEMPORAL_V1.md +174 -0
- data/docs/TOOLS_V1.md +347 -0
- data/docs/TRANSCRIPTION_V1.md +403 -0
- data/examples/README.md +37 -0
- data/examples/consensus.rb +239 -0
- data/examples/dataflow.rb +308 -0
- data/examples/elocal_webhook.rb +1 -0
- data/examples/incremental.rb +142 -0
- data/examples/llm_tools.rb +237 -0
- data/examples/mesh.rb +239 -0
- data/examples/mesh_discovery.rb +267 -0
- data/examples/mesh_gossip.rb +162 -0
- data/examples/ringcentral_routing.rb +1 -1
- data/lib/igniter/agents/ai/alert_agent.rb +111 -0
- data/lib/igniter/agents/ai/chain_agent.rb +127 -0
- data/lib/igniter/agents/ai/critic_agent.rb +163 -0
- data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
- data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
- data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
- data/lib/igniter/agents/ai/observer_agent.rb +184 -0
- data/lib/igniter/agents/ai/planner_agent.rb +210 -0
- data/lib/igniter/agents/ai/router_agent.rb +131 -0
- data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
- data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
- data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
- data/lib/igniter/agents/proactive_agent.rb +208 -0
- data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
- data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
- data/lib/igniter/agents.rb +56 -0
- data/lib/igniter/application/app_config.rb +32 -0
- data/lib/igniter/application/autoloader.rb +18 -0
- data/lib/igniter/application/generator.rb +157 -0
- data/lib/igniter/application/scheduler.rb +109 -0
- data/lib/igniter/application/yml_loader.rb +39 -0
- data/lib/igniter/application.rb +174 -0
- data/lib/igniter/capabilities.rb +68 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
- data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
- data/lib/igniter/consensus/cluster.rb +183 -0
- data/lib/igniter/consensus/errors.rb +14 -0
- data/lib/igniter/consensus/executors.rb +43 -0
- data/lib/igniter/consensus/node.rb +320 -0
- data/lib/igniter/consensus/read_query.rb +30 -0
- data/lib/igniter/consensus/state_machine.rb +58 -0
- data/lib/igniter/consensus.rb +58 -0
- data/lib/igniter/content_addressing.rb +133 -0
- data/lib/igniter/contract.rb +12 -0
- data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
- data/lib/igniter/dataflow/aggregate_state.rb +77 -0
- data/lib/igniter/dataflow/diff.rb +37 -0
- data/lib/igniter/dataflow/diff_state.rb +81 -0
- data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
- data/lib/igniter/dataflow/window_filter.rb +48 -0
- data/lib/igniter/dataflow.rb +65 -0
- data/lib/igniter/dsl/contract_builder.rb +71 -7
- data/lib/igniter/executor.rb +60 -0
- data/lib/igniter/extensions/capabilities.rb +39 -0
- data/lib/igniter/extensions/content_addressing.rb +5 -0
- data/lib/igniter/extensions/dataflow.rb +117 -0
- data/lib/igniter/extensions/incremental.rb +50 -0
- data/lib/igniter/extensions/mesh.rb +31 -0
- data/lib/igniter/fingerprint.rb +43 -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/llm/config.rb +48 -4
- data/lib/igniter/integrations/llm/executor.rb +221 -28
- data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
- data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
- data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
- data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
- data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
- data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
- data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
- data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
- data/lib/igniter/integrations/llm.rb +37 -1
- data/lib/igniter/memory/agent_memory.rb +104 -0
- data/lib/igniter/memory/episode.rb +29 -0
- data/lib/igniter/memory/fact.rb +27 -0
- data/lib/igniter/memory/memorable.rb +90 -0
- data/lib/igniter/memory/reflection_cycle.rb +96 -0
- data/lib/igniter/memory/reflection_record.rb +28 -0
- data/lib/igniter/memory/store.rb +115 -0
- data/lib/igniter/memory/stores/in_memory.rb +136 -0
- data/lib/igniter/memory/stores/sqlite.rb +284 -0
- data/lib/igniter/memory.rb +80 -0
- data/lib/igniter/mesh/announcer.rb +55 -0
- data/lib/igniter/mesh/config.rb +45 -0
- data/lib/igniter/mesh/discovery.rb +39 -0
- data/lib/igniter/mesh/errors.rb +31 -0
- data/lib/igniter/mesh/gossip.rb +47 -0
- data/lib/igniter/mesh/peer.rb +21 -0
- data/lib/igniter/mesh/peer_registry.rb +51 -0
- data/lib/igniter/mesh/poller.rb +77 -0
- data/lib/igniter/mesh/router.rb +109 -0
- data/lib/igniter/mesh.rb +85 -0
- data/lib/igniter/metrics/collector.rb +131 -0
- data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
- data/lib/igniter/metrics/snapshot.rb +8 -0
- data/lib/igniter/metrics.rb +37 -0
- data/lib/igniter/model/aggregate_node.rb +34 -0
- data/lib/igniter/model/collection_node.rb +3 -2
- data/lib/igniter/model/compute_node.rb +13 -0
- data/lib/igniter/model/remote_node.rb +18 -2
- data/lib/igniter/node_cache.rb +231 -0
- data/lib/igniter/replication/bootstrapper.rb +61 -0
- data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
- data/lib/igniter/replication/bootstrappers/git.rb +39 -0
- data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
- data/lib/igniter/replication/expansion_plan.rb +38 -0
- data/lib/igniter/replication/expansion_planner.rb +142 -0
- data/lib/igniter/replication/manifest.rb +45 -0
- data/lib/igniter/replication/network_topology.rb +123 -0
- data/lib/igniter/replication/node_role.rb +42 -0
- data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
- data/lib/igniter/replication/replication_agent.rb +87 -0
- data/lib/igniter/replication/role_registry.rb +73 -0
- data/lib/igniter/replication/ssh_session.rb +77 -0
- data/lib/igniter/replication.rb +54 -0
- data/lib/igniter/runtime/cache.rb +35 -6
- data/lib/igniter/runtime/execution.rb +26 -2
- data/lib/igniter/runtime/input_validator.rb +6 -2
- data/lib/igniter/runtime/node_state.rb +7 -2
- data/lib/igniter/runtime/resolver.rb +323 -31
- data/lib/igniter/runtime/stores/redis_store.rb +41 -4
- data/lib/igniter/server/client.rb +44 -1
- data/lib/igniter/server/config.rb +13 -6
- data/lib/igniter/server/handlers/event_handler.rb +4 -0
- data/lib/igniter/server/handlers/execute_handler.rb +6 -0
- data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
- data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
- data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
- data/lib/igniter/server/handlers/peers_handler.rb +115 -0
- data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
- data/lib/igniter/server/http_server.rb +54 -17
- data/lib/igniter/server/router.rb +54 -21
- data/lib/igniter/server/server_logger.rb +52 -0
- data/lib/igniter/server.rb +6 -0
- data/lib/igniter/skill/feedback.rb +116 -0
- data/lib/igniter/skill/output_schema.rb +110 -0
- data/lib/igniter/skill.rb +218 -0
- data/lib/igniter/temporal.rb +84 -0
- data/lib/igniter/tool/discoverable.rb +151 -0
- data/lib/igniter/tool.rb +52 -0
- data/lib/igniter/tool_registry.rb +144 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +17 -0
- metadata +128 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../agent"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Replication
|
|
7
|
+
# Agent that handles :replicate messages to deploy Igniter to remote servers.
|
|
8
|
+
#
|
|
9
|
+
# In production, start via ReplicationAgent.start and send messages through
|
|
10
|
+
# the Ref. In tests, instantiate directly and call handle_message/1.
|
|
11
|
+
#
|
|
12
|
+
# Message payload keys:
|
|
13
|
+
# host: [String] (required) remote hostname or IP
|
|
14
|
+
# user: [String] (required) SSH username
|
|
15
|
+
# key: [String] path to SSH private key (optional)
|
|
16
|
+
# port: [Integer] SSH port (default: 22)
|
|
17
|
+
# env: [Hash] environment variables for remote (default: {})
|
|
18
|
+
# strategy: [Symbol] :git, :gem, or :tarball (default: :git)
|
|
19
|
+
# target_path: [String] installation path on remote (default: /opt/igniter)
|
|
20
|
+
# bootstrapper_options: [Hash] forwarded to the bootstrapper constructor
|
|
21
|
+
#
|
|
22
|
+
class ReplicationAgent < Igniter::Agent
|
|
23
|
+
MAX_REPLICAS = 10
|
|
24
|
+
|
|
25
|
+
initial_state events: []
|
|
26
|
+
|
|
27
|
+
# Class-level handler for the agent mailbox runtime.
|
|
28
|
+
# Instantiates a temporary agent to run the replication so that
|
|
29
|
+
# deliver/1 can be overridden in subclasses.
|
|
30
|
+
on :replicate do |state:, payload:, **|
|
|
31
|
+
agent = new
|
|
32
|
+
agent.send(:run_replicate, payload)
|
|
33
|
+
state
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Emit a named lifecycle event. Override or stub in tests.
|
|
37
|
+
#
|
|
38
|
+
# @param type [Symbol] event name (e.g. :replication_started)
|
|
39
|
+
# @param payload [Hash] associated data
|
|
40
|
+
def deliver(type, payload = {})
|
|
41
|
+
# Base implementation: no-op. Override in subclasses for real routing.
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Process a raw message hash synchronously (used in tests and internal tooling).
|
|
45
|
+
#
|
|
46
|
+
# @param message [Hash] must have :type key; optional :payload key
|
|
47
|
+
def handle_message(message)
|
|
48
|
+
type = message.fetch(:type).to_sym
|
|
49
|
+
payload = message.fetch(:payload, {})
|
|
50
|
+
return unless type == :replicate
|
|
51
|
+
|
|
52
|
+
run_replicate(payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def run_replicate(payload) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
58
|
+
host = payload[:host] || raise(ArgumentError, "host is required")
|
|
59
|
+
user = payload[:user] || raise(ArgumentError, "user is required")
|
|
60
|
+
key = payload[:key]
|
|
61
|
+
port = payload.fetch(:port, 22)
|
|
62
|
+
env = payload.fetch(:env, {})
|
|
63
|
+
strategy = payload.fetch(:strategy, :git).to_sym
|
|
64
|
+
target_path = payload.fetch(:target_path, "/opt/igniter")
|
|
65
|
+
bs_options = payload.fetch(:bootstrapper_options, {})
|
|
66
|
+
|
|
67
|
+
session = SSHSession.new(host: host, user: user, key: key, port: port)
|
|
68
|
+
bootstrapper = Replication.bootstrapper_for(strategy, **bs_options)
|
|
69
|
+
manifest = Manifest.current
|
|
70
|
+
|
|
71
|
+
deliver(:replication_started, host: host, instance_id: manifest.instance_id)
|
|
72
|
+
|
|
73
|
+
bootstrapper.install(session: session, manifest: manifest,
|
|
74
|
+
env: env, target_path: target_path)
|
|
75
|
+
bootstrapper.start(session: session, manifest: manifest, target_path: target_path)
|
|
76
|
+
verified = bootstrapper.verify(session: session, target_path: target_path)
|
|
77
|
+
|
|
78
|
+
deliver(:replication_completed,
|
|
79
|
+
host: host, instance_id: manifest.instance_id, verified: verified)
|
|
80
|
+
rescue SSHSession::SSHError => e
|
|
81
|
+
deliver(:replication_failed, host: host, error: e.message)
|
|
82
|
+
rescue ArgumentError => e
|
|
83
|
+
deliver(:replication_failed, host: payload[:host], error: e.message)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Replication
|
|
5
|
+
# Module-level registry of named NodeRoles.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# RoleRegistry.define(:worker,
|
|
9
|
+
# contracts: ["ComputeContract"],
|
|
10
|
+
# capabilities: [:compute],
|
|
11
|
+
# env_overrides: { "WORKER_POOL" => "4" }
|
|
12
|
+
# )
|
|
13
|
+
#
|
|
14
|
+
# role = RoleRegistry.fetch(:worker)
|
|
15
|
+
# role.env_overrides # => { "WORKER_POOL" => "4" }
|
|
16
|
+
module RoleRegistry
|
|
17
|
+
@roles = {}
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Define and register a new role.
|
|
21
|
+
#
|
|
22
|
+
# @param name [Symbol, String]
|
|
23
|
+
# @param contracts [Array<String>]
|
|
24
|
+
# @param capabilities [Array<Symbol>]
|
|
25
|
+
# @param env_overrides [Hash]
|
|
26
|
+
# @param tags [Array<Symbol>]
|
|
27
|
+
# @return [NodeRole]
|
|
28
|
+
def define(name, contracts: [], capabilities: [], env_overrides: {}, tags: [])
|
|
29
|
+
role = NodeRole.new(
|
|
30
|
+
name: name,
|
|
31
|
+
contracts: contracts,
|
|
32
|
+
capabilities: capabilities,
|
|
33
|
+
env_overrides: env_overrides,
|
|
34
|
+
tags: tags
|
|
35
|
+
)
|
|
36
|
+
@roles[role.name] = role
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Fetch a role by name.
|
|
40
|
+
#
|
|
41
|
+
# @param name [Symbol, String]
|
|
42
|
+
# @return [NodeRole]
|
|
43
|
+
# @raise [ArgumentError] if not registered
|
|
44
|
+
def fetch(name)
|
|
45
|
+
@roles.fetch(name.to_sym) do
|
|
46
|
+
raise ArgumentError,
|
|
47
|
+
"Unknown role: #{name}. Available: #{@roles.keys.join(", ")}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns true if a role with the given name is registered.
|
|
52
|
+
#
|
|
53
|
+
# @param name [Symbol, String]
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def registered?(name)
|
|
56
|
+
@roles.key?(name.to_sym)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# All registered roles (copy to prevent external mutation).
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash{Symbol => NodeRole}]
|
|
62
|
+
def all
|
|
63
|
+
@roles.dup
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Remove all registrations. Useful in tests.
|
|
67
|
+
def reset!
|
|
68
|
+
@roles = {}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Replication
|
|
7
|
+
# Thin subprocess wrapper over the +ssh+ and +scp+ CLI tools.
|
|
8
|
+
#
|
|
9
|
+
# Provides exec/exec! for running remote commands and upload! for
|
|
10
|
+
# copying local files to the remote host. No external gems required.
|
|
11
|
+
class SSHSession
|
|
12
|
+
class SSHError < Igniter::Error; end
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONNECT_TIMEOUT = 10
|
|
15
|
+
|
|
16
|
+
def initialize(host:, user:, key: nil, port: 22, connect_timeout: DEFAULT_CONNECT_TIMEOUT)
|
|
17
|
+
@host = host
|
|
18
|
+
@user = user
|
|
19
|
+
@key = key
|
|
20
|
+
@port = port
|
|
21
|
+
@connect_timeout = connect_timeout
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Run a command on the remote host. Raises SSHError on non-zero exit.
|
|
25
|
+
# Returns stdout string on success.
|
|
26
|
+
def exec!(command)
|
|
27
|
+
result = exec(command)
|
|
28
|
+
raise SSHError, "SSH command failed on #{@host}: #{command.inspect}\n#{result[:stderr]}" \
|
|
29
|
+
unless result[:success]
|
|
30
|
+
|
|
31
|
+
result[:stdout]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Run a command on the remote host.
|
|
35
|
+
# Returns a Hash: { stdout:, stderr:, success:, exit_code: }
|
|
36
|
+
def exec(command)
|
|
37
|
+
stdout, stderr, status = Open3.capture3(*build_cmd(command))
|
|
38
|
+
{ stdout: stdout, stderr: stderr, success: status.success?, exit_code: status.exitstatus }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Upload a local file to the remote host via scp.
|
|
42
|
+
# Raises SSHError on failure.
|
|
43
|
+
def upload!(local_path, remote_path)
|
|
44
|
+
args = ["scp", *scp_opts, "-P", @port.to_s, local_path, "#{@user}@#{@host}:#{remote_path}"]
|
|
45
|
+
_, stderr, status = Open3.capture3(*args)
|
|
46
|
+
raise SSHError, "SCP upload failed to #{@host}: #{stderr}" unless status.success?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Quick connectivity test. Returns true if the remote responds.
|
|
50
|
+
def test_connection
|
|
51
|
+
exec("echo ok")[:success]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def build_cmd(command)
|
|
57
|
+
["ssh", *ssh_opts, "-p", @port.to_s, "#{@user}@#{@host}", command]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ssh_opts
|
|
61
|
+
opts = [
|
|
62
|
+
"-o", "StrictHostKeyChecking=no",
|
|
63
|
+
"-o", "BatchMode=yes",
|
|
64
|
+
"-o", "ConnectTimeout=#{@connect_timeout}"
|
|
65
|
+
]
|
|
66
|
+
opts += ["-i", @key] if @key
|
|
67
|
+
opts
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def scp_opts
|
|
71
|
+
opts = ["-o", "StrictHostKeyChecking=no", "-B"]
|
|
72
|
+
opts += ["-i", @key] if @key
|
|
73
|
+
opts
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "replication/manifest"
|
|
4
|
+
require_relative "replication/ssh_session"
|
|
5
|
+
require_relative "replication/bootstrapper"
|
|
6
|
+
require_relative "replication/bootstrappers/git"
|
|
7
|
+
require_relative "replication/bootstrappers/gem"
|
|
8
|
+
require_relative "replication/bootstrappers/tarball"
|
|
9
|
+
require_relative "replication/replication_agent"
|
|
10
|
+
require_relative "replication/node_role"
|
|
11
|
+
require_relative "replication/role_registry"
|
|
12
|
+
require_relative "replication/network_topology"
|
|
13
|
+
require_relative "replication/expansion_plan"
|
|
14
|
+
require_relative "replication/expansion_planner"
|
|
15
|
+
require_relative "replication/reflective_replication_agent"
|
|
16
|
+
|
|
17
|
+
module Igniter
|
|
18
|
+
# Self-replication capability: deploy a running Igniter instance to a
|
|
19
|
+
# remote server via SSH using one of three deployment strategies.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# require "igniter/replication"
|
|
23
|
+
# ref = Igniter::Replication::ReplicationAgent.start
|
|
24
|
+
# ref.send(:replicate,
|
|
25
|
+
# host: "10.0.0.2",
|
|
26
|
+
# user: "deploy",
|
|
27
|
+
# strategy: :git,
|
|
28
|
+
# bootstrapper_options: { repo_url: "https://github.com/org/app" }
|
|
29
|
+
# )
|
|
30
|
+
#
|
|
31
|
+
module Replication
|
|
32
|
+
ReplicationError = Class.new(Igniter::Error)
|
|
33
|
+
|
|
34
|
+
BOOTSTRAPPERS = {
|
|
35
|
+
git: Bootstrappers::Git,
|
|
36
|
+
gem: Bootstrappers::Gem,
|
|
37
|
+
tarball: Bootstrappers::Tarball
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# Instantiate the bootstrapper for the given strategy.
|
|
41
|
+
#
|
|
42
|
+
# @param strategy [Symbol] one of :git, :gem, :tarball
|
|
43
|
+
# @param options [Hash] forwarded to the bootstrapper constructor
|
|
44
|
+
# @return [Bootstrapper]
|
|
45
|
+
# @raise [ArgumentError] for unknown strategies
|
|
46
|
+
def self.bootstrapper_for(strategy, **options)
|
|
47
|
+
klass = BOOTSTRAPPERS.fetch(strategy.to_sym) do
|
|
48
|
+
raise ArgumentError,
|
|
49
|
+
"Unknown bootstrapper: #{strategy}. Available: #{BOOTSTRAPPERS.keys.join(", ")}"
|
|
50
|
+
end
|
|
51
|
+
klass.new(**options)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -13,24 +13,27 @@ module Igniter
|
|
|
13
13
|
@mutex.synchronize { @states[node_name.to_sym] }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def write(state)
|
|
16
|
+
def write(state) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
17
17
|
@mutex.synchronize do
|
|
18
18
|
current = @states[state.node.name]
|
|
19
19
|
version = state.version || (current&.running? ? current.version : next_version(current))
|
|
20
|
+
value_version = compute_value_version(state, current)
|
|
20
21
|
@states[state.node.name] = NodeState.new(
|
|
21
22
|
node: state.node,
|
|
22
23
|
status: state.status,
|
|
23
24
|
value: state.value,
|
|
24
25
|
error: state.error,
|
|
25
26
|
version: version,
|
|
27
|
+
value_version: value_version,
|
|
26
28
|
resolved_at: state.resolved_at,
|
|
27
|
-
invalidated_by: state.invalidated_by
|
|
29
|
+
invalidated_by: state.invalidated_by,
|
|
30
|
+
dep_snapshot: state.dep_snapshot
|
|
28
31
|
)
|
|
29
32
|
@condition.broadcast
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
|
|
33
|
-
def begin_resolution(node)
|
|
36
|
+
def begin_resolution(node) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
34
37
|
@mutex.synchronize do
|
|
35
38
|
loop do
|
|
36
39
|
current = @states[node.name]
|
|
@@ -43,8 +46,10 @@ module Igniter
|
|
|
43
46
|
value: current&.value,
|
|
44
47
|
error: current&.error,
|
|
45
48
|
version: next_version(current),
|
|
49
|
+
value_version: current&.value_version,
|
|
46
50
|
resolved_at: current&.resolved_at || Time.now.utc,
|
|
47
|
-
invalidated_by: nil
|
|
51
|
+
invalidated_by: nil,
|
|
52
|
+
dep_snapshot: current&.dep_snapshot
|
|
48
53
|
)
|
|
49
54
|
return [:started, @states[node.name]]
|
|
50
55
|
end
|
|
@@ -54,7 +59,7 @@ module Igniter
|
|
|
54
59
|
end
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
def stale!(node, invalidated_by:)
|
|
62
|
+
def stale!(node, invalidated_by:) # rubocop:disable Metrics/MethodLength
|
|
58
63
|
@mutex.synchronize do
|
|
59
64
|
current = @states[node.name]
|
|
60
65
|
return unless current
|
|
@@ -65,8 +70,10 @@ module Igniter
|
|
|
65
70
|
value: current.value,
|
|
66
71
|
error: current.error,
|
|
67
72
|
version: current.version + 1,
|
|
73
|
+
value_version: current.value_version,
|
|
68
74
|
resolved_at: current.resolved_at,
|
|
69
|
-
invalidated_by: invalidated_by
|
|
75
|
+
invalidated_by: invalidated_by,
|
|
76
|
+
dep_snapshot: current.dep_snapshot
|
|
70
77
|
)
|
|
71
78
|
@condition.broadcast
|
|
72
79
|
end
|
|
@@ -92,6 +99,28 @@ module Igniter
|
|
|
92
99
|
def next_version(current)
|
|
93
100
|
current ? current.version + 1 : 1
|
|
94
101
|
end
|
|
102
|
+
|
|
103
|
+
# value_version only increments when the actual value changes.
|
|
104
|
+
# When state.value_version is set explicitly (backdating from resolver), use it.
|
|
105
|
+
# For :succeeded states, compare the new value against the old value:
|
|
106
|
+
# - same value → preserve value_version
|
|
107
|
+
# - different (or first time) → increment
|
|
108
|
+
# For all other statuses (failed, pending, etc.) → no value_version.
|
|
109
|
+
def compute_value_version(state, current)
|
|
110
|
+
return state.value_version if state.value_version
|
|
111
|
+
|
|
112
|
+
return nil unless state.status == :succeeded
|
|
113
|
+
|
|
114
|
+
base_vv = current&.value_version || 0
|
|
115
|
+
# When current is :running, current.value holds the pre-stale value for comparison.
|
|
116
|
+
old_value = current&.value
|
|
117
|
+
|
|
118
|
+
if base_vv.positive? && old_value == state.value
|
|
119
|
+
base_vv
|
|
120
|
+
else
|
|
121
|
+
base_vv + 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
95
124
|
end
|
|
96
125
|
end
|
|
97
126
|
end
|
|
@@ -59,6 +59,24 @@ module Igniter
|
|
|
59
59
|
self
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Returns the DiffState for an incremental collection node (created on first access).
|
|
63
|
+
# Persists across update_inputs calls for the lifetime of this Execution.
|
|
64
|
+
def diff_state_for(node_name)
|
|
65
|
+
@diff_states ||= {}
|
|
66
|
+
@diff_states[node_name.to_sym] ||= Igniter::Dataflow::DiffState.new
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Returns the AggregateState for an aggregate node (created on first access).
|
|
71
|
+
# Persists across update_inputs calls for the lifetime of this Execution.
|
|
72
|
+
def aggregate_state_for(node_name)
|
|
73
|
+
@aggregate_states ||= {}
|
|
74
|
+
@aggregate_states[node_name.to_sym] ||= begin
|
|
75
|
+
node = compiled_graph.fetch_node(node_name)
|
|
76
|
+
Igniter::Dataflow::AggregateState.new(node.operator)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
62
80
|
def resume(node_name, value:)
|
|
63
81
|
node = compiled_graph.fetch_node(node_name)
|
|
64
82
|
current = cache.fetch(node.name)
|
|
@@ -251,10 +269,12 @@ module Igniter
|
|
|
251
269
|
memo[node_name] = {
|
|
252
270
|
status: state.status,
|
|
253
271
|
version: state.version,
|
|
272
|
+
value_version: state.value_version,
|
|
254
273
|
resolved_at: state.resolved_at&.iso8601,
|
|
255
274
|
invalidated_by: state.invalidated_by,
|
|
256
275
|
value: serialize_state_value(state.value),
|
|
257
|
-
error: serialize_state_error(state.error)
|
|
276
|
+
error: serialize_state_error(state.error),
|
|
277
|
+
dep_snapshot: state.dep_snapshot
|
|
258
278
|
}
|
|
259
279
|
end
|
|
260
280
|
end
|
|
@@ -262,14 +282,18 @@ module Igniter
|
|
|
262
282
|
def deserialize_states(snapshot_states)
|
|
263
283
|
snapshot_states.each_with_object({}) do |(node_name, state_data), memo|
|
|
264
284
|
node = compiled_graph.fetch_node(node_name)
|
|
285
|
+
raw_dep_snapshot = state_data[:dep_snapshot] || state_data["dep_snapshot"]
|
|
286
|
+
dep_snapshot = raw_dep_snapshot&.transform_keys(&:to_sym)
|
|
265
287
|
memo[node.name] = NodeState.new(
|
|
266
288
|
node: node,
|
|
267
289
|
status: (state_data[:status] || state_data["status"]).to_sym,
|
|
268
290
|
value: deserialize_state_value(node, state_data[:value] || state_data["value"]),
|
|
269
291
|
error: deserialize_state_error(state_data[:error] || state_data["error"]),
|
|
270
292
|
version: state_data[:version] || state_data["version"],
|
|
293
|
+
value_version: state_data[:value_version] || state_data["value_version"],
|
|
271
294
|
resolved_at: deserialize_time(state_data[:resolved_at] || state_data["resolved_at"]),
|
|
272
|
-
invalidated_by: (state_data[:invalidated_by] || state_data["invalidated_by"])&.to_sym
|
|
295
|
+
invalidated_by: (state_data[:invalidated_by] || state_data["invalidated_by"])&.to_sym,
|
|
296
|
+
dep_snapshot: dep_snapshot
|
|
273
297
|
)
|
|
274
298
|
end
|
|
275
299
|
end
|
|
@@ -64,7 +64,8 @@ module Igniter
|
|
|
64
64
|
next if inputs.key?(node.name)
|
|
65
65
|
next unless node.default?
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
d = node.default
|
|
68
|
+
inputs[node.name] = d.respond_to?(:call) ? d.call : d
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
|
@@ -77,7 +78,10 @@ module Igniter
|
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def missing_value!(input_node)
|
|
80
|
-
|
|
81
|
+
if input_node.default?
|
|
82
|
+
d = input_node.default
|
|
83
|
+
return d.respond_to?(:call) ? d.call : d
|
|
84
|
+
end
|
|
81
85
|
return nil unless input_node.required?
|
|
82
86
|
|
|
83
87
|
raise input_error(input_node, "Missing required input: #{input_node.name}")
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
module Igniter
|
|
4
4
|
module Runtime
|
|
5
5
|
class NodeState
|
|
6
|
-
attr_reader :node, :status, :value, :error, :version, :
|
|
6
|
+
attr_reader :node, :status, :value, :error, :version, :value_version,
|
|
7
|
+
:resolved_at, :invalidated_by, :dep_snapshot
|
|
7
8
|
|
|
8
|
-
def initialize(node:, status:, value: nil, error: nil, version: nil,
|
|
9
|
+
def initialize(node:, status:, value: nil, error: nil, version: nil, value_version: nil, # rubocop:disable Metrics/ParameterLists
|
|
10
|
+
resolved_at: Time.now.utc, invalidated_by: nil, dep_snapshot: nil)
|
|
9
11
|
@node = node
|
|
10
12
|
@status = status
|
|
11
13
|
@value = value
|
|
12
14
|
@error = error
|
|
13
15
|
@version = version
|
|
16
|
+
@value_version = value_version
|
|
14
17
|
@resolved_at = resolved_at
|
|
15
18
|
@invalidated_by = invalidated_by
|
|
19
|
+
@dep_snapshot = dep_snapshot
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def stale?
|
|
@@ -40,6 +44,7 @@ module Igniter
|
|
|
40
44
|
node_name: node.name,
|
|
41
45
|
status: status,
|
|
42
46
|
version: version,
|
|
47
|
+
value_version: value_version,
|
|
43
48
|
resolved_at: resolved_at,
|
|
44
49
|
invalidated_by: invalidated_by,
|
|
45
50
|
value: value,
|