phronomy 0.14.0 → 0.15.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 +65 -0
- data/README.md +236 -57
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +500 -411
- data/lib/phronomy/agent/context/capability/base.rb +51 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +42 -6
- data/lib/phronomy/engine/event_loop.rb +269 -112
- data/lib/phronomy/engine/fsm_session.rb +180 -142
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +2 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/tools/mcp.rb +25 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +54 -102
- data/lib/phronomy/workflow_runner.rb +238 -300
- data/lib/phronomy.rb +6 -4
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +18 -7
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -352
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
# Runtime-owned event loop
|
|
5
|
-
#
|
|
6
|
-
# A dedicated real thread reads from a Runtime-local AsyncQueue and dispatches
|
|
7
|
-
# events to their target FSMSession. The EventLoop is created at most once by
|
|
8
|
-
# its owning Runtime and is never restarted after shutdown.
|
|
9
|
-
#
|
|
10
|
-
# FSMSession handlers run on the EventLoop thread. They must not perform
|
|
11
|
-
# blocking work or call synchronous invoke APIs from that thread.
|
|
4
|
+
# Runtime-owned FIFO event loop for FSMSession instances.
|
|
12
5
|
class EventLoop
|
|
13
6
|
SYSTEM_CHANNEL_ID = "__event_loop__"
|
|
14
7
|
|
|
8
|
+
QUEUE_BACKLOG_WARNING_THRESHOLD = 1_000
|
|
9
|
+
QUEUE_BACKLOG_WARNING_INTERVAL_SECONDS = 60.0
|
|
10
|
+
|
|
11
|
+
TERMINAL_MANAGEMENT_EVENTS = %i[finished halted error].freeze
|
|
12
|
+
private_constant :TERMINAL_MANAGEMENT_EVENTS
|
|
13
|
+
|
|
15
14
|
STOP = Object.new.freeze
|
|
16
15
|
private_constant :STOP
|
|
17
16
|
|
|
18
|
-
# @param runtime [Phronomy::Runtime] owning Runtime
|
|
19
|
-
# @api private
|
|
20
17
|
def initialize(runtime:)
|
|
21
18
|
@runtime = runtime
|
|
22
19
|
@queue = Phronomy::Concurrency::AsyncQueue.new
|
|
20
|
+
@queue_metrics_mutex = Mutex.new
|
|
21
|
+
@queue_depth = 0
|
|
22
|
+
@max_queue_depth = 0
|
|
23
|
+
@last_queue_backlog_warning_at = nil
|
|
24
|
+
|
|
25
|
+
# @fsms and @waiting are dispatcher-thread-owned.
|
|
23
26
|
@fsms = {}
|
|
24
27
|
@waiting = {}
|
|
25
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
|
+
@admitted_session_ids = Set.new
|
|
33
|
+
|
|
26
34
|
@lifecycle_mutex = Mutex.new
|
|
27
35
|
@idle_cond = ConditionVariable.new
|
|
28
36
|
@shutdown_mutex = Mutex.new
|
|
@@ -40,20 +48,14 @@ module Phronomy
|
|
|
40
48
|
@task = @runtime.__spawn_event_loop_service { run_loop }
|
|
41
49
|
end
|
|
42
50
|
|
|
43
|
-
# @return [Float]
|
|
44
|
-
# @api private
|
|
45
51
|
def last_lag_seconds
|
|
46
52
|
@lag_mutex.synchronize { @last_lag_ns } / 1_000_000_000.0
|
|
47
53
|
end
|
|
48
54
|
|
|
49
|
-
# @return [Float]
|
|
50
|
-
# @api private
|
|
51
55
|
def max_lag_seconds
|
|
52
56
|
@lag_mutex.synchronize { @max_lag_ns } / 1_000_000_000.0
|
|
53
57
|
end
|
|
54
58
|
|
|
55
|
-
# @return [Float]
|
|
56
|
-
# @api private
|
|
57
59
|
def average_lag_seconds
|
|
58
60
|
@lag_mutex.synchronize do
|
|
59
61
|
return 0.0 if @dispatch_count.zero?
|
|
@@ -62,82 +64,134 @@ module Phronomy
|
|
|
62
64
|
end
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# @return [Phronomy::Concurrency::AsyncQueue, Phronomy::Task]
|
|
74
|
-
# @api private
|
|
67
|
+
def queue_depth
|
|
68
|
+
@queue_metrics_mutex.synchronize { @queue_depth }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def max_queue_depth
|
|
72
|
+
@queue_metrics_mutex.synchronize { @max_queue_depth }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
75
|
def register(fsm_session, completion: nil)
|
|
76
76
|
if current? && !completion.is_a?(Phronomy::Task)
|
|
77
77
|
raise Phronomy::Error,
|
|
78
|
-
"Cannot call synchronous
|
|
78
|
+
"Cannot call a synchronous invocation API from an EventLoop action. " \
|
|
79
79
|
"Schedule work asynchronously instead."
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
completion_queue =
|
|
82
|
+
completion_queue =
|
|
83
|
+
completion || Phronomy::Concurrency::AsyncQueue.new
|
|
83
84
|
scheduler = Phronomy::Runtime::Scheduler.current
|
|
84
|
-
if scheduler &&
|
|
85
|
+
if scheduler &&
|
|
86
|
+
completion_queue.respond_to?(:expect_cross_thread_push)
|
|
85
87
|
completion_queue.expect_cross_thread_push(scheduler)
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
event = Phronomy::Event.new(
|
|
89
91
|
type: :start,
|
|
90
92
|
target_id: SYSTEM_CHANNEL_ID,
|
|
91
|
-
payload: {
|
|
93
|
+
payload: {
|
|
94
|
+
session: fsm_session,
|
|
95
|
+
completion: completion_queue
|
|
96
|
+
}
|
|
92
97
|
)
|
|
98
|
+
queued_depth = nil
|
|
93
99
|
|
|
94
100
|
@lifecycle_mutex.synchronize do
|
|
95
101
|
ensure_accepting_registrations!
|
|
102
|
+
if @admitted_session_ids.include?(fsm_session.id)
|
|
103
|
+
raise Phronomy::Error,
|
|
104
|
+
"FSMSession #{fsm_session.id.inspect} is already registered"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
@admitted_session_ids.add(fsm_session.id)
|
|
96
108
|
@outstanding_sessions += 1
|
|
97
109
|
begin
|
|
98
|
-
|
|
110
|
+
queued_depth = enqueue(
|
|
111
|
+
[event, monotonic_nanoseconds]
|
|
112
|
+
)
|
|
99
113
|
rescue
|
|
114
|
+
@admitted_session_ids.delete(fsm_session.id)
|
|
100
115
|
@outstanding_sessions -= 1
|
|
101
116
|
@idle_cond.broadcast if @outstanding_sessions.zero?
|
|
102
117
|
raise
|
|
103
118
|
end
|
|
104
119
|
end
|
|
105
120
|
|
|
121
|
+
check_queue_backlog(queued_depth, event)
|
|
106
122
|
completion_queue
|
|
107
123
|
end
|
|
108
124
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
# Async completion callbacks may race with shutdown, so rejection is a
|
|
112
|
-
# boolean result rather than an exception.
|
|
113
|
-
#
|
|
114
|
-
# @param event [Phronomy::Event]
|
|
115
|
-
# @return [Boolean]
|
|
116
|
-
# @api private
|
|
125
|
+
# Internal post operation. Management terminal events close admission for
|
|
126
|
+
# their session before the terminal event is enqueued.
|
|
117
127
|
def post(event)
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
queued_depth = nil
|
|
129
|
+
accepted = @lifecycle_mutex.synchronize do
|
|
130
|
+
next false unless accepting_events?
|
|
131
|
+
|
|
132
|
+
terminal_session_id = nil
|
|
133
|
+
if terminal_management_event?(event)
|
|
134
|
+
terminal_session_id = event.payload.fetch(:session_id)
|
|
135
|
+
@admitted_session_ids.delete(terminal_session_id)
|
|
136
|
+
end
|
|
120
137
|
|
|
121
|
-
|
|
138
|
+
begin
|
|
139
|
+
queued_depth = enqueue(
|
|
140
|
+
[event, monotonic_nanoseconds]
|
|
141
|
+
)
|
|
142
|
+
rescue
|
|
143
|
+
@admitted_session_ids.add(terminal_session_id) if terminal_session_id
|
|
144
|
+
raise
|
|
145
|
+
end
|
|
146
|
+
true
|
|
122
147
|
end
|
|
148
|
+
return false unless accepted
|
|
149
|
+
|
|
150
|
+
check_queue_backlog(queued_depth, event)
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
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
|
+
def post_to_session(event)
|
|
160
|
+
if event.target_id == SYSTEM_CHANNEL_ID
|
|
161
|
+
raise ArgumentError,
|
|
162
|
+
"post_to_session cannot target the system channel"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
queued_depth = nil
|
|
166
|
+
accepted = @lifecycle_mutex.synchronize do
|
|
167
|
+
next false unless accepting_events?
|
|
168
|
+
next false unless @admitted_session_ids.include?(event.target_id)
|
|
169
|
+
|
|
170
|
+
queued_depth = enqueue(
|
|
171
|
+
[event, monotonic_nanoseconds]
|
|
172
|
+
)
|
|
173
|
+
true
|
|
174
|
+
end
|
|
175
|
+
return false unless accepted
|
|
176
|
+
|
|
177
|
+
check_queue_backlog(queued_depth, event)
|
|
123
178
|
true
|
|
124
179
|
end
|
|
125
180
|
|
|
126
|
-
|
|
127
|
-
|
|
181
|
+
def admitted_session?(session_id)
|
|
182
|
+
@lifecycle_mutex.synchronize do
|
|
183
|
+
@admitted_session_ids.include?(session_id)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
128
187
|
def current?
|
|
129
188
|
Phronomy::Task.current.equal?(@task)
|
|
130
189
|
end
|
|
131
190
|
|
|
132
|
-
# @return [Symbol]
|
|
133
|
-
# @api private
|
|
134
191
|
def state
|
|
135
192
|
@lifecycle_mutex.synchronize { @state }
|
|
136
193
|
end
|
|
137
194
|
|
|
138
|
-
# Stops external admission at the Runtime boundary while allowing already
|
|
139
|
-
# accepted work to complete on this EventLoop.
|
|
140
|
-
# @api private
|
|
141
195
|
def begin_draining
|
|
142
196
|
@lifecycle_mutex.synchronize do
|
|
143
197
|
@state = :draining if @state == :running
|
|
@@ -145,16 +199,12 @@ module Phronomy
|
|
|
145
199
|
self
|
|
146
200
|
end
|
|
147
201
|
|
|
148
|
-
# @return [Boolean]
|
|
149
|
-
# @api private
|
|
150
202
|
def idle?
|
|
151
|
-
@lifecycle_mutex.synchronize
|
|
203
|
+
@lifecycle_mutex.synchronize do
|
|
204
|
+
@outstanding_sessions.zero?
|
|
205
|
+
end
|
|
152
206
|
end
|
|
153
207
|
|
|
154
|
-
# Waits until no queued :start or active session remains.
|
|
155
|
-
# @param deadline [Numeric] absolute monotonic deadline
|
|
156
|
-
# @return [Boolean] false on timeout
|
|
157
|
-
# @api private
|
|
158
208
|
def wait_until_idle(deadline)
|
|
159
209
|
@lifecycle_mutex.synchronize do
|
|
160
210
|
until @outstanding_sessions.zero?
|
|
@@ -167,16 +217,6 @@ module Phronomy
|
|
|
167
217
|
end
|
|
168
218
|
end
|
|
169
219
|
|
|
170
|
-
# Runtime-only terminal shutdown.
|
|
171
|
-
#
|
|
172
|
-
# On graceful timeout the dispatcher is cancelled. Queue cleanup is only
|
|
173
|
-
# performed after the dispatcher is confirmed dead, so there is never more
|
|
174
|
-
# than one queue consumer.
|
|
175
|
-
#
|
|
176
|
-
# @param deadline [Numeric] absolute monotonic deadline
|
|
177
|
-
# @param cancel_grace [Numeric] seconds to wait after Task#cancel!
|
|
178
|
-
# @return [Symbol] :terminated, :cancelled, :cancel_timeout, or :failed
|
|
179
|
-
# @api private
|
|
180
220
|
def shutdown(deadline:, cancel_grace:)
|
|
181
221
|
@shutdown_mutex.synchronize do
|
|
182
222
|
return @shutdown_status if @shutdown_status
|
|
@@ -193,18 +233,17 @@ module Phronomy
|
|
|
193
233
|
join_until(deadline)
|
|
194
234
|
end
|
|
195
235
|
|
|
196
|
-
@shutdown_status =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
:failed
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
236
|
+
@shutdown_status =
|
|
237
|
+
if task_alive?
|
|
238
|
+
cancel_and_cleanup(cancel_grace)
|
|
239
|
+
elsif state == :failed
|
|
240
|
+
:failed
|
|
241
|
+
else
|
|
242
|
+
finalize_terminated(:terminated)
|
|
243
|
+
end
|
|
203
244
|
end
|
|
204
245
|
end
|
|
205
246
|
|
|
206
|
-
# @return [Boolean]
|
|
207
|
-
# @api private
|
|
208
247
|
def task_alive?
|
|
209
248
|
@task&.alive? || false
|
|
210
249
|
end
|
|
@@ -213,7 +252,7 @@ module Phronomy
|
|
|
213
252
|
|
|
214
253
|
def run_loop
|
|
215
254
|
loop do
|
|
216
|
-
item =
|
|
255
|
+
item = dequeue
|
|
217
256
|
break if item.equal?(STOP)
|
|
218
257
|
|
|
219
258
|
event, posted_at_ns = item
|
|
@@ -227,7 +266,13 @@ module Phronomy
|
|
|
227
266
|
check_dispatch_time(dispatch_start_ns, event)
|
|
228
267
|
end
|
|
229
268
|
rescue Phronomy::CancellationError => error
|
|
230
|
-
|
|
269
|
+
if shutdown_cancel_requested?
|
|
270
|
+
cleanup_abandoned_work(
|
|
271
|
+
Phronomy::CancellationError.new(
|
|
272
|
+
"Runtime shutdown timed out"
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
else
|
|
231
276
|
notify_unexpected_dispatcher_failure(error)
|
|
232
277
|
raise
|
|
233
278
|
end
|
|
@@ -235,33 +280,41 @@ module Phronomy
|
|
|
235
280
|
notify_unexpected_dispatcher_failure(error)
|
|
236
281
|
raise
|
|
237
282
|
ensure
|
|
238
|
-
@lifecycle_mutex.synchronize
|
|
283
|
+
@lifecycle_mutex.synchronize do
|
|
284
|
+
@idle_cond.broadcast
|
|
285
|
+
end
|
|
239
286
|
end
|
|
240
287
|
|
|
241
288
|
def dispatch(event)
|
|
242
289
|
if event.target_id == SYSTEM_CHANNEL_ID
|
|
243
290
|
dispatch_management(event)
|
|
291
|
+
return
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
fsm = @fsms[event.target_id]
|
|
295
|
+
if fsm
|
|
296
|
+
fsm.handle(event)
|
|
244
297
|
else
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
warn "[Phronomy::EventLoop] Dropped event #{event.type.inspect} — " \
|
|
250
|
-
"no handler for target_id #{event.target_id.inspect}"
|
|
251
|
-
end
|
|
298
|
+
warn(
|
|
299
|
+
"[Phronomy::EventLoop] Dropped event #{event.type.inspect} — " \
|
|
300
|
+
"no handler for target_id #{event.target_id.inspect}"
|
|
301
|
+
)
|
|
252
302
|
end
|
|
253
303
|
end
|
|
254
304
|
|
|
255
305
|
def dispatch_management(event)
|
|
256
306
|
case event.type
|
|
257
307
|
when :finished, :halted, :error
|
|
258
|
-
session_id = event.payload
|
|
308
|
+
session_id = event.payload.fetch(:session_id)
|
|
259
309
|
session = @fsms.delete(session_id)
|
|
260
310
|
waiter = @waiting.delete(session_id)
|
|
261
|
-
complete_waiter(
|
|
311
|
+
complete_waiter(
|
|
312
|
+
waiter,
|
|
313
|
+
event.payload.fetch(:result)
|
|
314
|
+
)
|
|
262
315
|
decrement_outstanding if session
|
|
263
316
|
when :start
|
|
264
|
-
session = event.payload
|
|
317
|
+
session = event.payload.fetch(:session)
|
|
265
318
|
waiter = event.payload[:completion]
|
|
266
319
|
@fsms[session.id] = session
|
|
267
320
|
@waiting[session.id] = waiter if waiter
|
|
@@ -269,13 +322,20 @@ module Phronomy
|
|
|
269
322
|
end
|
|
270
323
|
end
|
|
271
324
|
|
|
325
|
+
def terminal_management_event?(event)
|
|
326
|
+
event.target_id == SYSTEM_CHANNEL_ID &&
|
|
327
|
+
TERMINAL_MANAGEMENT_EVENTS.include?(event.type) &&
|
|
328
|
+
event.payload.is_a?(Hash) &&
|
|
329
|
+
event.payload.key?(:session_id)
|
|
330
|
+
end
|
|
331
|
+
|
|
272
332
|
def begin_stopping_if_idle
|
|
273
333
|
@lifecycle_mutex.synchronize do
|
|
274
334
|
return false unless @state == :draining
|
|
275
335
|
return false unless @outstanding_sessions.zero?
|
|
276
336
|
|
|
277
337
|
@state = :stopping
|
|
278
|
-
|
|
338
|
+
enqueue(STOP)
|
|
279
339
|
true
|
|
280
340
|
end
|
|
281
341
|
end
|
|
@@ -295,33 +355,43 @@ module Phronomy
|
|
|
295
355
|
end
|
|
296
356
|
|
|
297
357
|
if task&.alive?
|
|
298
|
-
@lifecycle_mutex.synchronize
|
|
358
|
+
@lifecycle_mutex.synchronize do
|
|
359
|
+
@state = :failed
|
|
360
|
+
end
|
|
299
361
|
return :cancel_timeout
|
|
300
362
|
end
|
|
301
363
|
|
|
302
364
|
return :failed if state == :failed
|
|
303
365
|
|
|
304
366
|
cleanup_abandoned_work(
|
|
305
|
-
Phronomy::CancellationError.new(
|
|
367
|
+
Phronomy::CancellationError.new(
|
|
368
|
+
"Runtime shutdown timed out"
|
|
369
|
+
)
|
|
306
370
|
)
|
|
307
371
|
finalize_terminated(:cancelled)
|
|
308
372
|
end
|
|
309
373
|
|
|
310
|
-
# Called only after the dispatcher is confirmed dead.
|
|
311
374
|
def cleanup_abandoned_work(error)
|
|
312
375
|
drain_queued_items.each do |item|
|
|
313
376
|
next if item.equal?(STOP)
|
|
314
377
|
|
|
315
378
|
event, = item
|
|
316
|
-
next unless event.target_id == SYSTEM_CHANNEL_ID
|
|
379
|
+
next unless event.target_id == SYSTEM_CHANNEL_ID
|
|
380
|
+
next unless event.type == :start
|
|
317
381
|
|
|
318
|
-
complete_waiter(
|
|
382
|
+
complete_waiter(
|
|
383
|
+
event.payload[:completion],
|
|
384
|
+
error
|
|
385
|
+
)
|
|
319
386
|
end
|
|
320
387
|
|
|
321
|
-
@waiting.values.each
|
|
388
|
+
@waiting.values.each do |waiter|
|
|
389
|
+
complete_waiter(waiter, error)
|
|
390
|
+
end
|
|
322
391
|
@waiting.clear
|
|
323
392
|
@fsms.clear
|
|
324
393
|
@lifecycle_mutex.synchronize do
|
|
394
|
+
@admitted_session_ids.clear
|
|
325
395
|
@outstanding_sessions = 0
|
|
326
396
|
@idle_cond.broadcast
|
|
327
397
|
end
|
|
@@ -330,7 +400,7 @@ module Phronomy
|
|
|
330
400
|
def drain_queued_items
|
|
331
401
|
items = []
|
|
332
402
|
loop do
|
|
333
|
-
item =
|
|
403
|
+
item = dequeue(timeout: 0)
|
|
334
404
|
break unless item
|
|
335
405
|
|
|
336
406
|
items << item
|
|
@@ -338,14 +408,15 @@ module Phronomy
|
|
|
338
408
|
items
|
|
339
409
|
end
|
|
340
410
|
|
|
341
|
-
# Framework failures are reported and made terminal. No automatic restart,
|
|
342
|
-
# replay, or pending-queue recovery is attempted.
|
|
343
411
|
def notify_unexpected_dispatcher_failure(error)
|
|
344
412
|
@lifecycle_mutex.synchronize do
|
|
345
413
|
@state = :failed
|
|
414
|
+
@admitted_session_ids.clear
|
|
346
415
|
@idle_cond.broadcast
|
|
347
416
|
end
|
|
348
|
-
@waiting.values.each
|
|
417
|
+
@waiting.values.each do |waiter|
|
|
418
|
+
complete_waiter(waiter, error)
|
|
419
|
+
end
|
|
349
420
|
@runtime.__event_loop_failed(error)
|
|
350
421
|
end
|
|
351
422
|
|
|
@@ -368,7 +439,9 @@ module Phronomy
|
|
|
368
439
|
|
|
369
440
|
def decrement_outstanding
|
|
370
441
|
@lifecycle_mutex.synchronize do
|
|
371
|
-
|
|
442
|
+
if @outstanding_sessions.positive?
|
|
443
|
+
@outstanding_sessions -= 1
|
|
444
|
+
end
|
|
372
445
|
@idle_cond.broadcast if @outstanding_sessions.zero?
|
|
373
446
|
end
|
|
374
447
|
end
|
|
@@ -387,6 +460,7 @@ module Phronomy
|
|
|
387
460
|
def finalize_terminated(status)
|
|
388
461
|
@lifecycle_mutex.synchronize do
|
|
389
462
|
@state = :terminated
|
|
463
|
+
@admitted_session_ids.clear
|
|
390
464
|
@task = nil unless @task&.alive?
|
|
391
465
|
@idle_cond.broadcast
|
|
392
466
|
end
|
|
@@ -402,13 +476,84 @@ module Phronomy
|
|
|
402
476
|
waiter.transition!(:failed, error: payload)
|
|
403
477
|
else
|
|
404
478
|
waiter.backend.unblock(payload, nil)
|
|
405
|
-
waiter.transition!(
|
|
479
|
+
waiter.transition!(
|
|
480
|
+
:completed,
|
|
481
|
+
value: payload
|
|
482
|
+
)
|
|
406
483
|
end
|
|
407
484
|
else
|
|
408
485
|
waiter.push(payload)
|
|
409
486
|
end
|
|
410
487
|
end
|
|
411
488
|
|
|
489
|
+
def enqueue(item)
|
|
490
|
+
depth = @queue_metrics_mutex.synchronize do
|
|
491
|
+
@queue_depth += 1
|
|
492
|
+
if @queue_depth > @max_queue_depth
|
|
493
|
+
@max_queue_depth = @queue_depth
|
|
494
|
+
end
|
|
495
|
+
@queue_depth
|
|
496
|
+
end
|
|
497
|
+
@queue.push(item)
|
|
498
|
+
depth
|
|
499
|
+
rescue
|
|
500
|
+
@queue_metrics_mutex.synchronize do
|
|
501
|
+
@queue_depth -= 1 if @queue_depth.positive?
|
|
502
|
+
end
|
|
503
|
+
raise
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def dequeue(timeout: nil)
|
|
507
|
+
item = nil
|
|
508
|
+
begin
|
|
509
|
+
item = @queue.pop(timeout: timeout)
|
|
510
|
+
ensure
|
|
511
|
+
if item
|
|
512
|
+
@queue_metrics_mutex.synchronize do
|
|
513
|
+
@queue_depth -= 1 if @queue_depth.positive?
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
item
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def check_queue_backlog(depth, event)
|
|
521
|
+
return unless depth >= QUEUE_BACKLOG_WARNING_THRESHOLD
|
|
522
|
+
|
|
523
|
+
now = monotonic_now
|
|
524
|
+
max_depth = nil
|
|
525
|
+
should_warn = @queue_metrics_mutex.synchronize do
|
|
526
|
+
last = @last_queue_backlog_warning_at
|
|
527
|
+
if last &&
|
|
528
|
+
(now - last) <
|
|
529
|
+
QUEUE_BACKLOG_WARNING_INTERVAL_SECONDS
|
|
530
|
+
next false
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
@last_queue_backlog_warning_at = now
|
|
534
|
+
max_depth = @max_queue_depth
|
|
535
|
+
true
|
|
536
|
+
end
|
|
537
|
+
return unless should_warn
|
|
538
|
+
|
|
539
|
+
warn_queue_backlog(
|
|
540
|
+
"[Phronomy::EventLoop] Queue backlog is high: " \
|
|
541
|
+
"depth=#{depth} max_depth=#{max_depth} " \
|
|
542
|
+
"threshold=#{QUEUE_BACKLOG_WARNING_THRESHOLD} " \
|
|
543
|
+
"event=#{event.type.inspect} " \
|
|
544
|
+
"target_id=#{event.target_id.inspect}. " \
|
|
545
|
+
"Events are not dropped; inspect slow callbacks or " \
|
|
546
|
+
"high streaming concurrency."
|
|
547
|
+
)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def warn_queue_backlog(message)
|
|
551
|
+
logger = Phronomy.configuration.logger
|
|
552
|
+
logger ? logger.warn(message) : Kernel.warn(message)
|
|
553
|
+
rescue
|
|
554
|
+
nil
|
|
555
|
+
end
|
|
556
|
+
|
|
412
557
|
def update_lag_metrics(lag_ns)
|
|
413
558
|
@lag_mutex.synchronize do
|
|
414
559
|
@last_lag_ns = lag_ns
|
|
@@ -419,29 +564,38 @@ module Phronomy
|
|
|
419
564
|
end
|
|
420
565
|
|
|
421
566
|
def check_starvation_lag(lag_ns, event)
|
|
422
|
-
threshold =
|
|
423
|
-
|
|
567
|
+
threshold =
|
|
568
|
+
Phronomy.configuration
|
|
569
|
+
.event_loop_starvation_threshold_seconds
|
|
570
|
+
return unless threshold
|
|
571
|
+
return unless lag_ns > (threshold * 1_000_000_000)
|
|
424
572
|
|
|
425
573
|
Phronomy.configuration.logger&.warn do
|
|
426
|
-
"[Phronomy::EventLoop] Starvation detected:
|
|
574
|
+
"[Phronomy::EventLoop] Starvation detected: " \
|
|
575
|
+
"event #{event.type.inspect} " \
|
|
427
576
|
"for target #{event.target_id.inspect} waited " \
|
|
428
|
-
"#{format("%.3f", lag_ns / 1_000_000_000.0)}s
|
|
429
|
-
"(threshold: #{threshold}s)"
|
|
577
|
+
"#{format("%.3f", lag_ns / 1_000_000_000.0)}s " \
|
|
578
|
+
"in queue (threshold: #{threshold}s)"
|
|
430
579
|
end
|
|
431
580
|
end
|
|
432
581
|
|
|
433
582
|
def check_dispatch_time(dispatch_start_ns, event)
|
|
434
|
-
threshold =
|
|
583
|
+
threshold =
|
|
584
|
+
Phronomy.configuration
|
|
585
|
+
.event_loop_dispatch_threshold_seconds
|
|
435
586
|
return unless threshold
|
|
436
587
|
|
|
437
588
|
elapsed_ns = monotonic_nanoseconds - dispatch_start_ns
|
|
438
|
-
return unless elapsed_ns >
|
|
589
|
+
return unless elapsed_ns >
|
|
590
|
+
(threshold * 1_000_000_000)
|
|
439
591
|
|
|
440
592
|
Phronomy.configuration.logger&.warn do
|
|
441
|
-
"[Phronomy::EventLoop] Long dispatch:
|
|
593
|
+
"[Phronomy::EventLoop] Long dispatch: " \
|
|
594
|
+
"event #{event.type.inspect} " \
|
|
442
595
|
"for target #{event.target_id.inspect} took " \
|
|
443
|
-
"#{format("%.3f", elapsed_ns / 1_000_000_000.0)}s
|
|
444
|
-
"(threshold: #{threshold}s).
|
|
596
|
+
"#{format("%.3f", elapsed_ns / 1_000_000_000.0)}s " \
|
|
597
|
+
"on the EventLoop thread (threshold: #{threshold}s). " \
|
|
598
|
+
"Consider moving blocking work to BlockingAdapterPool."
|
|
445
599
|
end
|
|
446
600
|
end
|
|
447
601
|
|
|
@@ -450,7 +604,10 @@ module Phronomy
|
|
|
450
604
|
end
|
|
451
605
|
|
|
452
606
|
def monotonic_nanoseconds
|
|
453
|
-
Process.clock_gettime(
|
|
607
|
+
Process.clock_gettime(
|
|
608
|
+
Process::CLOCK_MONOTONIC,
|
|
609
|
+
:nanosecond
|
|
610
|
+
)
|
|
454
611
|
end
|
|
455
612
|
end
|
|
456
613
|
end
|