phronomy 0.16.0 → 0.18.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/.mutant.yml +8 -9
- data/CHANGELOG.md +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +6 -7
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +89 -190
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +49 -377
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -265
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -2,96 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module LlmContextWindow
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
class UnknownModelError < Phronomy::Error; end
|
|
8
|
-
|
|
9
|
-
# Calculates the effective token budget available for conversation history
|
|
10
|
-
# and injected knowledge within a single LLM request.
|
|
11
|
-
#
|
|
12
|
-
# The window is divided as follows:
|
|
13
|
-
#
|
|
14
|
-
# context_window (total)
|
|
15
|
-
# ├─ max_output_tokens (reserved for model output = max_output_tokens)
|
|
16
|
-
# ├─ overhead (reserved for system prompt + tool definitions)
|
|
17
|
-
# └─ effective_input_limit (available for memory + knowledge)
|
|
18
|
-
#
|
|
19
|
-
# @example Auto-derive from RubyLLM model registry
|
|
20
|
-
# budget = Phronomy::LlmContextWindow::TokenBudget.new(model: "claude-3-5-sonnet-20241022")
|
|
21
|
-
#
|
|
22
|
-
# @example Explicit values (useful for local / unknown models)
|
|
23
|
-
# budget = Phronomy::LlmContextWindow::TokenBudget.new(
|
|
24
|
-
# context_window: 32_768,
|
|
25
|
-
# max_output_tokens: 4_096
|
|
26
|
-
# )
|
|
27
|
-
#
|
|
28
|
-
# @example With overhead for instructions + tool definitions
|
|
29
|
-
# budget = Phronomy::LlmContextWindow::TokenBudget.new(
|
|
30
|
-
# model: "gpt-4o",
|
|
31
|
-
# overhead: 800
|
|
32
|
-
# )
|
|
5
|
+
# Immutable arithmetic value for one resolved model context budget.
|
|
6
|
+
# Model-registry lookup belongs to Agent::TokenBudgetResolver.
|
|
33
7
|
class TokenBudget
|
|
34
|
-
|
|
35
|
-
attr_reader :context_window
|
|
36
|
-
|
|
37
|
-
# @return [Integer] tokens reserved for model output
|
|
38
|
-
attr_reader :max_output_tokens
|
|
8
|
+
attr_reader :context_window, :max_output_tokens
|
|
39
9
|
|
|
40
|
-
# @return [Integer] tokens reserved for instructions and tool definitions
|
|
41
|
-
attr_reader :overhead
|
|
42
|
-
|
|
43
|
-
# @param model [String, nil] model identifier looked up in RubyLLM
|
|
44
|
-
# @param context_window [Integer, nil] explicit total token limit
|
|
45
|
-
# @param max_output_tokens [Integer, nil] explicit output reservation; when nil
|
|
46
|
-
# and model is given, uses max_output_tokens
|
|
47
|
-
# @param overhead [Integer] tokens reserved for instructions/tools
|
|
48
10
|
# @api private
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@
|
|
52
|
-
|
|
53
|
-
if context_window
|
|
54
|
-
# Explicit values — no registry lookup needed.
|
|
55
|
-
@context_window = context_window.to_i
|
|
56
|
-
@max_output_tokens = (max_output_tokens || 0).to_i
|
|
57
|
-
elsif model
|
|
58
|
-
ruby_llm_model = lookup_model!(model)
|
|
59
|
-
@context_window = ruby_llm_model.context_window.to_i
|
|
60
|
-
@max_output_tokens = (max_output_tokens || ruby_llm_model.max_output_tokens).to_i
|
|
61
|
-
else
|
|
62
|
-
raise ArgumentError, "Provide either model: or context_window:"
|
|
63
|
-
end
|
|
11
|
+
def initialize(context_window:, max_output_tokens:)
|
|
12
|
+
@context_window = Integer(context_window)
|
|
13
|
+
@max_output_tokens = Integer(max_output_tokens)
|
|
64
14
|
end
|
|
65
15
|
|
|
66
|
-
# Tokens available for conversation history and knowledge after reservations.
|
|
67
|
-
# Always >= 0.
|
|
68
|
-
#
|
|
69
|
-
# @return [Integer]
|
|
70
16
|
# @api private
|
|
71
17
|
def effective_input_limit
|
|
72
|
-
[@context_window - @max_output_tokens
|
|
18
|
+
[@context_window - @max_output_tokens, 0].max
|
|
73
19
|
end
|
|
74
20
|
|
|
75
|
-
# Tokens still available after `used` tokens have been allocated.
|
|
76
|
-
#
|
|
77
|
-
# @param used [Integer] tokens already committed (e.g. from knowledge injection)
|
|
78
|
-
# @return [Integer] remaining tokens (always >= 0)
|
|
79
21
|
# @api private
|
|
80
|
-
# mutant:disable - used.to_i vs used vs used.to_int vs Integer(used) are genuine equivalents when used is an Integer; used:nil default is genuine because nil.to_i==0==default 0
|
|
81
22
|
def available(used: 0)
|
|
82
|
-
[effective_input_limit - used
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
private
|
|
86
|
-
|
|
87
|
-
# mutant:disable - raise(UnknownModelError) and raise(UnknownModelError,nil) and raise(UnknownModelError,"Model '#{nil}' not found") in both branches are genuine equivalents (spec checks exception class only, not message text)
|
|
88
|
-
def lookup_model!(model_name)
|
|
89
|
-
found = RubyLLM.models.find(model_name)
|
|
90
|
-
raise UnknownModelError, "Model '#{model_name}' not found in RubyLLM registry" unless found
|
|
91
|
-
|
|
92
|
-
found
|
|
93
|
-
rescue RubyLLM::ModelNotFoundError
|
|
94
|
-
raise UnknownModelError, "Model '#{model_name}' not found in RubyLLM registry"
|
|
23
|
+
[effective_input_limit - Integer(used), 0].max
|
|
95
24
|
end
|
|
96
25
|
end
|
|
97
26
|
end
|
data/lib/phronomy/metrics.rb
CHANGED
|
@@ -1,41 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# Collects live metrics from the shared Runtime components
|
|
7
|
-
# (BlockingAdapterPool, EventLoop, and Runtime task registry) and returns
|
|
8
|
-
# them as a plain Hash so they can be forwarded to any monitoring backend
|
|
9
|
-
# (Prometheus, OpenTelemetry, StatsD, etc.).
|
|
10
|
-
#
|
|
11
|
-
# All metrics are read at the moment {.snapshot} is called; no
|
|
12
|
-
# persistent state is held here.
|
|
13
|
-
#
|
|
14
|
-
# @example Exporting to a metrics endpoint
|
|
15
|
-
# data = Phronomy::Metrics.snapshot
|
|
16
|
-
# # => { blocking_pool_active: 2, active_agent_tasks: 1, ... }
|
|
4
|
+
# Runtime observability snapshot for the two concurrency boundaries:
|
|
5
|
+
# EventLoop and OffloadPool.
|
|
17
6
|
module Metrics
|
|
18
|
-
# Returns a Hash of current observability metrics.
|
|
19
|
-
#
|
|
20
|
-
# @return [Hash{Symbol => Numeric}]
|
|
21
|
-
# @api public
|
|
22
7
|
def self.snapshot
|
|
23
8
|
runtime = Runtime.instance
|
|
24
|
-
pool = runtime.
|
|
25
|
-
|
|
26
|
-
task_snap = runtime.task_snapshot
|
|
9
|
+
pool = runtime.offload
|
|
10
|
+
event_loop = runtime.event_loop
|
|
27
11
|
|
|
28
12
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
13
|
+
offload_pool_active: pool.active_count,
|
|
14
|
+
offload_pool_queue_length: pool.queue_depth,
|
|
15
|
+
offload_pool_abandoned_active: pool.abandoned_active_count,
|
|
16
|
+
offload_pool_abandoned_total: pool.abandoned_count,
|
|
17
|
+
offload_pool_size: pool.pool_size,
|
|
18
|
+
event_loop_queue_depth: event_loop.queue_depth,
|
|
19
|
+
event_loop_queue_max_depth: event_loop.max_queue_depth,
|
|
20
|
+
event_loop_lag_last_ms: (event_loop.last_lag_seconds * 1000).round(3),
|
|
21
|
+
event_loop_lag_max_ms: (event_loop.max_lag_seconds * 1000).round(3),
|
|
22
|
+
event_loop_lag_average_ms: (event_loop.average_lag_seconds * 1000).round(3)
|
|
23
|
+
}
|
|
39
24
|
end
|
|
40
25
|
end
|
|
41
26
|
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module MultiAgent
|
|
7
|
+
# Mutable FSM context for one fan-out operation.
|
|
8
|
+
class FanOutInvocation
|
|
9
|
+
Child = Data.define(:index, :agent, :input, :config, :thread_id)
|
|
10
|
+
|
|
11
|
+
attr_reader :id, :phase, :results
|
|
12
|
+
|
|
13
|
+
def initialize(children:, max_concurrency:, on_error:)
|
|
14
|
+
@id = SecureRandom.uuid.to_s
|
|
15
|
+
@phase = nil
|
|
16
|
+
@children = children
|
|
17
|
+
@max_concurrency = max_concurrency || children.length
|
|
18
|
+
@on_error = on_error
|
|
19
|
+
@pending = children.dup
|
|
20
|
+
@active = {}
|
|
21
|
+
@results = Array.new(children.length)
|
|
22
|
+
@child_errors = Array.new(children.length) # indexed by input order
|
|
23
|
+
@fatal_error = nil # driver_failed / timeout / cancel
|
|
24
|
+
@cancelled = false
|
|
25
|
+
@timed_out = false
|
|
26
|
+
@session_id = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_graph_metadata(thread_id: nil, phase: nil)
|
|
30
|
+
@session_id = thread_id if thread_id
|
|
31
|
+
@phase = phase
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handle_fsm_event(event)
|
|
35
|
+
case event.type
|
|
36
|
+
when :child_completed
|
|
37
|
+
payload = event.payload
|
|
38
|
+
index = payload.fetch(:index)
|
|
39
|
+
@active.delete(index)
|
|
40
|
+
child_error = payload[:error]
|
|
41
|
+
if child_error
|
|
42
|
+
if @on_error == :raise
|
|
43
|
+
@child_errors[index] = child_error
|
|
44
|
+
# Signal cancellation so other children can stop early, but continue
|
|
45
|
+
# waiting for all active children to respond so we can return the
|
|
46
|
+
# first error in INPUT ORDER (not arrival order).
|
|
47
|
+
cancel_active_children! unless @child_errors.any?(&:itself)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
@results[index] = payload[:result]
|
|
51
|
+
end
|
|
52
|
+
true
|
|
53
|
+
when :driver_failed
|
|
54
|
+
@fatal_error ||= event.payload.fetch(:error)
|
|
55
|
+
cancel_active_children!
|
|
56
|
+
true
|
|
57
|
+
when :timeout
|
|
58
|
+
@timed_out = true
|
|
59
|
+
@fatal_error ||= Phronomy::TimeoutError.new(event.payload.fetch(:message))
|
|
60
|
+
cancel_active_children!
|
|
61
|
+
true
|
|
62
|
+
when :cancel
|
|
63
|
+
@cancelled = true
|
|
64
|
+
@fatal_error ||= Phronomy::CancellationError.new("fan-out cancelled")
|
|
65
|
+
cancel_active_children!
|
|
66
|
+
true
|
|
67
|
+
else
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def start_available!(runtime)
|
|
73
|
+
return self if @fatal_error
|
|
74
|
+
|
|
75
|
+
while @active.length < @max_concurrency && (child = @pending.shift)
|
|
76
|
+
child_config = build_child_config(child.config)
|
|
77
|
+
handle = child.agent.invoke_async(
|
|
78
|
+
child.input,
|
|
79
|
+
config: child_config,
|
|
80
|
+
thread_id: child.thread_id
|
|
81
|
+
)
|
|
82
|
+
@active[child.index] = {handle: handle, token: child_config[:cancellation_token]}
|
|
83
|
+
# [child.index].each creates a block parameter with a unique binding
|
|
84
|
+
# per iteration, avoiding the while-loop variable capture problem.
|
|
85
|
+
[child.index].each do |captured_index|
|
|
86
|
+
handle.on_complete do |result, error|
|
|
87
|
+
runtime.event_loop.post_to_session(
|
|
88
|
+
Phronomy::Event.new(
|
|
89
|
+
type: :child_completed,
|
|
90
|
+
target_id: @id,
|
|
91
|
+
payload: {index: captured_index, result: result, error: error}
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
self
|
|
98
|
+
rescue => caught
|
|
99
|
+
@fatal_error ||= caught
|
|
100
|
+
cancel_active_children!
|
|
101
|
+
runtime.event_loop.post_to_session(
|
|
102
|
+
Phronomy::Event.new(
|
|
103
|
+
type: :driver_failed,
|
|
104
|
+
target_id: @id,
|
|
105
|
+
payload: {error: caught}
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def completed?
|
|
112
|
+
!failed? && @pending.empty? && @active.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def failed?
|
|
116
|
+
return true if @fatal_error
|
|
117
|
+
@on_error == :raise && @pending.empty? && @active.empty? && @child_errors.any?(&:itself)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def error
|
|
121
|
+
@fatal_error || @child_errors.find(&:itself)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def timed_out?
|
|
125
|
+
@timed_out
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def cancelled?
|
|
129
|
+
@cancelled
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def build_child_config(config)
|
|
135
|
+
config.dup
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def cancel_active_children!
|
|
139
|
+
@pending.clear
|
|
140
|
+
@active.each_value do |entry|
|
|
141
|
+
entry[:token]&.cancel!
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "state_machines"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module MultiAgent
|
|
7
|
+
# Builds and registers a fan-out coordination FSMSession.
|
|
8
|
+
class FanOutSessionBuilder
|
|
9
|
+
AUTO_STATE_SET = {idle: true}.freeze
|
|
10
|
+
DECLARED_STATES = %i[idle running completed failed timed_out cancelled].freeze
|
|
11
|
+
WAIT_STATES = [].freeze
|
|
12
|
+
|
|
13
|
+
def self.start(
|
|
14
|
+
invocation:,
|
|
15
|
+
timeout: nil,
|
|
16
|
+
cancellation_token: nil,
|
|
17
|
+
runtime: Phronomy::Runtime.instance
|
|
18
|
+
)
|
|
19
|
+
result = Phronomy::Task.deferred(name: "fan-out:#{invocation.id}")
|
|
20
|
+
|
|
21
|
+
if cancellation_token&.cancelled?
|
|
22
|
+
result.fail(Phronomy::CancellationError.new("fan-out cancelled"))
|
|
23
|
+
return result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
source = Phronomy::Task.deferred(name: "fan-out-source:#{invocation.id}")
|
|
27
|
+
source.on_complete do |context, error|
|
|
28
|
+
if error
|
|
29
|
+
result.fail(error)
|
|
30
|
+
elsif context.error
|
|
31
|
+
result.fail(context.error)
|
|
32
|
+
else
|
|
33
|
+
result.complete(context.results)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
session = build(invocation: invocation, runtime: runtime)
|
|
38
|
+
runtime.event_loop.register(session, completion: source)
|
|
39
|
+
|
|
40
|
+
if timeout
|
|
41
|
+
runtime.timer_queue.schedule(seconds: timeout) do
|
|
42
|
+
runtime.event_loop.post_to_session(
|
|
43
|
+
Phronomy::Event.new(
|
|
44
|
+
type: :timeout,
|
|
45
|
+
target_id: invocation.id,
|
|
46
|
+
payload: {message: "dispatch_parallel timed out after #{timeout}s"}
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
cancellation_token&.on_cancel do
|
|
53
|
+
runtime.event_loop.post_to_session(
|
|
54
|
+
Phronomy::Event.new(type: :cancel, target_id: invocation.id, payload: nil)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
result
|
|
59
|
+
rescue => error
|
|
60
|
+
result ||= Phronomy::Task.deferred(name: "fan-out:registration")
|
|
61
|
+
result.fail(error)
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.build(invocation:, runtime:)
|
|
66
|
+
phase_machine = build_phase_machine(runtime)
|
|
67
|
+
external_events = {
|
|
68
|
+
child_completed: [{from: :running, to: :running, guard: nil}],
|
|
69
|
+
driver_failed: [{from: :running, to: :failed, guard: nil}],
|
|
70
|
+
timeout: [{from: :running, to: :timed_out, guard: nil}],
|
|
71
|
+
cancel: [{from: :running, to: :cancelled, guard: nil}]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Phronomy::FSMSession.new(
|
|
75
|
+
id: invocation.id,
|
|
76
|
+
context: invocation,
|
|
77
|
+
entry_point: :idle,
|
|
78
|
+
entry_actions: {},
|
|
79
|
+
auto_state_set: AUTO_STATE_SET,
|
|
80
|
+
declared_states: DECLARED_STATES,
|
|
81
|
+
wait_state_names: WAIT_STATES,
|
|
82
|
+
external_events: external_events,
|
|
83
|
+
phase_machine_class: phase_machine,
|
|
84
|
+
recursion_limit: 10_000,
|
|
85
|
+
event_loop: runtime.event_loop
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
private_class_method :build
|
|
89
|
+
|
|
90
|
+
def self.build_phase_machine(runtime)
|
|
91
|
+
Class.new do
|
|
92
|
+
attr_accessor :context, :current_event
|
|
93
|
+
|
|
94
|
+
state_machine :phase, initial: :idle do
|
|
95
|
+
state :idle
|
|
96
|
+
state :running
|
|
97
|
+
state :completed
|
|
98
|
+
state :failed
|
|
99
|
+
state :timed_out
|
|
100
|
+
state :cancelled
|
|
101
|
+
|
|
102
|
+
event :state_completed do
|
|
103
|
+
transition idle: :running
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
event :child_completed do
|
|
107
|
+
transition running: :failed, if: ->(m) { m.context&.failed? }
|
|
108
|
+
transition running: :completed, if: ->(m) { m.context&.completed? }
|
|
109
|
+
transition running: :running
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
event(:driver_failed) { transition running: :failed }
|
|
113
|
+
event(:timeout) { transition running: :timed_out }
|
|
114
|
+
event(:cancel) { transition running: :cancelled }
|
|
115
|
+
|
|
116
|
+
after_transition to: :running do |machine|
|
|
117
|
+
machine.context.start_available!(runtime)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
private_class_method :build_phase_machine
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|