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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +236 -57
  4. data/benchmark/bench_agent_invoke.rb +2 -3
  5. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
  6. data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
  7. data/examples/workflows/agent_event_mapping.rb +104 -0
  8. data/examples/workflows/generic_task_event_mapping.rb +58 -0
  9. data/lib/phronomy/agent/agent_invocation.rb +385 -0
  10. data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
  11. data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
  12. data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
  13. data/lib/phronomy/agent/async_event_api.rb +471 -0
  14. data/lib/phronomy/agent/base.rb +500 -411
  15. data/lib/phronomy/agent/context/capability/base.rb +51 -119
  16. data/lib/phronomy/agent/llm_operation_result.rb +23 -0
  17. data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
  18. data/lib/phronomy/agent/tool_approval_request.rb +121 -0
  19. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
  20. data/lib/phronomy/agent/tool_executor.rb +47 -69
  21. data/lib/phronomy/agent/tool_invocation.rb +634 -0
  22. data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
  23. data/lib/phronomy/agent.rb +21 -9
  24. data/lib/phronomy/configuration.rb +42 -6
  25. data/lib/phronomy/engine/event_loop.rb +269 -112
  26. data/lib/phronomy/engine/fsm_session.rb +180 -142
  27. data/lib/phronomy/engine/task.rb +5 -10
  28. data/lib/phronomy/event.rb +8 -8
  29. data/lib/phronomy/generator_verifier.rb +253 -142
  30. data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
  31. data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
  32. data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
  33. data/lib/phronomy/invocation_context.rb +5 -19
  34. data/lib/phronomy/llm_adapter/base.rb +25 -34
  35. data/lib/phronomy/metrics.rb +2 -0
  36. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
  37. data/lib/phronomy/stream_callback_error.rb +35 -0
  38. data/lib/phronomy/tools/mcp.rb +25 -0
  39. data/lib/phronomy/version.rb +1 -1
  40. data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
  41. data/lib/phronomy/workflow.rb +122 -261
  42. data/lib/phronomy/workflow_context.rb +54 -102
  43. data/lib/phronomy/workflow_runner.rb +238 -300
  44. data/lib/phronomy.rb +6 -4
  45. data/scripts/check_readme_runnable.rb +4 -1
  46. metadata +18 -7
  47. data/lib/phronomy/agent/concerns/retryable.rb +0 -103
  48. data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
  49. data/lib/phronomy/agent/invocation_context.rb +0 -171
  50. data/lib/phronomy/agent/invocation_session.rb +0 -352
  51. 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 that manages all FSMSession instances.
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
- # Registers an FSMSession and returns its completion queue.
66
- #
67
- # +outstanding_sessions+ is incremented before the :start event is enqueued,
68
- # so shutdown also accounts for accepted sessions that have not yet been
69
- # dispatched.
70
- #
71
- # @param fsm_session [Phronomy::FSMSession]
72
- # @param completion [Phronomy::Task, nil]
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 Workflow#invoke from an EventLoop action. " \
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 = completion || Phronomy::Concurrency::AsyncQueue.new
82
+ completion_queue =
83
+ completion || Phronomy::Concurrency::AsyncQueue.new
83
84
  scheduler = Phronomy::Runtime::Scheduler.current
84
- if scheduler && completion_queue.respond_to?(:expect_cross_thread_push)
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: {session: fsm_session, completion: completion_queue}
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
- @queue.push([event, monotonic_nanoseconds])
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
- # Posts an event. Returns false after stopping begins.
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
- @lifecycle_mutex.synchronize do
119
- return false unless accepting_events?
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
- @queue.push([event, monotonic_nanoseconds])
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
- # Returns true only for this EventLoop's dispatcher Task.
127
- # @api private
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 { @outstanding_sessions.zero? }
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 = if task_alive?
197
- cancel_and_cleanup(cancel_grace)
198
- elsif state == :failed
199
- :failed
200
- else
201
- finalize_terminated(:terminated)
202
- end
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 = @queue.pop
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
- unless shutdown_cancel_requested?
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 { @idle_cond.broadcast }
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
- fsm = @fsms[event.target_id]
246
- if fsm
247
- fsm.handle(event)
248
- else
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[:session_id]
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(waiter, event.payload[:result])
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[:session]
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
- @queue.push(STOP)
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 { @state = :failed }
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("Runtime shutdown timed out")
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 && event.type == :start
379
+ next unless event.target_id == SYSTEM_CHANNEL_ID
380
+ next unless event.type == :start
317
381
 
318
- complete_waiter(event.payload[:completion], error)
382
+ complete_waiter(
383
+ event.payload[:completion],
384
+ error
385
+ )
319
386
  end
320
387
 
321
- @waiting.values.each { |waiter| complete_waiter(waiter, error) }
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 = @queue.pop(timeout: 0)
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 { |waiter| complete_waiter(waiter, error) }
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
- @outstanding_sessions -= 1 if @outstanding_sessions.positive?
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!(:completed, value: payload)
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 = Phronomy.configuration.event_loop_starvation_threshold_seconds
423
- return unless threshold && lag_ns > (threshold * 1_000_000_000)
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: event #{event.type.inspect} " \
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 in queue " \
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 = Phronomy.configuration.event_loop_dispatch_threshold_seconds
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 > (threshold * 1_000_000_000)
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: event #{event.type.inspect} " \
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 on the EventLoop thread " \
444
- "(threshold: #{threshold}s). Consider moving blocking work to BlockingAdapterPool."
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(Process::CLOCK_MONOTONIC, :nanosecond)
607
+ Process.clock_gettime(
608
+ Process::CLOCK_MONOTONIC,
609
+ :nanosecond
610
+ )
454
611
  end
455
612
  end
456
613
  end