phronomy 0.17.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/CHANGELOG.md +97 -1134
- data/README.md +68 -1280
- data/benchmark/bench_regression.rb +25 -5
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
- 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_session_builder.rb +2 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +35 -19
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +6 -6
- data/lib/phronomy/agent/shared_state.rb +2 -0
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +97 -47
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/configuration.rb +5 -29
- 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 +48 -3
- 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 +45 -158
- 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/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 +147 -99
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
- 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/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +23 -31
- 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 -193
- 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/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -2,104 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
class Runtime
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Replaces the pattern of spawning one +Thread.new { sleep(t); callback }+
|
|
8
|
-
# per deadline. Any number of timers share a single background thread that
|
|
9
|
-
# sleeps until the earliest pending deadline.
|
|
10
|
-
#
|
|
11
|
-
# Use {#schedule} to register a one-shot callback; call {#shutdown} when the
|
|
12
|
-
# queue is no longer needed (e.g. on process exit) to stop the background
|
|
13
|
-
# thread cleanly.
|
|
5
|
+
# Threadless monotonic timer heap driven by EventLoop.
|
|
14
6
|
class TimerQueue
|
|
15
|
-
# @param clock [#call] zero-argument callable that returns the current
|
|
16
|
-
# monotonic time in seconds (defaults to +Process::CLOCK_MONOTONIC+).
|
|
17
|
-
# Override in tests to inject a fake clock.
|
|
18
|
-
# @api private
|
|
19
7
|
def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
20
8
|
@clock = clock
|
|
21
|
-
@heap = []
|
|
9
|
+
@heap = []
|
|
22
10
|
@mutex = Mutex.new
|
|
23
|
-
@cond = ConditionVariable.new
|
|
24
11
|
@stopped = false
|
|
25
|
-
@
|
|
26
|
-
|
|
12
|
+
@wake = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Installs a lightweight wake callback used when a newly scheduled timer
|
|
16
|
+
# may change EventLoop's current wait deadline.
|
|
17
|
+
def wake_with(&block)
|
|
18
|
+
@mutex.synchronize { @wake = block }
|
|
19
|
+
self
|
|
27
20
|
end
|
|
28
21
|
|
|
29
|
-
# Schedule a one-shot callback to fire after +seconds+ from now.
|
|
30
|
-
#
|
|
31
|
-
# @param seconds [Numeric] delay before the callback fires
|
|
32
|
-
# @yield called (in the timer thread) when the deadline is reached
|
|
33
|
-
# @return [self]
|
|
34
|
-
# @api private
|
|
35
22
|
def schedule(seconds:, &callback)
|
|
23
|
+
raise ArgumentError, "schedule requires a block" unless callback
|
|
24
|
+
|
|
36
25
|
fire_at = @clock.call + seconds.to_f
|
|
26
|
+
wake = nil
|
|
37
27
|
@mutex.synchronize do
|
|
38
28
|
raise Phronomy::PoolShutdownError, "TimerQueue has been shut down" if @stopped
|
|
39
|
-
|
|
40
|
-
@
|
|
29
|
+
|
|
30
|
+
previous_first = @heap.first&.first
|
|
31
|
+
@heap << [fire_at, callback]
|
|
32
|
+
@heap.sort_by!(&:first)
|
|
33
|
+
wake = @wake if previous_first.nil? || fire_at < previous_first
|
|
41
34
|
end
|
|
35
|
+
wake&.call
|
|
42
36
|
self
|
|
43
37
|
end
|
|
44
38
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
# @return [self]
|
|
48
|
-
# @api private
|
|
49
|
-
def shutdown
|
|
39
|
+
# Seconds until the next timer is due, nil when no timers are pending.
|
|
40
|
+
def seconds_until_next
|
|
50
41
|
@mutex.synchronize do
|
|
51
|
-
@stopped
|
|
52
|
-
@
|
|
42
|
+
return nil if @stopped || @heap.empty?
|
|
43
|
+
[@heap.first.first - @clock.call, 0.0].max
|
|
53
44
|
end
|
|
54
|
-
@thread.join
|
|
55
|
-
self
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Number of pending (not yet fired) callbacks. Primarily for testing.
|
|
59
|
-
# @return [Integer]
|
|
60
|
-
# @api private
|
|
61
|
-
def pending_count
|
|
62
|
-
@mutex.synchronize { @heap.size }
|
|
63
45
|
end
|
|
64
46
|
|
|
65
|
-
|
|
47
|
+
# Executes all callbacks whose deadline is due. Must be called by EventLoop.
|
|
48
|
+
def fire_due
|
|
49
|
+
callbacks = @mutex.synchronize do
|
|
50
|
+
return 0 if @stopped
|
|
66
51
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
now = @clock.call
|
|
53
|
+
due_count = @heap.bsearch_index { |(fire_at, _)| fire_at > now } || @heap.length
|
|
54
|
+
@heap.shift(due_count).map(&:last)
|
|
55
|
+
end
|
|
71
56
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
callback&.call
|
|
78
|
-
rescue => e
|
|
79
|
-
Phronomy.configuration.logger&.error { "[TimerQueue] callback raised #{e.class}: #{e.message}" }
|
|
57
|
+
callbacks.each do |callback|
|
|
58
|
+
callback.call
|
|
59
|
+
rescue => error
|
|
60
|
+
Phronomy.configuration.logger&.error do
|
|
61
|
+
"[TimerQueue] callback raised #{error.class}: #{error.message}"
|
|
80
62
|
end
|
|
81
63
|
end
|
|
64
|
+
callbacks.length
|
|
82
65
|
end
|
|
83
66
|
|
|
84
|
-
def
|
|
85
|
-
@mutex.synchronize
|
|
86
|
-
|
|
87
|
-
return :stopped if @stopped
|
|
67
|
+
def pending_count
|
|
68
|
+
@mutex.synchronize { @heap.size }
|
|
69
|
+
end
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return @heap.shift[1]
|
|
96
|
-
else
|
|
97
|
-
remaining = fire_at - now
|
|
98
|
-
@cond.wait(@mutex, remaining)
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
71
|
+
def shutdown
|
|
72
|
+
wake = @mutex.synchronize do
|
|
73
|
+
return self if @stopped
|
|
74
|
+
@stopped = true
|
|
75
|
+
@heap.clear
|
|
76
|
+
@wake
|
|
102
77
|
end
|
|
78
|
+
wake&.call
|
|
79
|
+
self
|
|
103
80
|
end
|
|
104
81
|
end
|
|
105
82
|
end
|
|
@@ -2,38 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
class Runtime
|
|
5
|
-
# Lazy
|
|
6
|
-
#
|
|
7
|
-
# Returns a {SchedulerTimerAdapter} when the backing scheduler is a
|
|
8
|
-
# {DeterministicScheduler} (enabling virtual-time integration for the
|
|
9
|
-
# `:fiber` backend), or a standard {TimerQueue} (OS-thread backed) for all
|
|
10
|
-
# other schedulers.
|
|
11
|
-
# @api private
|
|
5
|
+
# Lazy owner of the Runtime's threadless timer queue.
|
|
12
6
|
class TimerService
|
|
13
|
-
|
|
14
|
-
# @api private
|
|
15
|
-
def initialize(scheduler)
|
|
16
|
-
@scheduler = scheduler
|
|
7
|
+
def initialize
|
|
17
8
|
@mutex = Mutex.new
|
|
18
9
|
@timer = nil
|
|
10
|
+
@waker = nil
|
|
19
11
|
end
|
|
20
12
|
|
|
21
|
-
# Returns (or lazily creates) the timer queue for this runtime.
|
|
22
|
-
# @return [TimerQueue, SchedulerTimerAdapter]
|
|
23
|
-
# @api private
|
|
24
13
|
def timer_queue
|
|
25
14
|
@mutex.synchronize do
|
|
26
|
-
@timer ||=
|
|
27
|
-
|
|
28
|
-
else
|
|
29
|
-
TimerQueue.new
|
|
15
|
+
@timer ||= TimerQueue.new.tap do |timer|
|
|
16
|
+
timer.wake_with(&@waker) if @waker
|
|
30
17
|
end
|
|
31
18
|
end
|
|
32
19
|
end
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
def wake_with(&block)
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
@waker = block
|
|
24
|
+
@timer&.wake_with(&block)
|
|
25
|
+
end
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
37
29
|
def shutdown
|
|
38
30
|
@mutex.synchronize { @timer&.shutdown }
|
|
39
31
|
end
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "runtime/scheduler"
|
|
4
|
-
require_relative "runtime/thread_scheduler"
|
|
5
|
-
require_relative "runtime/fake_scheduler"
|
|
6
|
-
require_relative "runtime/deterministic_scheduler"
|
|
7
3
|
require_relative "runtime/timer_queue"
|
|
8
|
-
require_relative "runtime/scheduler_timer_adapter"
|
|
9
|
-
require_relative "runtime/task_registry"
|
|
10
|
-
require_relative "runtime/runtime_metrics"
|
|
11
4
|
require_relative "runtime/shutdown_result"
|
|
12
5
|
require_relative "runtime/timer_service"
|
|
13
6
|
|
|
14
7
|
module Phronomy
|
|
15
|
-
#
|
|
8
|
+
# Owns the EventLoop, offloaded synchronous work, timers and shutdown lifecycle.
|
|
9
|
+
#
|
|
10
|
+
# Runtime no longer schedules arbitrary Tasks. Framework control flow belongs
|
|
11
|
+
# to EventLoop/FSMSession; synchronous work that must not run on the EventLoop
|
|
12
|
+
# belongs to OffloadPool.
|
|
16
13
|
class Runtime
|
|
17
14
|
@instance_mutex = Mutex.new
|
|
18
15
|
|
|
19
16
|
class << self
|
|
20
17
|
def instance
|
|
21
18
|
instance_mutex.synchronize do
|
|
22
|
-
@instance ||=
|
|
19
|
+
@instance ||= new
|
|
23
20
|
end
|
|
24
21
|
end
|
|
25
22
|
|
|
@@ -65,54 +62,12 @@ module Phronomy
|
|
|
65
62
|
def instance_mutex
|
|
66
63
|
@instance_mutex ||= Mutex.new
|
|
67
64
|
end
|
|
68
|
-
|
|
69
|
-
def build_default_runtime
|
|
70
|
-
scheduler = case Phronomy.configuration.runtime_backend
|
|
71
|
-
when :thread
|
|
72
|
-
ThreadScheduler.new
|
|
73
|
-
when :immediate
|
|
74
|
-
FakeScheduler.new
|
|
75
|
-
when :fiber
|
|
76
|
-
Phronomy.configuration.logger&.warn(
|
|
77
|
-
"[phronomy] runtime_backend: :fiber uses DeterministicScheduler in autorun mode. " \
|
|
78
|
-
"This is an EXPERIMENTAL Fiber-based cooperative scheduler. " \
|
|
79
|
-
"Wall-clock timer integration is available via SchedulerTimerAdapter (Issues #331, #337). " \
|
|
80
|
-
"Not recommended for production use."
|
|
81
|
-
)
|
|
82
|
-
DeterministicScheduler.new(autorun: true)
|
|
83
|
-
else
|
|
84
|
-
raise Phronomy::ConfigurationError,
|
|
85
|
-
"unknown runtime_backend: #{Phronomy.configuration.runtime_backend.inspect}"
|
|
86
|
-
end
|
|
87
|
-
new(scheduler: scheduler)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def self.in_scheduler_context?
|
|
92
|
-
!Task.current.nil?
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def self.measure_ms
|
|
96
|
-
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
|
-
result = yield
|
|
98
|
-
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round
|
|
99
|
-
[result, elapsed_ms]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
attr_reader :scheduler
|
|
103
|
-
|
|
104
|
-
def state
|
|
105
|
-
@lifecycle_mutex.synchronize { @state }
|
|
106
65
|
end
|
|
107
66
|
|
|
108
|
-
def initialize
|
|
109
|
-
@
|
|
110
|
-
@event_loop_scheduler = ThreadScheduler.new
|
|
111
|
-
@task_registry = TaskRegistry.new
|
|
112
|
-
@metrics = RuntimeMetrics.new
|
|
113
|
-
@timer_service = TimerService.new(scheduler)
|
|
67
|
+
def initialize
|
|
68
|
+
@timer_service = TimerService.new
|
|
114
69
|
@pool_registry = Phronomy::Concurrency::PoolRegistry.new(
|
|
115
|
-
timer_queue_provider: -> {
|
|
70
|
+
timer_queue_provider: -> { timer_queue }
|
|
116
71
|
)
|
|
117
72
|
@lifecycle_mutex = Mutex.new
|
|
118
73
|
@shutdown_mutex = Mutex.new
|
|
@@ -122,73 +77,13 @@ module Phronomy
|
|
|
122
77
|
@shutdown_result = nil
|
|
123
78
|
end
|
|
124
79
|
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
slice_start = Task.current_cpu_slice_start_ms
|
|
128
|
-
if slice_start
|
|
129
|
-
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - slice_start
|
|
130
|
-
if elapsed > threshold
|
|
131
|
-
name = Task.current&.name || "unknown"
|
|
132
|
-
Phronomy.configuration.logger&.warn(
|
|
133
|
-
"[Phronomy] CPU-bound task detected: '#{name}' ran #{elapsed.round}ms " \
|
|
134
|
-
"without yielding (threshold: #{threshold}ms)"
|
|
135
|
-
)
|
|
136
|
-
@metrics.increment_starvation
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
Task.record_yield!
|
|
141
|
-
@scheduler.yield
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def non_yield_threshold_violation_count
|
|
145
|
-
@metrics.starvation_count
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def yield_if_needed(every: 1000)
|
|
149
|
-
self.yield if (Task.increment_yield_counter! % every).zero?
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def task_group(limit: Float::INFINITY, failure_policy: :fail_fast)
|
|
153
|
-
ensure_accepting_work!
|
|
154
|
-
TaskGroup.new(limit: limit, failure_policy: failure_policy, runtime: self)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def spawn(name: nil, &block)
|
|
158
|
-
ensure_accepting_work!
|
|
159
|
-
type = _task_type(name)
|
|
160
|
-
spawn_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
161
|
-
@metrics.record_start(type)
|
|
162
|
-
|
|
163
|
-
task = @scheduler.spawn(name: name, parent: Task.current) do
|
|
164
|
-
run_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
165
|
-
@metrics.record_wait(run_start - spawn_at)
|
|
166
|
-
begin
|
|
167
|
-
result = block.call
|
|
168
|
-
@metrics.record_end(type, :completed, run_start)
|
|
169
|
-
result
|
|
170
|
-
rescue CancellationError
|
|
171
|
-
@metrics.record_end(type, :cancelled, run_start)
|
|
172
|
-
raise
|
|
173
|
-
rescue => error
|
|
174
|
-
@metrics.record_end(type, :failed, run_start)
|
|
175
|
-
raise error
|
|
176
|
-
ensure
|
|
177
|
-
current = Task.current
|
|
178
|
-
@task_registry.deregister(current) if current
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
@task_registry.register(task)
|
|
182
|
-
task
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def task_snapshot
|
|
186
|
-
@metrics.snapshot
|
|
80
|
+
def state
|
|
81
|
+
@lifecycle_mutex.synchronize { @state }
|
|
187
82
|
end
|
|
188
83
|
|
|
189
|
-
def
|
|
190
|
-
pool_size: Phronomy.configuration.
|
|
191
|
-
queue_size: Phronomy.configuration.
|
|
84
|
+
def offload(
|
|
85
|
+
pool_size: Phronomy.configuration.offload_pool_size,
|
|
86
|
+
queue_size: Phronomy.configuration.offload_queue_size
|
|
192
87
|
)
|
|
193
88
|
ensure_accepting_work!
|
|
194
89
|
@pool_registry.default_pool(pool_size: pool_size, queue_size: queue_size)
|
|
@@ -199,8 +94,16 @@ module Phronomy
|
|
|
199
94
|
@pool_registry.named_pool(name, size: size, queue_size: queue_size)
|
|
200
95
|
end
|
|
201
96
|
|
|
97
|
+
# Public timer access also ensures that the EventLoop that drives timers is alive.
|
|
202
98
|
def timer_queue
|
|
203
99
|
ensure_accepting_work!
|
|
100
|
+
timer = @timer_service.timer_queue
|
|
101
|
+
event_loop
|
|
102
|
+
timer
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Internal EventLoop access that does not recursively initialise EventLoop.
|
|
106
|
+
def __timer_queue
|
|
204
107
|
@timer_service.timer_queue
|
|
205
108
|
end
|
|
206
109
|
|
|
@@ -208,7 +111,11 @@ module Phronomy
|
|
|
208
111
|
@lifecycle_mutex.synchronize do
|
|
209
112
|
case @state
|
|
210
113
|
when :running
|
|
211
|
-
@event_loop
|
|
114
|
+
unless @event_loop
|
|
115
|
+
@event_loop = EventLoop.new(runtime: self)
|
|
116
|
+
@timer_service.wake_with { @event_loop&.wake }
|
|
117
|
+
end
|
|
118
|
+
@event_loop
|
|
212
119
|
when :draining
|
|
213
120
|
return @event_loop if @event_loop
|
|
214
121
|
|
|
@@ -222,12 +129,8 @@ module Phronomy
|
|
|
222
129
|
end
|
|
223
130
|
|
|
224
131
|
def event_loop_current?
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def __spawn_event_loop_service(&block)
|
|
230
|
-
@event_loop_scheduler.spawn(name: "event-loop", parent: nil, &block)
|
|
132
|
+
loop_instance = @lifecycle_mutex.synchronize { @event_loop }
|
|
133
|
+
loop_instance&.current? || false
|
|
231
134
|
end
|
|
232
135
|
|
|
233
136
|
def __event_loop_failed(error)
|
|
@@ -243,7 +146,7 @@ module Phronomy
|
|
|
243
146
|
timeout: Phronomy.configuration.event_loop_stop_grace_seconds,
|
|
244
147
|
cancel_grace: timeout
|
|
245
148
|
)
|
|
246
|
-
if
|
|
149
|
+
if event_loop_current?
|
|
247
150
|
raise Phronomy::RuntimeShutdownReentrancyError,
|
|
248
151
|
"Runtime#shutdown must be called from an external management thread"
|
|
249
152
|
end
|
|
@@ -253,29 +156,33 @@ module Phronomy
|
|
|
253
156
|
@shutdown_mutex.synchronize do
|
|
254
157
|
return @shutdown_result if @shutdown_result
|
|
255
158
|
|
|
256
|
-
|
|
257
|
-
|
|
159
|
+
# Phase 1 — drain sessions with the full configured grace.
|
|
160
|
+
drain_deadline = monotonic_now + timeout
|
|
161
|
+
loop_instance = @lifecycle_mutex.synchronize do
|
|
258
162
|
@state = :draining unless @state == :failed
|
|
259
163
|
@event_loop
|
|
260
164
|
end
|
|
261
|
-
|
|
165
|
+
loop_instance&.begin_draining
|
|
262
166
|
|
|
263
|
-
|
|
167
|
+
loop_idle = !loop_instance || loop_instance.wait_until_idle(drain_deadline)
|
|
264
168
|
|
|
265
169
|
@lifecycle_mutex.synchronize do
|
|
266
170
|
@state = :stopping unless @state == :failed
|
|
267
171
|
end
|
|
268
172
|
|
|
269
|
-
|
|
270
|
-
|
|
173
|
+
# Phase 2 — stop the EventLoop thread with a short independent budget.
|
|
174
|
+
# An idle EventLoop processes STOP and exits in < 1ms normally; the 0.2s
|
|
175
|
+
# budget here is only a safety net for OS scheduling jitter.
|
|
176
|
+
stop_deadline = monotonic_now + [cancel_grace.to_f, 0.2].max
|
|
177
|
+
event_loop_status = if loop_instance
|
|
178
|
+
loop_instance.stop_and_join(deadline: stop_deadline)
|
|
271
179
|
else
|
|
272
180
|
:not_started
|
|
273
181
|
end
|
|
274
182
|
|
|
275
183
|
subsystem_error = shutdown_pools_and_timer
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
(!event_loop || !event_loop.task_alive?) &&
|
|
184
|
+
cleanup_complete = loop_idle &&
|
|
185
|
+
(!loop_instance || !loop_instance.thread_alive?) &&
|
|
279
186
|
event_loop_status != :cancel_timeout &&
|
|
280
187
|
subsystem_error.nil?
|
|
281
188
|
|
|
@@ -285,11 +192,12 @@ module Phronomy
|
|
|
285
192
|
else
|
|
286
193
|
:terminated
|
|
287
194
|
end
|
|
195
|
+
|
|
288
196
|
result = ShutdownResult.new(
|
|
289
197
|
runtime_outcome: runtime_outcome,
|
|
290
198
|
cleanup_status: cleanup_complete ? :complete : :incomplete,
|
|
291
199
|
event_loop_status: event_loop_status,
|
|
292
|
-
task_registry_status:
|
|
200
|
+
task_registry_status: :empty,
|
|
293
201
|
error: failure
|
|
294
202
|
)
|
|
295
203
|
|
|
@@ -311,17 +219,6 @@ module Phronomy
|
|
|
311
219
|
"Runtime is #{current_state}; new work is not accepted"
|
|
312
220
|
end
|
|
313
221
|
|
|
314
|
-
def drain_runtime_work(event_loop, deadline)
|
|
315
|
-
loop do
|
|
316
|
-
task_status = @task_registry.drain_until(deadline)
|
|
317
|
-
return :timeout if task_status == :timeout
|
|
318
|
-
|
|
319
|
-
event_loop_idle = !event_loop || event_loop.wait_until_idle(deadline)
|
|
320
|
-
return :timeout unless event_loop_idle
|
|
321
|
-
return :empty if @task_registry.empty?
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
222
|
def shutdown_pools_and_timer
|
|
326
223
|
error = nil
|
|
327
224
|
begin
|
|
@@ -347,15 +244,5 @@ module Phronomy
|
|
|
347
244
|
def monotonic_now
|
|
348
245
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
349
246
|
end
|
|
350
|
-
|
|
351
|
-
TASK_TYPE_PREFIXES = %w[agent tool workflow rag llm vector].freeze
|
|
352
|
-
private_constant :TASK_TYPE_PREFIXES
|
|
353
|
-
|
|
354
|
-
def _task_type(name)
|
|
355
|
-
return :other if name.nil?
|
|
356
|
-
|
|
357
|
-
prefix = TASK_TYPE_PREFIXES.find { |candidate| name.to_s.start_with?("#{candidate}-") }
|
|
358
|
-
prefix ? prefix.to_sym : :other
|
|
359
|
-
end
|
|
360
247
|
end
|
|
361
248
|
end
|