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,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Concurrency
|
|
5
|
-
# Registry and lifecycle manager for {
|
|
5
|
+
# Registry and lifecycle manager for {OffloadPool} instances.
|
|
6
6
|
#
|
|
7
7
|
# Maintains one unnamed "default" pool (accessed via {#default_pool}) and
|
|
8
8
|
# an arbitrary number of named pools (accessed via {#named_pool}).
|
|
@@ -21,10 +21,10 @@ module Phronomy
|
|
|
21
21
|
# Returns (or lazily creates) the unnamed default pool.
|
|
22
22
|
# @param pool_size [Integer]
|
|
23
23
|
# @param queue_size [Integer]
|
|
24
|
-
# @return [
|
|
24
|
+
# @return [OffloadPool]
|
|
25
25
|
# @api private
|
|
26
26
|
def default_pool(pool_size: 10, queue_size: 100)
|
|
27
|
-
@default ||=
|
|
27
|
+
@default ||= OffloadPool.new(
|
|
28
28
|
name: :default,
|
|
29
29
|
pool_size: pool_size,
|
|
30
30
|
queue_size: queue_size,
|
|
@@ -36,11 +36,11 @@ module Phronomy
|
|
|
36
36
|
# @param name [Symbol, String]
|
|
37
37
|
# @param size [Integer]
|
|
38
38
|
# @param queue_size [Integer]
|
|
39
|
-
# @return [
|
|
39
|
+
# @return [OffloadPool]
|
|
40
40
|
# @api private
|
|
41
41
|
def named_pool(name, size: 10, queue_size: 100)
|
|
42
42
|
@mutex.synchronize do
|
|
43
|
-
@pools[name.to_sym] ||=
|
|
43
|
+
@pools[name.to_sym] ||= OffloadPool.new(
|
|
44
44
|
name: name,
|
|
45
45
|
pool_size: size,
|
|
46
46
|
queue_size: queue_size,
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
# Runtime-owned FIFO event loop for FSMSession instances.
|
|
5
|
+
#
|
|
6
|
+
# EventLoop owns the framework's sole control-plane OS thread. All session
|
|
7
|
+
# lifecycle progression happens by short event dispatches on this thread.
|
|
5
8
|
class EventLoop
|
|
6
9
|
SYSTEM_CHANNEL_ID = "__event_loop__"
|
|
7
10
|
|
|
@@ -12,7 +15,8 @@ module Phronomy
|
|
|
12
15
|
private_constant :TERMINAL_MANAGEMENT_EVENTS
|
|
13
16
|
|
|
14
17
|
STOP = Object.new.freeze
|
|
15
|
-
|
|
18
|
+
WAKE = Object.new.freeze
|
|
19
|
+
private_constant :STOP, :WAKE
|
|
16
20
|
|
|
17
21
|
def initialize(runtime:)
|
|
18
22
|
@runtime = runtime
|
|
@@ -22,13 +26,8 @@ module Phronomy
|
|
|
22
26
|
@max_queue_depth = 0
|
|
23
27
|
@last_queue_backlog_warning_at = nil
|
|
24
28
|
|
|
25
|
-
# @fsms and @waiting are dispatcher-thread-owned.
|
|
26
29
|
@fsms = {}
|
|
27
30
|
@waiting = {}
|
|
28
|
-
|
|
29
|
-
# Admission is shared by caller threads and the dispatcher. A session ID
|
|
30
|
-
# enters this set before its :start event is queued and leaves when its
|
|
31
|
-
# terminal management event is queued.
|
|
32
31
|
@admitted_session_ids = Set.new
|
|
33
32
|
|
|
34
33
|
@lifecycle_mutex = Mutex.new
|
|
@@ -36,7 +35,6 @@ module Phronomy
|
|
|
36
35
|
@shutdown_mutex = Mutex.new
|
|
37
36
|
@state = :running
|
|
38
37
|
@outstanding_sessions = 0
|
|
39
|
-
@cancel_requested = false
|
|
40
38
|
@shutdown_status = nil
|
|
41
39
|
|
|
42
40
|
@lag_mutex = Mutex.new
|
|
@@ -45,7 +43,8 @@ module Phronomy
|
|
|
45
43
|
@dispatch_count = 0
|
|
46
44
|
@total_lag_ns = 0
|
|
47
45
|
|
|
48
|
-
@
|
|
46
|
+
@thread = Thread.new { run_loop }
|
|
47
|
+
@thread.name = "phronomy-event-loop"
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
def last_lag_seconds
|
|
@@ -59,7 +58,6 @@ module Phronomy
|
|
|
59
58
|
def average_lag_seconds
|
|
60
59
|
@lag_mutex.synchronize do
|
|
61
60
|
return 0.0 if @dispatch_count.zero?
|
|
62
|
-
|
|
63
61
|
@total_lag_ns.to_f / @dispatch_count / 1_000_000_000.0
|
|
64
62
|
end
|
|
65
63
|
end
|
|
@@ -79,21 +77,11 @@ module Phronomy
|
|
|
79
77
|
"Schedule work asynchronously instead."
|
|
80
78
|
end
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
completion || Phronomy::Concurrency::AsyncQueue.new
|
|
84
|
-
scheduler = Phronomy::Runtime::Scheduler.current
|
|
85
|
-
if scheduler &&
|
|
86
|
-
completion_queue.respond_to?(:expect_cross_thread_push)
|
|
87
|
-
completion_queue.expect_cross_thread_push(scheduler)
|
|
88
|
-
end
|
|
89
|
-
|
|
80
|
+
completion_handle = completion || Phronomy::Concurrency::AsyncQueue.new
|
|
90
81
|
event = Phronomy::Event.new(
|
|
91
82
|
type: :start,
|
|
92
83
|
target_id: SYSTEM_CHANNEL_ID,
|
|
93
|
-
payload: {
|
|
94
|
-
session: fsm_session,
|
|
95
|
-
completion: completion_queue
|
|
96
|
-
}
|
|
84
|
+
payload: {session: fsm_session, completion: completion_handle}
|
|
97
85
|
)
|
|
98
86
|
queued_depth = nil
|
|
99
87
|
|
|
@@ -107,9 +95,7 @@ module Phronomy
|
|
|
107
95
|
@admitted_session_ids.add(fsm_session.id)
|
|
108
96
|
@outstanding_sessions += 1
|
|
109
97
|
begin
|
|
110
|
-
queued_depth = enqueue(
|
|
111
|
-
[event, monotonic_nanoseconds]
|
|
112
|
-
)
|
|
98
|
+
queued_depth = enqueue([event, monotonic_nanoseconds])
|
|
113
99
|
rescue
|
|
114
100
|
@admitted_session_ids.delete(fsm_session.id)
|
|
115
101
|
@outstanding_sessions -= 1
|
|
@@ -119,11 +105,9 @@ module Phronomy
|
|
|
119
105
|
end
|
|
120
106
|
|
|
121
107
|
check_queue_backlog(queued_depth, event)
|
|
122
|
-
|
|
108
|
+
completion_handle
|
|
123
109
|
end
|
|
124
110
|
|
|
125
|
-
# Internal post operation. Management terminal events close admission for
|
|
126
|
-
# their session before the terminal event is enqueued.
|
|
127
111
|
def post(event)
|
|
128
112
|
queued_depth = nil
|
|
129
113
|
accepted = @lifecycle_mutex.synchronize do
|
|
@@ -136,9 +120,7 @@ module Phronomy
|
|
|
136
120
|
end
|
|
137
121
|
|
|
138
122
|
begin
|
|
139
|
-
queued_depth = enqueue(
|
|
140
|
-
[event, monotonic_nanoseconds]
|
|
141
|
-
)
|
|
123
|
+
queued_depth = enqueue([event, monotonic_nanoseconds])
|
|
142
124
|
rescue
|
|
143
125
|
@admitted_session_ids.add(terminal_session_id) if terminal_session_id
|
|
144
126
|
raise
|
|
@@ -151,15 +133,9 @@ module Phronomy
|
|
|
151
133
|
true
|
|
152
134
|
end
|
|
153
135
|
|
|
154
|
-
# Posts an event only when the target session has been admitted and has not
|
|
155
|
-
# queued a terminal management event. The event remains FIFO with all other
|
|
156
|
-
# EventLoop work.
|
|
157
|
-
#
|
|
158
|
-
# A true result reports admission, not transition success.
|
|
159
136
|
def post_to_session(event)
|
|
160
137
|
if event.target_id == SYSTEM_CHANNEL_ID
|
|
161
|
-
raise ArgumentError,
|
|
162
|
-
"post_to_session cannot target the system channel"
|
|
138
|
+
raise ArgumentError, "post_to_session cannot target the system channel"
|
|
163
139
|
end
|
|
164
140
|
|
|
165
141
|
queued_depth = nil
|
|
@@ -167,9 +143,7 @@ module Phronomy
|
|
|
167
143
|
next false unless accepting_events?
|
|
168
144
|
next false unless @admitted_session_ids.include?(event.target_id)
|
|
169
145
|
|
|
170
|
-
queued_depth = enqueue(
|
|
171
|
-
[event, monotonic_nanoseconds]
|
|
172
|
-
)
|
|
146
|
+
queued_depth = enqueue([event, monotonic_nanoseconds])
|
|
173
147
|
true
|
|
174
148
|
end
|
|
175
149
|
return false unless accepted
|
|
@@ -178,14 +152,20 @@ module Phronomy
|
|
|
178
152
|
true
|
|
179
153
|
end
|
|
180
154
|
|
|
155
|
+
# Interrupts the queue wait so EventLoop can recompute the next timer deadline.
|
|
156
|
+
def wake
|
|
157
|
+
@queue.push(WAKE)
|
|
158
|
+
true
|
|
159
|
+
rescue ClosedQueueError
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
|
|
181
163
|
def admitted_session?(session_id)
|
|
182
|
-
@lifecycle_mutex.synchronize
|
|
183
|
-
@admitted_session_ids.include?(session_id)
|
|
184
|
-
end
|
|
164
|
+
@lifecycle_mutex.synchronize { @admitted_session_ids.include?(session_id) }
|
|
185
165
|
end
|
|
186
166
|
|
|
187
167
|
def current?
|
|
188
|
-
|
|
168
|
+
Thread.current.equal?(@thread)
|
|
189
169
|
end
|
|
190
170
|
|
|
191
171
|
def state
|
|
@@ -200,9 +180,7 @@ module Phronomy
|
|
|
200
180
|
end
|
|
201
181
|
|
|
202
182
|
def idle?
|
|
203
|
-
@lifecycle_mutex.synchronize
|
|
204
|
-
@outstanding_sessions.zero?
|
|
205
|
-
end
|
|
183
|
+
@lifecycle_mutex.synchronize { @outstanding_sessions.zero? }
|
|
206
184
|
end
|
|
207
185
|
|
|
208
186
|
def wait_until_idle(deadline)
|
|
@@ -210,14 +188,15 @@ module Phronomy
|
|
|
210
188
|
until @outstanding_sessions.zero?
|
|
211
189
|
remaining = deadline - monotonic_now
|
|
212
190
|
return false if remaining <= 0
|
|
213
|
-
|
|
214
191
|
@idle_cond.wait(@lifecycle_mutex, remaining)
|
|
215
192
|
end
|
|
216
193
|
true
|
|
217
194
|
end
|
|
218
195
|
end
|
|
219
196
|
|
|
220
|
-
|
|
197
|
+
# Sends STOP to the queue and joins the EventLoop thread.
|
|
198
|
+
# Assumes sessions have already been drained before this call.
|
|
199
|
+
def stop_and_join(deadline:)
|
|
221
200
|
@shutdown_mutex.synchronize do
|
|
222
201
|
return @shutdown_status if @shutdown_status
|
|
223
202
|
|
|
@@ -227,32 +206,40 @@ module Phronomy
|
|
|
227
206
|
return @shutdown_status
|
|
228
207
|
end
|
|
229
208
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
begin_stopping_if_idle
|
|
233
|
-
join_until(deadline)
|
|
234
|
-
end
|
|
209
|
+
begin_stopping_if_idle
|
|
210
|
+
join_until(deadline)
|
|
235
211
|
|
|
236
|
-
@shutdown_status =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
212
|
+
@shutdown_status = if thread_alive?
|
|
213
|
+
@lifecycle_mutex.synchronize { @state = :failed }
|
|
214
|
+
:cancel_timeout
|
|
215
|
+
elsif state == :failed
|
|
216
|
+
:failed
|
|
217
|
+
else
|
|
218
|
+
finalize_terminated(:terminated)
|
|
219
|
+
end
|
|
244
220
|
end
|
|
245
221
|
end
|
|
246
222
|
|
|
247
|
-
|
|
248
|
-
|
|
223
|
+
# Legacy entry point kept for any callers that pass deadline:/cancel_grace:.
|
|
224
|
+
def shutdown(deadline:, cancel_grace: deadline)
|
|
225
|
+
stop_and_join(deadline: deadline)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def thread_alive?
|
|
229
|
+
@thread&.alive? || false
|
|
249
230
|
end
|
|
250
231
|
|
|
232
|
+
# Compatibility for existing runtime/shutdown observers during migration.
|
|
233
|
+
alias_method :task_alive?, :thread_alive?
|
|
234
|
+
|
|
251
235
|
private
|
|
252
236
|
|
|
253
237
|
def run_loop
|
|
254
238
|
loop do
|
|
255
|
-
|
|
239
|
+
fire_due_timers
|
|
240
|
+
timeout = @runtime.__timer_queue.seconds_until_next
|
|
241
|
+
item = dequeue(timeout: timeout)
|
|
242
|
+
next if item.nil? || item.equal?(WAKE)
|
|
256
243
|
break if item.equal?(STOP)
|
|
257
244
|
|
|
258
245
|
event, posted_at_ns = item
|
|
@@ -265,24 +252,16 @@ module Phronomy
|
|
|
265
252
|
dispatch(event)
|
|
266
253
|
check_dispatch_time(dispatch_start_ns, event)
|
|
267
254
|
end
|
|
268
|
-
|
|
269
|
-
if shutdown_cancel_requested?
|
|
270
|
-
cleanup_abandoned_work(
|
|
271
|
-
Phronomy::CancellationError.new(
|
|
272
|
-
"Runtime shutdown timed out"
|
|
273
|
-
)
|
|
274
|
-
)
|
|
275
|
-
else
|
|
276
|
-
notify_unexpected_dispatcher_failure(error)
|
|
277
|
-
raise
|
|
278
|
-
end
|
|
255
|
+
fire_due_timers
|
|
279
256
|
rescue => error
|
|
280
257
|
notify_unexpected_dispatcher_failure(error)
|
|
281
258
|
raise
|
|
282
259
|
ensure
|
|
283
|
-
@lifecycle_mutex.synchronize
|
|
284
|
-
|
|
285
|
-
|
|
260
|
+
@lifecycle_mutex.synchronize { @idle_cond.broadcast }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def fire_due_timers
|
|
264
|
+
@runtime.__timer_queue.fire_due
|
|
286
265
|
end
|
|
287
266
|
|
|
288
267
|
def dispatch(event)
|
|
@@ -308,11 +287,9 @@ module Phronomy
|
|
|
308
287
|
session_id = event.payload.fetch(:session_id)
|
|
309
288
|
session = @fsms.delete(session_id)
|
|
310
289
|
waiter = @waiting.delete(session_id)
|
|
311
|
-
|
|
312
|
-
waiter,
|
|
313
|
-
event.payload.fetch(:result)
|
|
314
|
-
)
|
|
290
|
+
# decrement before waking caller so wait_until_idle sees zero immediately
|
|
315
291
|
decrement_outstanding if session
|
|
292
|
+
complete_waiter(waiter, event.payload.fetch(:result))
|
|
316
293
|
when :start
|
|
317
294
|
session = event.payload.fetch(:session)
|
|
318
295
|
waiter = event.payload[:completion]
|
|
@@ -320,6 +297,8 @@ module Phronomy
|
|
|
320
297
|
@waiting[session.id] = waiter if waiter
|
|
321
298
|
session.start
|
|
322
299
|
when :agent_terminal_ready
|
|
300
|
+
# Existing Agent terminalisation is retained in this proposal. A separate
|
|
301
|
+
# follow-up may move it to an Agent-owned terminalising FSM state.
|
|
323
302
|
cmd = event.payload.fetch(:command)
|
|
324
303
|
cmd.coordinator.deliver_on_event_loop(cmd)
|
|
325
304
|
end
|
|
@@ -338,59 +317,22 @@ module Phronomy
|
|
|
338
317
|
return false unless @outstanding_sessions.zero?
|
|
339
318
|
|
|
340
319
|
@state = :stopping
|
|
341
|
-
|
|
320
|
+
@queue.push(STOP)
|
|
342
321
|
true
|
|
343
322
|
end
|
|
344
323
|
end
|
|
345
324
|
|
|
346
|
-
def cancel_and_cleanup(cancel_grace)
|
|
347
|
-
task = @task
|
|
348
|
-
@lifecycle_mutex.synchronize do
|
|
349
|
-
@state = :stopping unless @state == :failed
|
|
350
|
-
@cancel_requested = true
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
task&.cancel!
|
|
354
|
-
begin
|
|
355
|
-
task&.join(cancel_grace)
|
|
356
|
-
rescue
|
|
357
|
-
nil
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
if task&.alive?
|
|
361
|
-
@lifecycle_mutex.synchronize do
|
|
362
|
-
@state = :failed
|
|
363
|
-
end
|
|
364
|
-
return :cancel_timeout
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
return :failed if state == :failed
|
|
368
|
-
|
|
369
|
-
cleanup_abandoned_work(
|
|
370
|
-
Phronomy::CancellationError.new(
|
|
371
|
-
"Runtime shutdown timed out"
|
|
372
|
-
)
|
|
373
|
-
)
|
|
374
|
-
finalize_terminated(:cancelled)
|
|
375
|
-
end
|
|
376
|
-
|
|
377
325
|
def cleanup_abandoned_work(error)
|
|
378
326
|
drain_queued_items.each do |item|
|
|
379
|
-
next if item.equal?(STOP)
|
|
327
|
+
next if item.equal?(STOP) || item.equal?(WAKE)
|
|
380
328
|
|
|
381
329
|
event, = item
|
|
382
330
|
next unless event.target_id == SYSTEM_CHANNEL_ID
|
|
383
331
|
next unless event.type == :start
|
|
384
|
-
|
|
385
|
-
complete_waiter(
|
|
386
|
-
event.payload[:completion],
|
|
387
|
-
error
|
|
388
|
-
)
|
|
332
|
+
complete_waiter(event.payload[:completion], error)
|
|
389
333
|
end
|
|
390
334
|
|
|
391
|
-
@waiting.values.each
|
|
392
|
-
complete_waiter(waiter, error)
|
|
393
|
-
end
|
|
335
|
+
@waiting.values.each { |waiter| complete_waiter(waiter, error) }
|
|
394
336
|
@waiting.clear
|
|
395
337
|
@fsms.clear
|
|
396
338
|
@lifecycle_mutex.synchronize do
|
|
@@ -405,7 +347,6 @@ module Phronomy
|
|
|
405
347
|
loop do
|
|
406
348
|
item = dequeue(timeout: 0)
|
|
407
349
|
break unless item
|
|
408
|
-
|
|
409
350
|
items << item
|
|
410
351
|
end
|
|
411
352
|
items
|
|
@@ -417,34 +358,23 @@ module Phronomy
|
|
|
417
358
|
@admitted_session_ids.clear
|
|
418
359
|
@idle_cond.broadcast
|
|
419
360
|
end
|
|
420
|
-
|
|
421
|
-
complete_waiter(waiter, error)
|
|
422
|
-
end
|
|
361
|
+
cleanup_abandoned_work(error)
|
|
423
362
|
@runtime.__event_loop_failed(error)
|
|
424
363
|
end
|
|
425
364
|
|
|
426
|
-
def shutdown_cancel_requested?
|
|
427
|
-
@lifecycle_mutex.synchronize do
|
|
428
|
-
@cancel_requested && @state == :stopping
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
|
|
432
365
|
def accepting_events?
|
|
433
366
|
%i[running draining].include?(@state)
|
|
434
367
|
end
|
|
435
368
|
|
|
436
369
|
def ensure_accepting_registrations!
|
|
437
370
|
return if accepting_events?
|
|
438
|
-
|
|
439
371
|
raise Phronomy::RuntimeShutdownError,
|
|
440
372
|
"EventLoop is #{@state}; new sessions are not accepted"
|
|
441
373
|
end
|
|
442
374
|
|
|
443
375
|
def decrement_outstanding
|
|
444
376
|
@lifecycle_mutex.synchronize do
|
|
445
|
-
if @outstanding_sessions.positive?
|
|
446
|
-
@outstanding_sessions -= 1
|
|
447
|
-
end
|
|
377
|
+
@outstanding_sessions -= 1 if @outstanding_sessions.positive?
|
|
448
378
|
@idle_cond.broadcast if @outstanding_sessions.zero?
|
|
449
379
|
end
|
|
450
380
|
end
|
|
@@ -452,19 +382,16 @@ module Phronomy
|
|
|
452
382
|
def join_until(deadline)
|
|
453
383
|
remaining = deadline - monotonic_now
|
|
454
384
|
return if remaining <= 0
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
rescue
|
|
459
|
-
nil
|
|
460
|
-
end
|
|
385
|
+
@thread&.join(remaining)
|
|
386
|
+
rescue
|
|
387
|
+
nil
|
|
461
388
|
end
|
|
462
389
|
|
|
463
390
|
def finalize_terminated(status)
|
|
464
391
|
@lifecycle_mutex.synchronize do
|
|
465
392
|
@state = :terminated
|
|
466
393
|
@admitted_session_ids.clear
|
|
467
|
-
@
|
|
394
|
+
@thread = nil unless @thread&.alive?
|
|
468
395
|
@idle_cond.broadcast
|
|
469
396
|
end
|
|
470
397
|
status
|
|
@@ -474,16 +401,7 @@ module Phronomy
|
|
|
474
401
|
return unless waiter
|
|
475
402
|
|
|
476
403
|
if waiter.is_a?(Phronomy::Task)
|
|
477
|
-
|
|
478
|
-
waiter.backend.unblock(nil, payload)
|
|
479
|
-
waiter.transition!(:failed, error: payload)
|
|
480
|
-
else
|
|
481
|
-
waiter.backend.unblock(payload, nil)
|
|
482
|
-
waiter.transition!(
|
|
483
|
-
:completed,
|
|
484
|
-
value: payload
|
|
485
|
-
)
|
|
486
|
-
end
|
|
404
|
+
payload.is_a?(Exception) ? waiter.fail(payload) : waiter.complete(payload)
|
|
487
405
|
else
|
|
488
406
|
waiter.push(payload)
|
|
489
407
|
end
|
|
@@ -492,9 +410,7 @@ module Phronomy
|
|
|
492
410
|
def enqueue(item)
|
|
493
411
|
depth = @queue_metrics_mutex.synchronize do
|
|
494
412
|
@queue_depth += 1
|
|
495
|
-
if @queue_depth > @max_queue_depth
|
|
496
|
-
@max_queue_depth = @queue_depth
|
|
497
|
-
end
|
|
413
|
+
@max_queue_depth = @queue_depth if @queue_depth > @max_queue_depth
|
|
498
414
|
@queue_depth
|
|
499
415
|
end
|
|
500
416
|
@queue.push(item)
|
|
@@ -511,7 +427,7 @@ module Phronomy
|
|
|
511
427
|
begin
|
|
512
428
|
item = @queue.pop(timeout: timeout)
|
|
513
429
|
ensure
|
|
514
|
-
if item
|
|
430
|
+
if item && !item.equal?(WAKE) && !item.equal?(STOP)
|
|
515
431
|
@queue_metrics_mutex.synchronize do
|
|
516
432
|
@queue_depth -= 1 if @queue_depth.positive?
|
|
517
433
|
end
|
|
@@ -527,12 +443,9 @@ module Phronomy
|
|
|
527
443
|
max_depth = nil
|
|
528
444
|
should_warn = @queue_metrics_mutex.synchronize do
|
|
529
445
|
last = @last_queue_backlog_warning_at
|
|
530
|
-
if last &&
|
|
531
|
-
(now - last) <
|
|
532
|
-
QUEUE_BACKLOG_WARNING_INTERVAL_SECONDS
|
|
446
|
+
if last && (now - last) < QUEUE_BACKLOG_WARNING_INTERVAL_SECONDS
|
|
533
447
|
next false
|
|
534
448
|
end
|
|
535
|
-
|
|
536
449
|
@last_queue_backlog_warning_at = now
|
|
537
450
|
max_depth = @max_queue_depth
|
|
538
451
|
true
|
|
@@ -541,12 +454,9 @@ module Phronomy
|
|
|
541
454
|
|
|
542
455
|
warn_queue_backlog(
|
|
543
456
|
"[Phronomy::EventLoop] Queue backlog is high: " \
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
"target_id=#{event.target_id.inspect}. " \
|
|
548
|
-
"Events are not dropped; inspect slow callbacks or " \
|
|
549
|
-
"high streaming concurrency."
|
|
457
|
+
"depth=#{depth} max_depth=#{max_depth} " \
|
|
458
|
+
"threshold=#{QUEUE_BACKLOG_WARNING_THRESHOLD} " \
|
|
459
|
+
"event=#{event.type.inspect} target_id=#{event.target_id.inspect}."
|
|
550
460
|
)
|
|
551
461
|
end
|
|
552
462
|
|
|
@@ -567,38 +477,30 @@ module Phronomy
|
|
|
567
477
|
end
|
|
568
478
|
|
|
569
479
|
def check_starvation_lag(lag_ns, event)
|
|
570
|
-
threshold =
|
|
571
|
-
Phronomy.configuration
|
|
572
|
-
.event_loop_starvation_threshold_seconds
|
|
480
|
+
threshold = Phronomy.configuration.event_loop_starvation_threshold_seconds
|
|
573
481
|
return unless threshold
|
|
574
482
|
return unless lag_ns > (threshold * 1_000_000_000)
|
|
575
483
|
|
|
576
484
|
Phronomy.configuration.logger&.warn do
|
|
577
|
-
"[Phronomy::EventLoop] Starvation detected: " \
|
|
578
|
-
"event #{event.type.inspect} " \
|
|
485
|
+
"[Phronomy::EventLoop] Starvation detected: event #{event.type.inspect} " \
|
|
579
486
|
"for target #{event.target_id.inspect} waited " \
|
|
580
|
-
"#{format("%.3f", lag_ns / 1_000_000_000.0)}s " \
|
|
581
|
-
"
|
|
487
|
+
"#{format("%.3f", lag_ns / 1_000_000_000.0)}s in queue " \
|
|
488
|
+
"(threshold: #{threshold}s)"
|
|
582
489
|
end
|
|
583
490
|
end
|
|
584
491
|
|
|
585
492
|
def check_dispatch_time(dispatch_start_ns, event)
|
|
586
|
-
threshold =
|
|
587
|
-
Phronomy.configuration
|
|
588
|
-
.event_loop_dispatch_threshold_seconds
|
|
493
|
+
threshold = Phronomy.configuration.event_loop_dispatch_threshold_seconds
|
|
589
494
|
return unless threshold
|
|
590
495
|
|
|
591
496
|
elapsed_ns = monotonic_nanoseconds - dispatch_start_ns
|
|
592
|
-
return unless elapsed_ns >
|
|
593
|
-
(threshold * 1_000_000_000)
|
|
497
|
+
return unless elapsed_ns > (threshold * 1_000_000_000)
|
|
594
498
|
|
|
595
499
|
Phronomy.configuration.logger&.warn do
|
|
596
|
-
"[Phronomy::EventLoop] Long dispatch: " \
|
|
597
|
-
"event #{event.type.inspect} " \
|
|
500
|
+
"[Phronomy::EventLoop] Long dispatch: event #{event.type.inspect} " \
|
|
598
501
|
"for target #{event.target_id.inspect} took " \
|
|
599
|
-
"#{format("%.3f", elapsed_ns / 1_000_000_000.0)}s " \
|
|
600
|
-
"
|
|
601
|
-
"Consider moving blocking work to BlockingAdapterPool."
|
|
502
|
+
"#{format("%.3f", elapsed_ns / 1_000_000_000.0)}s on the EventLoop thread " \
|
|
503
|
+
"(threshold: #{threshold}s). Move blocking I/O to OffloadPool."
|
|
602
504
|
end
|
|
603
505
|
end
|
|
604
506
|
|
|
@@ -607,10 +509,7 @@ module Phronomy
|
|
|
607
509
|
end
|
|
608
510
|
|
|
609
511
|
def monotonic_nanoseconds
|
|
610
|
-
Process.clock_gettime(
|
|
611
|
-
Process::CLOCK_MONOTONIC,
|
|
612
|
-
:nanosecond
|
|
613
|
-
)
|
|
512
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
614
513
|
end
|
|
615
514
|
end
|
|
616
515
|
end
|